@robinmordasiewicz/f5xc-terraform-mcp 3.8.28 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +34 -4
- package/dist/index.js.map +1 -1
- package/dist/metadata/resource-metadata.json +1 -1
- package/dist/metadata/validation-patterns.json +1 -1
- package/dist/schemas/common.d.ts +24 -3
- package/dist/schemas/common.d.ts.map +1 -1
- package/dist/schemas/common.js +18 -2
- package/dist/schemas/common.js.map +1 -1
- package/dist/tools/auth.d.ts +30 -0
- package/dist/tools/auth.d.ts.map +1 -0
- package/dist/tools/auth.js +364 -0
- package/dist/tools/auth.js.map +1 -0
- package/dist/tools/discover.d.ts.map +1 -1
- package/dist/tools/discover.js +51 -1
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/metadata.d.ts.map +1 -1
- package/dist/tools/metadata.js +1162 -1
- package/dist/tools/metadata.js.map +1 -1
- package/package.json +4 -2
package/dist/tools/metadata.js
CHANGED
|
@@ -44,6 +44,12 @@ export async function handleMetadata(input) {
|
|
|
44
44
|
return handleSummary(response_format);
|
|
45
45
|
case 'syntax':
|
|
46
46
|
return handleSyntax(input, response_format);
|
|
47
|
+
case 'validate':
|
|
48
|
+
return handleValidate(input, response_format);
|
|
49
|
+
case 'example':
|
|
50
|
+
return handleExample(input, response_format);
|
|
51
|
+
case 'mistakes':
|
|
52
|
+
return handleMistakes(input, response_format);
|
|
47
53
|
default:
|
|
48
54
|
throw new Error(`Unknown operation: ${operation}`);
|
|
49
55
|
}
|
|
@@ -698,6 +704,836 @@ function handleSyntax(input, format) {
|
|
|
698
704
|
// Return the generated markdown guide
|
|
699
705
|
return guide.syntaxGuide;
|
|
700
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Validates a Terraform config snippet for common syntax errors
|
|
709
|
+
* Phase 2: Now detects unsupported arguments/blocks, not just boolean assignment
|
|
710
|
+
*/
|
|
711
|
+
function handleValidate(input, format) {
|
|
712
|
+
const { resource, config } = input;
|
|
713
|
+
if (!resource) {
|
|
714
|
+
return formatError('Missing parameter', 'resource parameter is required for validate operation', format);
|
|
715
|
+
}
|
|
716
|
+
if (!config) {
|
|
717
|
+
return formatError('Missing parameter', 'config parameter is required for validate operation. Provide a Terraform config snippet to validate.', format);
|
|
718
|
+
}
|
|
719
|
+
const resourceMeta = getResourceMetadata(resource);
|
|
720
|
+
if (!resourceMeta) {
|
|
721
|
+
return formatError('Resource not found', `Resource '${resource}' not found in metadata`, format);
|
|
722
|
+
}
|
|
723
|
+
const errors = [];
|
|
724
|
+
const validFields = Object.keys(resourceMeta.attributes);
|
|
725
|
+
// Phase 1: Check for boolean assignment errors (existing)
|
|
726
|
+
const booleanAssignmentPattern = /^\s*(\w+)\s*=\s*(true|false)\s*$/gm;
|
|
727
|
+
let match;
|
|
728
|
+
while ((match = booleanAssignmentPattern.exec(config)) !== null) {
|
|
729
|
+
const fieldName = match[1];
|
|
730
|
+
const value = match[2];
|
|
731
|
+
const attrMeta = resourceMeta.attributes[fieldName];
|
|
732
|
+
if (attrMeta && attrMeta.is_block && attrMeta.type === 'object') {
|
|
733
|
+
errors.push({
|
|
734
|
+
type: 'boolean_assignment',
|
|
735
|
+
field: fieldName,
|
|
736
|
+
found: `${fieldName} = ${value}`,
|
|
737
|
+
expected: `${fieldName} {}`,
|
|
738
|
+
reason: attrMeta.oneof_group
|
|
739
|
+
? `This is a OneOf choice field in group '${attrMeta.oneof_group}', not a boolean`
|
|
740
|
+
: 'This is a block-type attribute, not a boolean',
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// Phase 2: Parse HCL and check for unsupported fields
|
|
745
|
+
const parsed = parseHCLForFields(config);
|
|
746
|
+
// Check each parsed field against the schema
|
|
747
|
+
for (const field of parsed.fields) {
|
|
748
|
+
// Skip standard fields that are always valid at root level
|
|
749
|
+
if (!field.parent && ['name', 'namespace', 'description', 'labels', 'annotations'].includes(field.name)) {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
// Check if field exists in resource schema
|
|
753
|
+
const parentMeta = field.parent ? resourceMeta.attributes[field.parent] : null;
|
|
754
|
+
// Strategy 1: Check if parent is a KNOWN reference block (handles nested cases like pool inside origin_pools_weights)
|
|
755
|
+
// This catches cases where the parent block name is in KNOWN_REFERENCE_BLOCKS even if
|
|
756
|
+
// we don't have schema metadata for it (e.g., nested blocks)
|
|
757
|
+
if (field.parent && KNOWN_REFERENCE_BLOCKS.has(field.parent)) {
|
|
758
|
+
const allowedFields = ['name', 'namespace', 'tenant'];
|
|
759
|
+
if (!allowedFields.includes(field.name)) {
|
|
760
|
+
const refResource = getReferenceResourceName(field.parent);
|
|
761
|
+
errors.push({
|
|
762
|
+
type: field.type === 'block' ? 'unsupported_block' : 'reference_inline',
|
|
763
|
+
field: field.name,
|
|
764
|
+
line: field.line,
|
|
765
|
+
location: `Inside \`${field.parent}\` block`,
|
|
766
|
+
found: field.type === 'block' ? `${field.name} {}` : `${field.name} = ${field.value || '...'}`,
|
|
767
|
+
expected: `Only name, namespace, tenant are valid inside ${field.parent}`,
|
|
768
|
+
reason: `\`${field.parent}\` is a reference block - it references a separate resource, not inline configuration`,
|
|
769
|
+
suggestions: [`Create a separate ${refResource} resource with these parameters`],
|
|
770
|
+
referenceResource: refResource,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
continue; // Skip further checks for this field
|
|
774
|
+
}
|
|
775
|
+
// Strategy 2: Check parent using schema metadata (handles top-level blocks)
|
|
776
|
+
if (field.parent && parentMeta) {
|
|
777
|
+
// Field is inside a parent block - check if parent is a reference block via schema
|
|
778
|
+
if (isReferenceBlock(parentMeta, field.parent)) {
|
|
779
|
+
const allowedFields = ['name', 'namespace', 'tenant'];
|
|
780
|
+
if (!allowedFields.includes(field.name)) {
|
|
781
|
+
const refResource = getReferenceResourceName(field.parent);
|
|
782
|
+
errors.push({
|
|
783
|
+
type: field.type === 'block' ? 'unsupported_block' : 'reference_inline',
|
|
784
|
+
field: field.name,
|
|
785
|
+
line: field.line,
|
|
786
|
+
location: `Inside \`${field.parent}\` block`,
|
|
787
|
+
found: field.type === 'block' ? `${field.name} {}` : `${field.name} = ${field.value || '...'}`,
|
|
788
|
+
expected: `Only name, namespace, tenant are valid inside ${field.parent}`,
|
|
789
|
+
reason: `\`${field.parent}\` is a reference block - it references a separate resource, not inline configuration`,
|
|
790
|
+
suggestions: [`Create a separate ${refResource} resource with these parameters`],
|
|
791
|
+
referenceResource: refResource,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
// Note: For non-reference blocks without nested_attributes in schema,
|
|
796
|
+
// we can't validate nested fields - skip validation for these cases
|
|
797
|
+
}
|
|
798
|
+
else if (!field.parent) {
|
|
799
|
+
// Field is at root level - check against resource schema
|
|
800
|
+
if (!resourceMeta.attributes[field.name]) {
|
|
801
|
+
const similar = findSimilarFieldNames(field.name, validFields);
|
|
802
|
+
errors.push({
|
|
803
|
+
type: field.type === 'block' ? 'unsupported_block' : 'unsupported_argument',
|
|
804
|
+
field: field.name,
|
|
805
|
+
line: field.line,
|
|
806
|
+
found: field.type === 'block' ? `${field.name} {}` : `${field.name} = ${field.value || '...'}`,
|
|
807
|
+
expected: `Valid attributes for ${resource}`,
|
|
808
|
+
reason: `An ${field.type === 'block' ? 'block' : 'argument'} named "${field.name}" is not expected here`,
|
|
809
|
+
suggestions: similar.length > 0 ? [`Did you mean: ${similar.join(', ')}?`] : undefined,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Build response
|
|
815
|
+
if (errors.length === 0) {
|
|
816
|
+
if (format === ResponseFormat.JSON) {
|
|
817
|
+
return JSON.stringify({
|
|
818
|
+
operation: 'validate',
|
|
819
|
+
resource,
|
|
820
|
+
status: 'valid',
|
|
821
|
+
errors: [],
|
|
822
|
+
message: 'No syntax errors detected in the provided configuration.',
|
|
823
|
+
}, null, 2);
|
|
824
|
+
}
|
|
825
|
+
return [
|
|
826
|
+
`# Validation Results: ${resource}`,
|
|
827
|
+
'',
|
|
828
|
+
'✅ **No syntax errors detected**',
|
|
829
|
+
'',
|
|
830
|
+
'The provided configuration appears to use correct syntax.',
|
|
831
|
+
'',
|
|
832
|
+
'*Note: This validates common syntax patterns only. Run `terraform validate` for complete validation.*',
|
|
833
|
+
].join('\n');
|
|
834
|
+
}
|
|
835
|
+
// Errors found
|
|
836
|
+
if (format === ResponseFormat.JSON) {
|
|
837
|
+
return JSON.stringify({
|
|
838
|
+
operation: 'validate',
|
|
839
|
+
resource,
|
|
840
|
+
status: 'invalid',
|
|
841
|
+
errors,
|
|
842
|
+
error_count: errors.length,
|
|
843
|
+
}, null, 2);
|
|
844
|
+
}
|
|
845
|
+
const lines = [
|
|
846
|
+
`# Validation Results: ${errors.length} Error(s) Found`,
|
|
847
|
+
'',
|
|
848
|
+
];
|
|
849
|
+
for (let i = 0; i < errors.length; i++) {
|
|
850
|
+
const err = errors[i];
|
|
851
|
+
const errorTitle = getErrorTitle(err.type);
|
|
852
|
+
lines.push(`## Error ${i + 1}: ${errorTitle}`);
|
|
853
|
+
lines.push('');
|
|
854
|
+
lines.push(`- **Field**: \`${err.field}\``);
|
|
855
|
+
if (err.line) {
|
|
856
|
+
lines.push(`- **Line**: ${err.line}`);
|
|
857
|
+
}
|
|
858
|
+
if (err.location) {
|
|
859
|
+
lines.push(`- **Location**: ${err.location}`);
|
|
860
|
+
}
|
|
861
|
+
lines.push(`- **Found**: \`${err.found}\``);
|
|
862
|
+
lines.push(`- **Expected**: ${err.expected}`);
|
|
863
|
+
lines.push(`- **Reason**: ${err.reason}`);
|
|
864
|
+
if (err.suggestions && err.suggestions.length > 0) {
|
|
865
|
+
lines.push(`- **Suggestion**: ${err.suggestions.join('; ')}`);
|
|
866
|
+
}
|
|
867
|
+
lines.push('');
|
|
868
|
+
}
|
|
869
|
+
// Add corrected pattern for reference block errors
|
|
870
|
+
const refErrors = errors.filter(e => e.type === 'reference_inline' || (e.type === 'unsupported_block' && e.referenceResource));
|
|
871
|
+
if (refErrors.length > 0) {
|
|
872
|
+
const refErr = refErrors[0];
|
|
873
|
+
const parentBlock = refErr.location?.match(/`(\w+)`/)?.[1] || 'unknown';
|
|
874
|
+
const refResource = refErr.referenceResource || `f5xc_${parentBlock}`;
|
|
875
|
+
lines.push('## Correct Pattern');
|
|
876
|
+
lines.push('');
|
|
877
|
+
lines.push('```hcl');
|
|
878
|
+
lines.push(`# Create the ${parentBlock} resource separately`);
|
|
879
|
+
lines.push(`resource "${refResource}" "example" {`);
|
|
880
|
+
lines.push(` name = "my-${parentBlock}"`);
|
|
881
|
+
lines.push(' namespace = "default"');
|
|
882
|
+
lines.push('');
|
|
883
|
+
lines.push(' # Add your configuration here');
|
|
884
|
+
lines.push(' # ...');
|
|
885
|
+
lines.push('}');
|
|
886
|
+
lines.push('');
|
|
887
|
+
lines.push(`# Reference it in ${resource}`);
|
|
888
|
+
lines.push(`resource "f5xc_${resource}" "example" {`);
|
|
889
|
+
lines.push(` ${parentBlock} {`);
|
|
890
|
+
lines.push(` name = ${refResource}.example.name`);
|
|
891
|
+
lines.push(' namespace = "default"');
|
|
892
|
+
lines.push(' }');
|
|
893
|
+
lines.push('}');
|
|
894
|
+
lines.push('```');
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
// For non-reference errors, show corrected config
|
|
898
|
+
const boolErrors = errors.filter(e => e.type === 'boolean_assignment');
|
|
899
|
+
if (boolErrors.length > 0) {
|
|
900
|
+
lines.push('## Corrected Configuration');
|
|
901
|
+
lines.push('');
|
|
902
|
+
lines.push('```hcl');
|
|
903
|
+
lines.push(generateCorrectedConfig(config, boolErrors));
|
|
904
|
+
lines.push('```');
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return lines.join('\n');
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Get human-readable error title
|
|
911
|
+
*/
|
|
912
|
+
function getErrorTitle(type) {
|
|
913
|
+
switch (type) {
|
|
914
|
+
case 'boolean_assignment':
|
|
915
|
+
return 'Invalid boolean assignment';
|
|
916
|
+
case 'unsupported_argument':
|
|
917
|
+
return 'Unsupported argument';
|
|
918
|
+
case 'unsupported_block':
|
|
919
|
+
return 'Unsupported block type';
|
|
920
|
+
case 'reference_inline':
|
|
921
|
+
return 'Inline configuration in reference block';
|
|
922
|
+
default:
|
|
923
|
+
return 'Validation error';
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Generates corrected config by replacing boolean assignments with empty blocks
|
|
928
|
+
*/
|
|
929
|
+
function generateCorrectedConfig(config, errors) {
|
|
930
|
+
let corrected = config;
|
|
931
|
+
for (const err of errors) {
|
|
932
|
+
if (err.type === 'boolean_assignment') {
|
|
933
|
+
// Replace "field = true" or "field = false" with "field {}"
|
|
934
|
+
const pattern = new RegExp(`\\b${err.field}\\s*=\\s*(true|false)`, 'g');
|
|
935
|
+
corrected = corrected.replace(pattern, `${err.field} {}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return corrected;
|
|
939
|
+
}
|
|
940
|
+
// =============================================================================
|
|
941
|
+
// EXAMPLE OPERATION HANDLER
|
|
942
|
+
// =============================================================================
|
|
943
|
+
/**
|
|
944
|
+
* Generates complete, syntactically correct Terraform examples
|
|
945
|
+
*/
|
|
946
|
+
function handleExample(input, format) {
|
|
947
|
+
const { resource, pattern = 'basic' } = input;
|
|
948
|
+
if (!resource) {
|
|
949
|
+
return formatError('Missing parameter', 'resource parameter is required for example operation', format);
|
|
950
|
+
}
|
|
951
|
+
// Get example based on resource type and pattern
|
|
952
|
+
const example = getExampleForResource(resource, pattern);
|
|
953
|
+
if (!example) {
|
|
954
|
+
return formatError('Example not found', `No example available for resource '${resource}' with pattern '${pattern}'. Available patterns: basic, with_waf, with_bot_defense, with_rate_limiting, full`, format);
|
|
955
|
+
}
|
|
956
|
+
if (format === ResponseFormat.JSON) {
|
|
957
|
+
return JSON.stringify({
|
|
958
|
+
operation: 'example',
|
|
959
|
+
resource,
|
|
960
|
+
pattern,
|
|
961
|
+
...example,
|
|
962
|
+
}, null, 2);
|
|
963
|
+
}
|
|
964
|
+
return example.markdown;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Returns example configuration for a resource
|
|
968
|
+
*/
|
|
969
|
+
function getExampleForResource(resource, pattern) {
|
|
970
|
+
// HTTP Load Balancer examples
|
|
971
|
+
if (resource === 'http_loadbalancer') {
|
|
972
|
+
return getHttpLoadbalancerExample(pattern);
|
|
973
|
+
}
|
|
974
|
+
// Origin Pool examples
|
|
975
|
+
if (resource === 'origin_pool') {
|
|
976
|
+
return getOriginPoolExample(pattern);
|
|
977
|
+
}
|
|
978
|
+
// Namespace example
|
|
979
|
+
if (resource === 'namespace') {
|
|
980
|
+
return {
|
|
981
|
+
description: 'Basic namespace configuration',
|
|
982
|
+
terraform: `resource "f5xc_namespace" "example" {
|
|
983
|
+
name = "my-namespace"
|
|
984
|
+
}`,
|
|
985
|
+
markdown: [
|
|
986
|
+
'# Complete Example: Namespace (Basic Pattern)',
|
|
987
|
+
'',
|
|
988
|
+
'Creates a namespace in F5 Distributed Cloud.',
|
|
989
|
+
'',
|
|
990
|
+
'```hcl',
|
|
991
|
+
'resource "f5xc_namespace" "example" {',
|
|
992
|
+
' name = "my-namespace"',
|
|
993
|
+
'}',
|
|
994
|
+
'```',
|
|
995
|
+
'',
|
|
996
|
+
'## Key Syntax Notes',
|
|
997
|
+
'',
|
|
998
|
+
'- Namespace is the simplest resource - just needs a name',
|
|
999
|
+
'- Most other resources require a namespace reference',
|
|
1000
|
+
].join('\n'),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
// App Firewall example
|
|
1004
|
+
if (resource === 'app_firewall') {
|
|
1005
|
+
return getAppFirewallExample(pattern);
|
|
1006
|
+
}
|
|
1007
|
+
// Generic fallback - generate from syntax guide
|
|
1008
|
+
const syntaxGuide = generateTerraformSyntaxGuide(resource);
|
|
1009
|
+
if (syntaxGuide) {
|
|
1010
|
+
const blocks = syntaxGuide.blocks.slice(0, 5);
|
|
1011
|
+
const attrs = syntaxGuide.attributes.slice(0, 5);
|
|
1012
|
+
const exampleLines = [
|
|
1013
|
+
`resource "f5xc_${resource}" "example" {`,
|
|
1014
|
+
' name = "example-' + resource.replace(/_/g, '-') + '"',
|
|
1015
|
+
' namespace = "default"',
|
|
1016
|
+
'',
|
|
1017
|
+
];
|
|
1018
|
+
// Add required OneOf selections
|
|
1019
|
+
for (const [groupName, fields] of Object.entries(syntaxGuide.oneOfGroups)) {
|
|
1020
|
+
if (fields.length > 0) {
|
|
1021
|
+
exampleLines.push(` # OneOf group: ${groupName} - choose one`);
|
|
1022
|
+
exampleLines.push(` ${fields[0]} {}`);
|
|
1023
|
+
exampleLines.push('');
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
exampleLines.push('}');
|
|
1027
|
+
return {
|
|
1028
|
+
description: `Auto-generated example for ${resource}`,
|
|
1029
|
+
terraform: exampleLines.join('\n'),
|
|
1030
|
+
markdown: [
|
|
1031
|
+
`# Complete Example: ${resource} (Auto-Generated)`,
|
|
1032
|
+
'',
|
|
1033
|
+
'⚠️ **Note**: This is an auto-generated example. Verify required fields in documentation.',
|
|
1034
|
+
'',
|
|
1035
|
+
'```hcl',
|
|
1036
|
+
exampleLines.join('\n'),
|
|
1037
|
+
'```',
|
|
1038
|
+
'',
|
|
1039
|
+
'## Block-Type Attributes (use empty block syntax)',
|
|
1040
|
+
'',
|
|
1041
|
+
blocks.map(b => `- \`${b} {}\``).join('\n'),
|
|
1042
|
+
'',
|
|
1043
|
+
'## Simple Attributes',
|
|
1044
|
+
'',
|
|
1045
|
+
attrs.map(a => `- \`${a} = <value>\``).join('\n'),
|
|
1046
|
+
].join('\n'),
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
function getHttpLoadbalancerExample(pattern) {
|
|
1052
|
+
const baseExample = `# 1. Create the origin pool
|
|
1053
|
+
resource "f5xc_origin_pool" "example" {
|
|
1054
|
+
name = "httpbin-pool"
|
|
1055
|
+
namespace = "default"
|
|
1056
|
+
|
|
1057
|
+
# TLS choice - use empty block syntax
|
|
1058
|
+
no_tls {}
|
|
1059
|
+
|
|
1060
|
+
# Port choice - use empty block syntax
|
|
1061
|
+
automatic_port {}
|
|
1062
|
+
|
|
1063
|
+
origin_servers {
|
|
1064
|
+
public_name {
|
|
1065
|
+
dns_name = "httpbin.org"
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
loadbalancer_algorithm = "ROUND_ROBIN"
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
# 2. Create the HTTP load balancer
|
|
1073
|
+
resource "f5xc_http_loadbalancer" "example" {
|
|
1074
|
+
name = "httpbin-lb"
|
|
1075
|
+
namespace = "default"
|
|
1076
|
+
domains = ["httpbin.example.com"]
|
|
1077
|
+
|
|
1078
|
+
# Load balancer type - use empty block syntax
|
|
1079
|
+
https_auto_cert {
|
|
1080
|
+
http_redirect = true
|
|
1081
|
+
add_hsts = false
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
# Advertising - use empty block syntax
|
|
1085
|
+
advertise_on_public_default_vip {}
|
|
1086
|
+
|
|
1087
|
+
# Origin pool reference
|
|
1088
|
+
default_route_pools {
|
|
1089
|
+
pool {
|
|
1090
|
+
name = f5xc_origin_pool.example.name
|
|
1091
|
+
namespace = "default"
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
# Security options - use empty block syntax
|
|
1096
|
+
disable_waf {}
|
|
1097
|
+
no_challenge {}
|
|
1098
|
+
disable_rate_limit {}
|
|
1099
|
+
no_service_policies {}
|
|
1100
|
+
disable_bot_defense {}
|
|
1101
|
+
disable_api_definition {}
|
|
1102
|
+
disable_api_discovery {}
|
|
1103
|
+
|
|
1104
|
+
# Load balancing algorithm - use empty block syntax
|
|
1105
|
+
round_robin {}
|
|
1106
|
+
}`;
|
|
1107
|
+
if (pattern === 'basic') {
|
|
1108
|
+
return {
|
|
1109
|
+
description: 'Basic HTTP Load Balancer with origin pool pointing to httpbin.org',
|
|
1110
|
+
terraform: baseExample,
|
|
1111
|
+
markdown: [
|
|
1112
|
+
'# Complete Example: HTTP Load Balancer (Basic Pattern)',
|
|
1113
|
+
'',
|
|
1114
|
+
'Creates an HTTP load balancer with an origin pool pointing to httpbin.org.',
|
|
1115
|
+
'',
|
|
1116
|
+
'## Required Resources',
|
|
1117
|
+
'',
|
|
1118
|
+
'```hcl',
|
|
1119
|
+
baseExample,
|
|
1120
|
+
'```',
|
|
1121
|
+
'',
|
|
1122
|
+
'## Key Syntax Notes',
|
|
1123
|
+
'',
|
|
1124
|
+
'1. **Empty blocks for OneOf choices**: `no_tls {}`, `advertise_on_public_default_vip {}`',
|
|
1125
|
+
'2. **Nested blocks with values**: `https_auto_cert { http_redirect = true }`',
|
|
1126
|
+
'3. **Reference syntax**: `f5xc_origin_pool.example.name`',
|
|
1127
|
+
'',
|
|
1128
|
+
'## Common Mistakes to Avoid',
|
|
1129
|
+
'',
|
|
1130
|
+
'```hcl',
|
|
1131
|
+
'# WRONG - Do not use boolean assignment for these fields!',
|
|
1132
|
+
'no_tls = true # ERROR: use no_tls {}',
|
|
1133
|
+
'advertise_on_public_default_vip = true # ERROR: use advertise_on_public_default_vip {}',
|
|
1134
|
+
'round_robin = true # ERROR: use round_robin {}',
|
|
1135
|
+
'```',
|
|
1136
|
+
].join('\n'),
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
if (pattern === 'with_waf') {
|
|
1140
|
+
const wafExample = baseExample.replace(' disable_waf {}', ` app_firewall {
|
|
1141
|
+
name = f5xc_app_firewall.example.name
|
|
1142
|
+
namespace = "default"
|
|
1143
|
+
}`);
|
|
1144
|
+
const fullExample = `# 0. Create App Firewall first
|
|
1145
|
+
resource "f5xc_app_firewall" "example" {
|
|
1146
|
+
name = "my-waf"
|
|
1147
|
+
namespace = "default"
|
|
1148
|
+
|
|
1149
|
+
# Use blocking mode (not monitoring)
|
|
1150
|
+
blocking {}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
${wafExample}`;
|
|
1154
|
+
return {
|
|
1155
|
+
description: 'HTTP Load Balancer with WAF/App Firewall protection',
|
|
1156
|
+
terraform: fullExample,
|
|
1157
|
+
markdown: [
|
|
1158
|
+
'# Complete Example: HTTP Load Balancer with WAF',
|
|
1159
|
+
'',
|
|
1160
|
+
'Creates an HTTP load balancer with Web Application Firewall protection.',
|
|
1161
|
+
'',
|
|
1162
|
+
'```hcl',
|
|
1163
|
+
fullExample,
|
|
1164
|
+
'```',
|
|
1165
|
+
'',
|
|
1166
|
+
'## WAF Configuration Notes',
|
|
1167
|
+
'',
|
|
1168
|
+
'- Create `f5xc_app_firewall` resource first',
|
|
1169
|
+
'- Reference it in `http_loadbalancer.app_firewall` block',
|
|
1170
|
+
'- Use `blocking {}` for enforcement, `monitoring {}` for detection-only',
|
|
1171
|
+
].join('\n'),
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
// Default to basic
|
|
1175
|
+
return getHttpLoadbalancerExample('basic');
|
|
1176
|
+
}
|
|
1177
|
+
function getOriginPoolExample(pattern) {
|
|
1178
|
+
const example = `# Optional: Create a healthcheck resource first
|
|
1179
|
+
# (healthcheck is a REFERENCE BLOCK - only name/namespace/tenant are valid)
|
|
1180
|
+
resource "f5xc_healthcheck" "example" {
|
|
1181
|
+
name = "my-healthcheck"
|
|
1182
|
+
namespace = "default"
|
|
1183
|
+
|
|
1184
|
+
# Health check type - select ONE
|
|
1185
|
+
http_health_check {
|
|
1186
|
+
use_origin_server_name {}
|
|
1187
|
+
path = "/health"
|
|
1188
|
+
}
|
|
1189
|
+
# OR: tcp_health_check {}
|
|
1190
|
+
# OR: grpc_health_check { ... }
|
|
1191
|
+
|
|
1192
|
+
healthy_threshold = 2
|
|
1193
|
+
unhealthy_threshold = 3
|
|
1194
|
+
interval = 15
|
|
1195
|
+
timeout = 5
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
resource "f5xc_origin_pool" "example" {
|
|
1199
|
+
name = "my-origin-pool"
|
|
1200
|
+
namespace = "default"
|
|
1201
|
+
|
|
1202
|
+
# TLS Choice - select ONE (use empty block syntax)
|
|
1203
|
+
# Option 1: No TLS (plain HTTP to origin)
|
|
1204
|
+
no_tls {}
|
|
1205
|
+
|
|
1206
|
+
# Option 2: Use TLS (uncomment and configure)
|
|
1207
|
+
# use_tls {
|
|
1208
|
+
# # Nested TLS configuration
|
|
1209
|
+
# skip_server_verification {}
|
|
1210
|
+
# # OR
|
|
1211
|
+
# # use_server_verification {}
|
|
1212
|
+
# }
|
|
1213
|
+
|
|
1214
|
+
# Port Choice - select ONE
|
|
1215
|
+
# Option 1: Automatic port (80 for HTTP, 443 for HTTPS)
|
|
1216
|
+
automatic_port {}
|
|
1217
|
+
|
|
1218
|
+
# Option 2: Same as endpoint port
|
|
1219
|
+
# same_as_endpoint_port {}
|
|
1220
|
+
|
|
1221
|
+
# Option 3: Explicit port number (use attribute, not block)
|
|
1222
|
+
# port = 8080
|
|
1223
|
+
|
|
1224
|
+
# Origin servers (at least one required)
|
|
1225
|
+
origin_servers {
|
|
1226
|
+
# Public DNS name
|
|
1227
|
+
public_name {
|
|
1228
|
+
dns_name = "api.example.com"
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
# Healthcheck - REFERENCE BLOCK (only name/namespace/tenant!)
|
|
1233
|
+
# This references the separate f5xc_healthcheck resource above
|
|
1234
|
+
# DO NOT put configuration parameters here (interval, timeout, etc.)
|
|
1235
|
+
healthcheck {
|
|
1236
|
+
name = f5xc_healthcheck.example.name
|
|
1237
|
+
namespace = "default"
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
# Load balancing algorithm (attribute, not block)
|
|
1241
|
+
loadbalancer_algorithm = "ROUND_ROBIN"
|
|
1242
|
+
}`;
|
|
1243
|
+
return {
|
|
1244
|
+
description: 'Origin Pool with healthcheck reference and common configuration patterns',
|
|
1245
|
+
terraform: example,
|
|
1246
|
+
markdown: [
|
|
1247
|
+
'# Complete Example: Origin Pool with Healthcheck',
|
|
1248
|
+
'',
|
|
1249
|
+
'Creates an origin pool with healthcheck reference and multiple configuration options.',
|
|
1250
|
+
'',
|
|
1251
|
+
'```hcl',
|
|
1252
|
+
example,
|
|
1253
|
+
'```',
|
|
1254
|
+
'',
|
|
1255
|
+
'## Reference Blocks vs Inline Blocks',
|
|
1256
|
+
'',
|
|
1257
|
+
'**IMPORTANT**: The `healthcheck` field is a **reference block**, NOT an inline configuration block.',
|
|
1258
|
+
'',
|
|
1259
|
+
'### Reference Blocks (only accept name/namespace/tenant)',
|
|
1260
|
+
'- `healthcheck {}` - References a separate `f5xc_healthcheck` resource',
|
|
1261
|
+
'- Configuration parameters (interval, timeout, path, etc.) go in the separate resource',
|
|
1262
|
+
'',
|
|
1263
|
+
'### Common Error (WRONG):',
|
|
1264
|
+
'```hcl',
|
|
1265
|
+
'# DO NOT DO THIS - healthcheck is a reference, not inline config',
|
|
1266
|
+
'healthcheck {',
|
|
1267
|
+
' interval_seconds = 30 # ERROR!',
|
|
1268
|
+
' http_request {} # ERROR!',
|
|
1269
|
+
'}',
|
|
1270
|
+
'```',
|
|
1271
|
+
'',
|
|
1272
|
+
'## OneOf Groups Explained',
|
|
1273
|
+
'',
|
|
1274
|
+
'### tls_choice',
|
|
1275
|
+
'- `no_tls {}` - Plain HTTP to origin (use empty block)',
|
|
1276
|
+
'- `use_tls { ... }` - TLS to origin (use block with nested config)',
|
|
1277
|
+
'',
|
|
1278
|
+
'### port_choice',
|
|
1279
|
+
'- `automatic_port {}` - Auto-select based on TLS (use empty block)',
|
|
1280
|
+
'- `same_as_endpoint_port {}` - Use endpoint\'s port (use empty block)',
|
|
1281
|
+
'- `port = 8080` - Explicit port (use attribute assignment)',
|
|
1282
|
+
'',
|
|
1283
|
+
'## Key Syntax Pattern',
|
|
1284
|
+
'',
|
|
1285
|
+
'Block-type fields like `no_tls`, `automatic_port` use `{}` syntax.',
|
|
1286
|
+
'Attribute fields like `port` use `= value` syntax.',
|
|
1287
|
+
'Reference fields like `healthcheck` only accept `name`/`namespace`/`tenant`.',
|
|
1288
|
+
].join('\n'),
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function getAppFirewallExample(pattern) {
|
|
1292
|
+
const example = `resource "f5xc_app_firewall" "example" {
|
|
1293
|
+
name = "my-waf-policy"
|
|
1294
|
+
namespace = "default"
|
|
1295
|
+
|
|
1296
|
+
# Enforcement mode - select ONE (use empty block syntax)
|
|
1297
|
+
# Option 1: Blocking mode (actively blocks attacks)
|
|
1298
|
+
blocking {}
|
|
1299
|
+
|
|
1300
|
+
# Option 2: Monitoring mode (logs only, no blocking)
|
|
1301
|
+
# monitoring {}
|
|
1302
|
+
|
|
1303
|
+
# Anonymization setting - select ONE
|
|
1304
|
+
default_anonymization {}
|
|
1305
|
+
# OR: custom_anonymization { ... }
|
|
1306
|
+
# OR: disable_anonymization {}
|
|
1307
|
+
|
|
1308
|
+
# Bot protection settings - select ONE
|
|
1309
|
+
default_bot_setting {}
|
|
1310
|
+
# OR: custom_bot_protection_setting { ... }
|
|
1311
|
+
|
|
1312
|
+
# Detection settings - select ONE
|
|
1313
|
+
default_detection_settings {}
|
|
1314
|
+
# OR: detection_settings { ... }
|
|
1315
|
+
}`;
|
|
1316
|
+
return {
|
|
1317
|
+
description: 'App Firewall (WAF) with common configuration patterns',
|
|
1318
|
+
terraform: example,
|
|
1319
|
+
markdown: [
|
|
1320
|
+
'# Complete Example: App Firewall (WAF)',
|
|
1321
|
+
'',
|
|
1322
|
+
'Creates a Web Application Firewall policy.',
|
|
1323
|
+
'',
|
|
1324
|
+
'```hcl',
|
|
1325
|
+
example,
|
|
1326
|
+
'```',
|
|
1327
|
+
'',
|
|
1328
|
+
'## Key OneOf Groups',
|
|
1329
|
+
'',
|
|
1330
|
+
'### enforcement_mode_choice',
|
|
1331
|
+
'- `blocking {}` - Actively block attacks',
|
|
1332
|
+
'- `monitoring {}` - Detection only, no blocking',
|
|
1333
|
+
'',
|
|
1334
|
+
'### anonymization_setting',
|
|
1335
|
+
'- `default_anonymization {}` - Use default settings',
|
|
1336
|
+
'- `custom_anonymization { ... }` - Custom configuration',
|
|
1337
|
+
'- `disable_anonymization {}` - No anonymization',
|
|
1338
|
+
'',
|
|
1339
|
+
'## Important',
|
|
1340
|
+
'',
|
|
1341
|
+
'All OneOf options use **empty block syntax** `{}`, not boolean assignment.',
|
|
1342
|
+
].join('\n'),
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
// =============================================================================
|
|
1346
|
+
// MISTAKES OPERATION HANDLER
|
|
1347
|
+
// =============================================================================
|
|
1348
|
+
/**
|
|
1349
|
+
* Returns common syntax mistakes and how to fix them
|
|
1350
|
+
*/
|
|
1351
|
+
function handleMistakes(input, format) {
|
|
1352
|
+
const { resource } = input;
|
|
1353
|
+
const mistakes = getCommonMistakes(resource);
|
|
1354
|
+
if (format === ResponseFormat.JSON) {
|
|
1355
|
+
return JSON.stringify({
|
|
1356
|
+
operation: 'mistakes',
|
|
1357
|
+
resource: resource || 'all',
|
|
1358
|
+
mistakes,
|
|
1359
|
+
total: mistakes.length,
|
|
1360
|
+
}, null, 2);
|
|
1361
|
+
}
|
|
1362
|
+
const lines = [
|
|
1363
|
+
'# Common Terraform Syntax Mistakes for F5XC Provider',
|
|
1364
|
+
'',
|
|
1365
|
+
];
|
|
1366
|
+
if (resource) {
|
|
1367
|
+
lines.push(`**Resource**: ${resource}`);
|
|
1368
|
+
lines.push('');
|
|
1369
|
+
}
|
|
1370
|
+
for (let i = 0; i < mistakes.length; i++) {
|
|
1371
|
+
const m = mistakes[i];
|
|
1372
|
+
lines.push(`## Mistake #${i + 1}: ${m.title}`);
|
|
1373
|
+
lines.push('');
|
|
1374
|
+
lines.push(`**Severity**: ${m.severity}`);
|
|
1375
|
+
lines.push('');
|
|
1376
|
+
lines.push('**Wrong**:');
|
|
1377
|
+
lines.push('```hcl');
|
|
1378
|
+
lines.push(m.wrong);
|
|
1379
|
+
lines.push('```');
|
|
1380
|
+
lines.push('');
|
|
1381
|
+
lines.push('**Correct**:');
|
|
1382
|
+
lines.push('```hcl');
|
|
1383
|
+
lines.push(m.correct);
|
|
1384
|
+
lines.push('```');
|
|
1385
|
+
lines.push('');
|
|
1386
|
+
lines.push(`**Explanation**: ${m.explanation}`);
|
|
1387
|
+
lines.push('');
|
|
1388
|
+
if (m.detection) {
|
|
1389
|
+
lines.push(`**How to detect**: ${m.detection}`);
|
|
1390
|
+
lines.push('');
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return lines.join('\n');
|
|
1394
|
+
}
|
|
1395
|
+
function getCommonMistakes(resource) {
|
|
1396
|
+
const allMistakes = [
|
|
1397
|
+
{
|
|
1398
|
+
title: 'Boolean Assignment Instead of Empty Block',
|
|
1399
|
+
severity: 'Critical',
|
|
1400
|
+
wrong: `no_tls = true
|
|
1401
|
+
advertise_on_public_default_vip = true
|
|
1402
|
+
round_robin = true
|
|
1403
|
+
disable_api_definition = true
|
|
1404
|
+
no_challenge = true`,
|
|
1405
|
+
correct: `no_tls {}
|
|
1406
|
+
advertise_on_public_default_vip {}
|
|
1407
|
+
round_robin {}
|
|
1408
|
+
disable_api_definition {}
|
|
1409
|
+
no_challenge {}`,
|
|
1410
|
+
explanation: 'OneOf choice fields in F5XC provider are modeled as blocks, not booleans. Use empty block syntax {} to select an option.',
|
|
1411
|
+
detection: 'Query f5xc_terraform_metadata(operation="syntax", resource="...") and check "Block-Type Attributes" section.',
|
|
1412
|
+
resources: ['http_loadbalancer', 'origin_pool', 'tcp_loadbalancer', 'app_firewall'],
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
title: 'Missing Required OneOf Selection',
|
|
1416
|
+
severity: 'Critical',
|
|
1417
|
+
wrong: `resource "f5xc_http_loadbalancer" "example" {
|
|
1418
|
+
name = "test"
|
|
1419
|
+
namespace = "default"
|
|
1420
|
+
domains = ["example.com"]
|
|
1421
|
+
# Missing loadbalancer_type selection!
|
|
1422
|
+
}`,
|
|
1423
|
+
correct: `resource "f5xc_http_loadbalancer" "example" {
|
|
1424
|
+
name = "test"
|
|
1425
|
+
namespace = "default"
|
|
1426
|
+
domains = ["example.com"]
|
|
1427
|
+
|
|
1428
|
+
# Required: one of http/https/https_auto_cert
|
|
1429
|
+
https_auto_cert {
|
|
1430
|
+
http_redirect = true
|
|
1431
|
+
}
|
|
1432
|
+
}`,
|
|
1433
|
+
explanation: 'Some OneOf groups have a required selection. For http_loadbalancer, you must specify http {}, https {}, or https_auto_cert {}.',
|
|
1434
|
+
detection: 'Query f5xc_terraform_metadata(operation="oneof", resource="...") to see required OneOf groups.',
|
|
1435
|
+
resources: ['http_loadbalancer'],
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
title: 'Confusing Block Attribute with Simple Attribute',
|
|
1439
|
+
severity: 'High',
|
|
1440
|
+
wrong: `# Using 'port' thinking it's part of port_choice OneOf
|
|
1441
|
+
port {} # ERROR: port is a simple attribute
|
|
1442
|
+
|
|
1443
|
+
# Using loadbalancer_algorithm as a block
|
|
1444
|
+
round_robin {} # This is for hash_policy_choice, not algorithm`,
|
|
1445
|
+
correct: `# port is a simple integer attribute
|
|
1446
|
+
port = 8080
|
|
1447
|
+
|
|
1448
|
+
# loadbalancer_algorithm is a string attribute
|
|
1449
|
+
loadbalancer_algorithm = "ROUND_ROBIN"
|
|
1450
|
+
|
|
1451
|
+
# hash_policy_choice uses blocks for stickiness
|
|
1452
|
+
round_robin {} # OR source_ip_stickiness {} etc.`,
|
|
1453
|
+
explanation: 'Not all similarly-named fields work the same way. Some are simple attributes (use =), others are blocks (use {}).',
|
|
1454
|
+
detection: 'Query f5xc_terraform_metadata(operation="attribute", resource="...", attribute="...") to check the type.',
|
|
1455
|
+
resources: ['origin_pool', 'http_loadbalancer'],
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
title: 'Nested Block Syntax Errors',
|
|
1459
|
+
severity: 'Medium',
|
|
1460
|
+
wrong: `use_tls {
|
|
1461
|
+
no_mtls = true # ERROR
|
|
1462
|
+
volterra_trusted_ca = true # ERROR
|
|
1463
|
+
}`,
|
|
1464
|
+
correct: `use_tls {
|
|
1465
|
+
no_mtls {} # Empty block syntax
|
|
1466
|
+
volterra_trusted_ca {} # Empty block syntax
|
|
1467
|
+
|
|
1468
|
+
tls_config {
|
|
1469
|
+
default_security {} # Also empty block
|
|
1470
|
+
}
|
|
1471
|
+
}`,
|
|
1472
|
+
explanation: 'Empty block syntax applies to nested fields too, not just top-level attributes.',
|
|
1473
|
+
detection: 'Recursively check nested fields using the syntax operation.',
|
|
1474
|
+
resources: ['origin_pool', 'http_loadbalancer'],
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
title: 'Inline Configuration in Reference Block',
|
|
1478
|
+
severity: 'Critical',
|
|
1479
|
+
wrong: `# WRONG - trying to configure healthcheck inline
|
|
1480
|
+
resource "f5xc_origin_pool" "example" {
|
|
1481
|
+
name = "my-pool"
|
|
1482
|
+
namespace = "default"
|
|
1483
|
+
|
|
1484
|
+
healthcheck {
|
|
1485
|
+
interval_seconds = 30 # ERROR: not a valid attribute
|
|
1486
|
+
timeout_seconds = 5 # ERROR: not a valid attribute
|
|
1487
|
+
healthy_threshold = 2 # ERROR: not a valid attribute
|
|
1488
|
+
unhealthy_threshold = 3 # ERROR: not a valid attribute
|
|
1489
|
+
|
|
1490
|
+
http_request { # ERROR: not a valid block
|
|
1491
|
+
path = "/health"
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}`,
|
|
1495
|
+
correct: `# CORRECT - create healthcheck as separate resource, then reference it
|
|
1496
|
+
# Step 1: Create the healthcheck resource
|
|
1497
|
+
resource "f5xc_healthcheck" "example" {
|
|
1498
|
+
name = "my-healthcheck"
|
|
1499
|
+
namespace = "default"
|
|
1500
|
+
|
|
1501
|
+
http_health_check {
|
|
1502
|
+
use_origin_server_name {}
|
|
1503
|
+
path = "/health"
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
healthy_threshold = 2
|
|
1507
|
+
unhealthy_threshold = 3
|
|
1508
|
+
interval = 30
|
|
1509
|
+
timeout = 5
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
# Step 2: Reference it by name in origin_pool
|
|
1513
|
+
resource "f5xc_origin_pool" "example" {
|
|
1514
|
+
name = "my-pool"
|
|
1515
|
+
namespace = "default"
|
|
1516
|
+
|
|
1517
|
+
healthcheck {
|
|
1518
|
+
name = f5xc_healthcheck.example.name
|
|
1519
|
+
namespace = "default"
|
|
1520
|
+
}
|
|
1521
|
+
}`,
|
|
1522
|
+
explanation: `Some fields like "healthcheck" are REFERENCE BLOCKS - they reference separate resources, not inline configuration.
|
|
1523
|
+
Reference blocks ONLY accept "name", "namespace", and optionally "tenant" - NOT configuration parameters.
|
|
1524
|
+
The actual configuration must be done in a separate resource (e.g., f5xc_healthcheck).`,
|
|
1525
|
+
detection: `Query f5xc_terraform_metadata(operation="attribute", resource="origin_pool", attribute="healthcheck")
|
|
1526
|
+
and check if it's a reference block (only has name/namespace/tenant nested attributes).
|
|
1527
|
+
Or use f5xc_terraform_metadata(operation="validate", resource="origin_pool", config="...") to detect this error.`,
|
|
1528
|
+
resources: ['origin_pool', 'http_loadbalancer', 'tcp_loadbalancer'],
|
|
1529
|
+
},
|
|
1530
|
+
];
|
|
1531
|
+
// Filter by resource if specified
|
|
1532
|
+
if (resource) {
|
|
1533
|
+
return allMistakes.filter(m => !m.resources || m.resources.includes(resource));
|
|
1534
|
+
}
|
|
1535
|
+
return allMistakes;
|
|
1536
|
+
}
|
|
701
1537
|
// =============================================================================
|
|
702
1538
|
// HELPERS
|
|
703
1539
|
// =============================================================================
|
|
@@ -713,11 +1549,336 @@ function formatNoData(title, message, format) {
|
|
|
713
1549
|
}
|
|
714
1550
|
return `# ${title}\n\n${message}`;
|
|
715
1551
|
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Parse HCL config to extract all field/block names for validation
|
|
1554
|
+
*/
|
|
1555
|
+
function parseHCLForFields(hcl) {
|
|
1556
|
+
const fields = [];
|
|
1557
|
+
const blocks = [];
|
|
1558
|
+
const lines = hcl.split('\n');
|
|
1559
|
+
// Track block nesting
|
|
1560
|
+
const blockStack = [];
|
|
1561
|
+
let braceCount = 0;
|
|
1562
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
1563
|
+
const line = lines[lineNum];
|
|
1564
|
+
const trimmed = line.trim();
|
|
1565
|
+
// Skip comments and empty lines
|
|
1566
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('//') || trimmed === '') {
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
// Count braces to track nesting
|
|
1570
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
1571
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
1572
|
+
// Check for block definition: name { or name "label" {
|
|
1573
|
+
const blockMatch = trimmed.match(/^(\w+)\s*(?:"[^"]*"\s*)?\{(.*)$/);
|
|
1574
|
+
if (blockMatch) {
|
|
1575
|
+
const blockName = blockMatch[1];
|
|
1576
|
+
const inlineContent = blockMatch[2] || '';
|
|
1577
|
+
// Skip resource/data/provider/variable declarations
|
|
1578
|
+
if (!['resource', 'data', 'provider', 'variable', 'output', 'locals', 'terraform', 'module'].includes(blockName)) {
|
|
1579
|
+
const parent = blockStack.length > 0 ? blockStack[blockStack.length - 1].name : undefined;
|
|
1580
|
+
blocks.push({
|
|
1581
|
+
name: blockName,
|
|
1582
|
+
line: lineNum + 1,
|
|
1583
|
+
parent,
|
|
1584
|
+
children: [],
|
|
1585
|
+
});
|
|
1586
|
+
fields.push({
|
|
1587
|
+
name: blockName,
|
|
1588
|
+
type: 'block',
|
|
1589
|
+
line: lineNum + 1,
|
|
1590
|
+
parent,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
blockStack.push({ name: blockMatch[1], line: lineNum + 1 });
|
|
1594
|
+
// Handle inline content after opening brace (e.g., "block { attr = value }")
|
|
1595
|
+
if (inlineContent.trim() && !inlineContent.trim().startsWith('}')) {
|
|
1596
|
+
const currentBlockName = blockStack[blockStack.length - 1].name;
|
|
1597
|
+
// Extract attributes from inline content
|
|
1598
|
+
const inlineAttrMatch = inlineContent.match(/(\w+)\s*=\s*([^}]+)/);
|
|
1599
|
+
if (inlineAttrMatch) {
|
|
1600
|
+
const inlineAttrName = inlineAttrMatch[1];
|
|
1601
|
+
const inlineAttrValue = inlineAttrMatch[2].trim();
|
|
1602
|
+
fields.push({
|
|
1603
|
+
name: inlineAttrName,
|
|
1604
|
+
type: 'attribute',
|
|
1605
|
+
line: lineNum + 1,
|
|
1606
|
+
parent: currentBlockName,
|
|
1607
|
+
value: inlineAttrValue,
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
// Check for nested blocks in inline content
|
|
1611
|
+
const inlineBlockMatch = inlineContent.match(/(\w+)\s*\{/);
|
|
1612
|
+
if (inlineBlockMatch && !inlineAttrMatch) {
|
|
1613
|
+
fields.push({
|
|
1614
|
+
name: inlineBlockMatch[1],
|
|
1615
|
+
type: 'block',
|
|
1616
|
+
line: lineNum + 1,
|
|
1617
|
+
parent: currentBlockName,
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
// Check for attribute assignment: name = value (only if not part of a block definition)
|
|
1623
|
+
const attrMatch = trimmed.match(/^(\w+)\s*=\s*(.+?)$/);
|
|
1624
|
+
if (attrMatch && !blockMatch) {
|
|
1625
|
+
const attrName = attrMatch[1];
|
|
1626
|
+
const attrValue = attrMatch[2];
|
|
1627
|
+
const parent = blockStack.length > 0 ? blockStack[blockStack.length - 1].name : undefined;
|
|
1628
|
+
// Skip top-level resource attributes like 'name', 'namespace' at resource level
|
|
1629
|
+
if (blockStack.length > 0 || !['resource', 'data'].includes(blockStack[0]?.name || '')) {
|
|
1630
|
+
fields.push({
|
|
1631
|
+
name: attrName,
|
|
1632
|
+
type: 'attribute',
|
|
1633
|
+
line: lineNum + 1,
|
|
1634
|
+
parent: parent !== 'resource' ? parent : undefined,
|
|
1635
|
+
value: attrValue,
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
// Track brace closing
|
|
1640
|
+
braceCount += openBraces - closeBraces;
|
|
1641
|
+
if (closeBraces > 0 && blockStack.length > 0) {
|
|
1642
|
+
for (let i = 0; i < closeBraces; i++) {
|
|
1643
|
+
if (blockStack.length > 0) {
|
|
1644
|
+
blockStack.pop();
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return { fields, blocks };
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Known reference block field names in F5XC provider.
|
|
1653
|
+
* These blocks only accept name/namespace/tenant, not inline configuration.
|
|
1654
|
+
*
|
|
1655
|
+
* COMPREHENSIVE LIST - derived from analysis of all 98 resources in the provider.
|
|
1656
|
+
* This list is used for deterministic detection when description patterns are unclear.
|
|
1657
|
+
*/
|
|
1658
|
+
const KNOWN_REFERENCE_BLOCKS = new Set([
|
|
1659
|
+
// Security references
|
|
1660
|
+
'app_firewall',
|
|
1661
|
+
'bot_defense_policy',
|
|
1662
|
+
'csrf_policy',
|
|
1663
|
+
'data_guard_rules',
|
|
1664
|
+
'dos_protection',
|
|
1665
|
+
'graphql_rules',
|
|
1666
|
+
'ip_reputation',
|
|
1667
|
+
'jwt_validation',
|
|
1668
|
+
'malicious_user_mitigation',
|
|
1669
|
+
'protected_cookies',
|
|
1670
|
+
'slow_ddos_mitigation',
|
|
1671
|
+
'trusted_clients',
|
|
1672
|
+
'usb_policy',
|
|
1673
|
+
'waf_exclusion_rules',
|
|
1674
|
+
// Load balancer and pool references
|
|
1675
|
+
'healthcheck',
|
|
1676
|
+
'origin_pool',
|
|
1677
|
+
'pool',
|
|
1678
|
+
'cluster',
|
|
1679
|
+
// Policy references
|
|
1680
|
+
'rate_limiter',
|
|
1681
|
+
'rate_limiter_allowed_prefixes',
|
|
1682
|
+
'service_policy',
|
|
1683
|
+
'policer',
|
|
1684
|
+
// Identity references
|
|
1685
|
+
'user_identification',
|
|
1686
|
+
'authentication',
|
|
1687
|
+
'ad_client',
|
|
1688
|
+
// API references
|
|
1689
|
+
'api_definition',
|
|
1690
|
+
'api_definitions',
|
|
1691
|
+
// Certificate references
|
|
1692
|
+
'certificate',
|
|
1693
|
+
'certificates',
|
|
1694
|
+
'crl',
|
|
1695
|
+
'trusted_ca',
|
|
1696
|
+
'trusted_ca_list',
|
|
1697
|
+
// Infrastructure references
|
|
1698
|
+
'site',
|
|
1699
|
+
'virtual_site',
|
|
1700
|
+
'virtual_network',
|
|
1701
|
+
'segment',
|
|
1702
|
+
'network_connector',
|
|
1703
|
+
'cloud_credentials',
|
|
1704
|
+
'aws_cred',
|
|
1705
|
+
'azure_cred',
|
|
1706
|
+
'gcp_cred',
|
|
1707
|
+
'k8s_cluster',
|
|
1708
|
+
'log_receiver',
|
|
1709
|
+
// Site group references
|
|
1710
|
+
'dc_cluster_group',
|
|
1711
|
+
'dc_cluster_group_inside',
|
|
1712
|
+
'dc_cluster_group_sli',
|
|
1713
|
+
'dc_cluster_group_slo',
|
|
1714
|
+
'ce_site_reference',
|
|
1715
|
+
// Selector references (name/namespace pattern)
|
|
1716
|
+
'client_selector',
|
|
1717
|
+
'server_selector',
|
|
1718
|
+
'site_selector',
|
|
1719
|
+
'prefix_selector',
|
|
1720
|
+
'proxy_label_selector',
|
|
1721
|
+
'ip_prefix_set',
|
|
1722
|
+
// Advertise location references
|
|
1723
|
+
'public_ip',
|
|
1724
|
+
'where',
|
|
1725
|
+
// DR references
|
|
1726
|
+
'sli_to_global_dr',
|
|
1727
|
+
'slo_to_global_dr',
|
|
1728
|
+
// Nested reference blocks (commonly found inside other blocks)
|
|
1729
|
+
'policies', // inside active_service_policies
|
|
1730
|
+
]);
|
|
1731
|
+
/**
|
|
1732
|
+
* Description patterns that indicate a reference block.
|
|
1733
|
+
* These are exact patterns from F5XC API documentation.
|
|
1734
|
+
*/
|
|
1735
|
+
const REFERENCE_DESCRIPTION_PATTERNS = [
|
|
1736
|
+
// Exact F5XC pattern for object references
|
|
1737
|
+
'type establishes a direct reference from one object',
|
|
1738
|
+
'establishes a direct reference',
|
|
1739
|
+
'such a reference is in form of tenant/namespace/name',
|
|
1740
|
+
'reference is in form of tenant/namespace/name',
|
|
1741
|
+
// Simpler reference patterns
|
|
1742
|
+
'reference to',
|
|
1743
|
+
'references to',
|
|
1744
|
+
'reference object',
|
|
1745
|
+
'refers to another',
|
|
1746
|
+
// Configuration object reference patterns
|
|
1747
|
+
'configuration object(e.g. virtual_host) refers to another',
|
|
1748
|
+
'when a configuration object',
|
|
1749
|
+
];
|
|
1750
|
+
/**
|
|
1751
|
+
* Check if an attribute is a reference block (only accepts name/namespace/tenant)
|
|
1752
|
+
*
|
|
1753
|
+
* Detection strategy (in order of precedence):
|
|
1754
|
+
* 1. Known reference block names (deterministic, hardcoded list)
|
|
1755
|
+
* 2. Description pattern matching (F5XC documentation patterns)
|
|
1756
|
+
* 3. Type + block flag analysis
|
|
1757
|
+
*
|
|
1758
|
+
* This function is critical for preventing AI trial-and-error by correctly
|
|
1759
|
+
* identifying blocks that ONLY accept name/namespace/tenant.
|
|
1760
|
+
*/
|
|
1761
|
+
function isReferenceBlock(attrMeta, fieldName) {
|
|
1762
|
+
// Strategy 1: Check if it's a known reference block (most reliable)
|
|
1763
|
+
if (fieldName && KNOWN_REFERENCE_BLOCKS.has(fieldName)) {
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
// Strategy 2: Must be a block type for description-based detection
|
|
1767
|
+
if (!attrMeta.is_block && attrMeta.type !== 'object' && attrMeta.type !== 'list') {
|
|
1768
|
+
return false;
|
|
1769
|
+
}
|
|
1770
|
+
// Strategy 3: Check description for F5XC reference patterns
|
|
1771
|
+
const desc = (attrMeta.description || '').toLowerCase();
|
|
1772
|
+
// Check against all known reference patterns
|
|
1773
|
+
for (const pattern of REFERENCE_DESCRIPTION_PATTERNS) {
|
|
1774
|
+
if (desc.includes(pattern.toLowerCase())) {
|
|
1775
|
+
return true;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Calculate Levenshtein distance between two strings
|
|
1782
|
+
*/
|
|
1783
|
+
function levenshteinDistance(str1, str2) {
|
|
1784
|
+
const m = str1.length;
|
|
1785
|
+
const n = str2.length;
|
|
1786
|
+
// Create distance matrix
|
|
1787
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
1788
|
+
// Initialize base cases
|
|
1789
|
+
for (let i = 0; i <= m; i++)
|
|
1790
|
+
dp[i][0] = i;
|
|
1791
|
+
for (let j = 0; j <= n; j++)
|
|
1792
|
+
dp[0][j] = j;
|
|
1793
|
+
// Fill in the rest of the matrix
|
|
1794
|
+
for (let i = 1; i <= m; i++) {
|
|
1795
|
+
for (let j = 1; j <= n; j++) {
|
|
1796
|
+
if (str1[i - 1] === str2[j - 1]) {
|
|
1797
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
1798
|
+
}
|
|
1799
|
+
else {
|
|
1800
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], // deletion
|
|
1801
|
+
dp[i][j - 1], // insertion
|
|
1802
|
+
dp[i - 1][j - 1]);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return dp[m][n];
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Find similar field names for "did you mean?" suggestions
|
|
1810
|
+
*/
|
|
1811
|
+
function findSimilarFieldNames(typo, validFields, maxDistance = 3) {
|
|
1812
|
+
return validFields
|
|
1813
|
+
.map(f => ({ field: f, distance: levenshteinDistance(typo.toLowerCase(), f.toLowerCase()) }))
|
|
1814
|
+
.filter(x => x.distance <= maxDistance && x.distance > 0)
|
|
1815
|
+
.sort((a, b) => a.distance - b.distance)
|
|
1816
|
+
.slice(0, 3)
|
|
1817
|
+
.map(x => x.field);
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Get the resource that a reference block points to.
|
|
1821
|
+
* Comprehensive mapping for all known reference blocks.
|
|
1822
|
+
*/
|
|
1823
|
+
function getReferenceResourceName(fieldName) {
|
|
1824
|
+
// Comprehensive reference field mappings
|
|
1825
|
+
const mappings = {
|
|
1826
|
+
// Security resources
|
|
1827
|
+
'app_firewall': 'f5xc_app_firewall',
|
|
1828
|
+
'waf': 'f5xc_app_firewall',
|
|
1829
|
+
'bot_defense_policy': 'f5xc_bot_defense_policy',
|
|
1830
|
+
'malicious_user_mitigation': 'f5xc_malicious_user_mitigation',
|
|
1831
|
+
'usb_policy': 'f5xc_usb_policy',
|
|
1832
|
+
// Load balancer and pool resources
|
|
1833
|
+
'healthcheck': 'f5xc_healthcheck',
|
|
1834
|
+
'origin_pool': 'f5xc_origin_pool',
|
|
1835
|
+
'pool': 'f5xc_origin_pool',
|
|
1836
|
+
'cluster': 'f5xc_cluster',
|
|
1837
|
+
// Policy resources
|
|
1838
|
+
'service_policy': 'f5xc_service_policy',
|
|
1839
|
+
'policies': 'f5xc_service_policy',
|
|
1840
|
+
'rate_limiter': 'f5xc_rate_limiter',
|
|
1841
|
+
'policer': 'f5xc_policer',
|
|
1842
|
+
// Identity resources
|
|
1843
|
+
'user_identification': 'f5xc_user_identification',
|
|
1844
|
+
'authentication': 'f5xc_authentication',
|
|
1845
|
+
// API resources
|
|
1846
|
+
'api_definition': 'f5xc_api_definition',
|
|
1847
|
+
'api_definitions': 'f5xc_api_definition',
|
|
1848
|
+
// Certificate resources
|
|
1849
|
+
'certificate': 'f5xc_certificate',
|
|
1850
|
+
'certificates': 'f5xc_certificate',
|
|
1851
|
+
'crl': 'f5xc_crl',
|
|
1852
|
+
'trusted_ca': 'f5xc_trusted_ca_list',
|
|
1853
|
+
'trusted_ca_list': 'f5xc_trusted_ca_list',
|
|
1854
|
+
// Infrastructure resources
|
|
1855
|
+
'site': 'f5xc_site',
|
|
1856
|
+
'virtual_site': 'f5xc_virtual_site',
|
|
1857
|
+
'virtual_network': 'f5xc_virtual_network',
|
|
1858
|
+
'segment': 'f5xc_segment',
|
|
1859
|
+
'network_connector': 'f5xc_network_connector',
|
|
1860
|
+
'cloud_credentials': 'f5xc_cloud_credentials',
|
|
1861
|
+
'aws_cred': 'f5xc_cloud_credentials',
|
|
1862
|
+
'azure_cred': 'f5xc_cloud_credentials',
|
|
1863
|
+
'gcp_cred': 'f5xc_cloud_credentials',
|
|
1864
|
+
'k8s_cluster': 'f5xc_cluster',
|
|
1865
|
+
'log_receiver': 'f5xc_log_receiver',
|
|
1866
|
+
// Site group resources
|
|
1867
|
+
'dc_cluster_group': 'f5xc_dc_cluster_group',
|
|
1868
|
+
'dc_cluster_group_inside': 'f5xc_dc_cluster_group',
|
|
1869
|
+
'dc_cluster_group_sli': 'f5xc_dc_cluster_group',
|
|
1870
|
+
'dc_cluster_group_slo': 'f5xc_dc_cluster_group',
|
|
1871
|
+
// IP/Prefix resources
|
|
1872
|
+
'ip_prefix_set': 'f5xc_ip_prefix_set',
|
|
1873
|
+
'public_ip': 'f5xc_cloud_elastic_ip',
|
|
1874
|
+
};
|
|
1875
|
+
return mappings[fieldName] || `f5xc_${fieldName}`;
|
|
1876
|
+
}
|
|
716
1877
|
// =============================================================================
|
|
717
1878
|
// TOOL DEFINITION
|
|
718
1879
|
// =============================================================================
|
|
719
1880
|
export const METADATA_TOOL_DEFINITION = {
|
|
720
1881
|
name: 'f5xc_terraform_metadata',
|
|
721
|
-
description: 'Query resource metadata for deterministic Terraform configuration generation. Operations: oneof (mutually exclusive fields), validation (regex/range patterns), defaults, enums, attribute (full metadata), requires_replace, tier (subscription requirements), dependencies (creation order), troubleshoot (error remediation), syntax (correct Terraform block vs attribute syntax), summary.',
|
|
1882
|
+
description: 'Query resource metadata for deterministic Terraform configuration generation. Operations: oneof (mutually exclusive fields), validation (regex/range patterns), defaults, enums, attribute (full metadata), requires_replace, tier (subscription requirements), dependencies (creation order), troubleshoot (error remediation), syntax (correct Terraform block vs attribute syntax), validate (check config for syntax errors), example (generate complete working examples), mistakes (common errors and fixes), summary.',
|
|
722
1883
|
};
|
|
723
1884
|
//# sourceMappingURL=metadata.js.map
|