Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/call_validate-terraform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ jobs:
yarn build:generate-terraform-test-config
echo "✅ Test configuration generation completed"

- name: Validate generated Terraform
- name: Validate generated Terraform (JSON and HCL)
id: terraform-validate
continue-on-error: true
run: |
echo "🔍 Validating generated Terraform configuration..."
echo "🔍 Validating generated Terraform configuration (JSON and HCL)..."
echo "📦 Checking Grafana provider version..."

# Extract provider version constraint from generated config
Expand Down Expand Up @@ -185,6 +185,12 @@ jobs:

let message = commentIdentifier + '\n\n';
message += 'All generated terraform configurations are valid and compatible with the Grafana provider schema. 🎉\n\n';

message += '## ✅ Validation Results\n\n';
message += '- **JSON Syntax**: Valid Terraform JSON configuration\n';
message += '- **HCL Syntax**: Valid Terraform HCL configuration\n';
message += '- **Schema Compatibility**: Compatible with Grafana provider\n\n';

message += '**Validated Resources:**\n';
message += '- `grafana_synthetic_monitoring_check` (HTTP, DNS, TCP, Ping, MultiHTTP, Scripted, Traceroute)\n';
message += '- `grafana_synthetic_monitoring_probe` (Public, Private, Online, Offline)\n';
Expand Down
27 changes: 21 additions & 6 deletions scripts/terraform-validation/generate-test-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,18 @@ async function generateConfigs() {
check: fixtures.BASIC_MULTIHTTP_CHECK,
probe: probeFixtures.ONLINE_PROBE,
},
// Scripted Check
{
name: 'basic-scripted',
check: fixtures.BASIC_SCRIPTED_CHECK,
probe: probeFixtures.SCRIPTED_DISABLED_PROBE,
},
// Scripted Check
{
name: 'basic-scripted',
check: fixtures.BASIC_SCRIPTED_CHECK,
probe: probeFixtures.SCRIPTED_DISABLED_PROBE,
},
// Browser Check with template literals
{
name: 'complex-browser',
check: fixtures.COMPLEX_BROWSER_CHECK,
probe: probeFixtures.PRIVATE_PROBE,
},
// Traceroute Check
{
name: 'basic-traceroute',
Expand Down Expand Up @@ -128,6 +134,15 @@ async function generateConfigs() {
fs.writeFileSync(configPath, JSON.stringify(comprehensiveConfig, null, 2));
console.log(`Generated terraform config: ${configPath}`);

// Generate HCL configuration
console.log('🔧 Generating HCL configuration from JSON...');
const { jsonToHcl } = await import('../../src/components/TerraformConfig/terraformJsonToHcl');
const hclContent = jsonToHcl(comprehensiveConfig as TFConfig);
const hclPath = path.join(outputDir, 'testTerraformConfig.tf');
fs.writeFileSync(hclPath, hclContent);
console.log(`✅ HCL configuration generated successfully!`);
console.log(`📄 Generated: ${hclPath}`);

console.log('\n✅ SUCCESS! Generated comprehensive configuration using REAL production code!');
console.log('✅ Covers: HTTP, DNS, TCP, Ping, MultiHTTP, Scripted, Traceroute checks');
console.log('✅ Covers: Public, Private, Online, Offline, Scripted-disabled probes');
Expand Down
30 changes: 25 additions & 5 deletions scripts/terraform-validation/verify-terraform-test-config.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash

# Terraform validation script
# This script changes to the terraform validation directory and runs terraform validate
# This script validates both JSON and HCL formats

set -e

Expand All @@ -27,16 +27,36 @@ echo
# Change to terraform directory and run validation
cd "$TERRAFORM_DIR"

# Initialize terraform if not already done
# Prevent duplicate configurations (move HCL file)
echo "🧪 Prevent duplicate configurations..."
if [ -f "testTerraformConfig.tf" ]; then
echo " → Moving HCL file temporarily..."
mv testTerraformConfig.tf testTerraformConfig.tf.bak
fi

# Initialize terraform if not already done (now only JSON exists)
if [ ! -d ".terraform" ]; then
echo "📦 Initializing terraform..."
terraform init
echo
fi
echo " → Running terraform validate for JSON..."
terraform validate
echo "✅ JSON validation passed!"
echo " → Removing JSON file..."
rm testTerraformConfig.tf.json
echo

# Restore HCL file and validate HCL format
echo "🧪 Restoring HCL configuration..."
if [ -f "testTerraformConfig.tf.bak" ]; then
echo " → Restoring HCL file..."
mv testTerraformConfig.tf.bak testTerraformConfig.tf
fi

# Run terraform validate
echo "🧪 Running terraform validate..."
echo " → Running terraform validate for HCL..."
terraform validate
echo "✅ HCL validation passed!"

echo
echo "✅ Terraform validation completed successfully!"
echo "✅ Both JSON and HCL terraform validation completed successfully!"
77 changes: 77 additions & 0 deletions src/components/TerraformConfig/hcl/core/HclWriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { HCL_CONFIG } from './hclConfig';
import { HclValue, HclWriterInterface } from './hclTypes';
import { isHclArray, isHclObject } from './hclUtils';

export class HclWriter implements HclWriterInterface {
constructor(public readonly indentLevel = 0, public readonly indentSize = HCL_CONFIG.INDENT_SIZE) {}

indent(): string {
return ' '.repeat(this.indentLevel * this.indentSize);
}

child(): HclWriterInterface {
return new HclWriter(this.indentLevel + 1, this.indentSize);
}

private escapeHclString(str: string): string {
return str
.replace(/\\/g, HCL_CONFIG.ESCAPE_CHARS['\\'])
.replace(/"/g, HCL_CONFIG.ESCAPE_CHARS['"'])
.replace(/\n/g, HCL_CONFIG.ESCAPE_CHARS['\n'])
.replace(/\r/g, HCL_CONFIG.ESCAPE_CHARS['\r'])
.replace(/\t/g, HCL_CONFIG.ESCAPE_CHARS['\t'])
.replace(/\$\{/g, HCL_CONFIG.ESCAPE_CHARS['${']);
}

writeValue(value: HclValue): string {
if (value === null || value === undefined) {
return 'null';
}

if (typeof value === 'string') {
if (value.includes('\n')) {
const escapedValue = value.replace(/\$\{/g, HCL_CONFIG.ESCAPE_CHARS['${']);
return `<<EOF\n${escapedValue}\nEOF`;
}
return `"${this.escapeHclString(value)}"`;
}

if (typeof value === 'boolean') {
return value.toString();
}

if (typeof value === 'number') {
return value.toString();
}

if (isHclArray(value)) {
const items = value.map((item) => this.writeValue(item));
return `[${items.join(', ')}]`;
}

if (isHclObject(value)) {
const entries = Object.entries(value)
.filter(([_, v]) => v !== null && v !== undefined)
.map(([k, v]) => `${k} = ${this.writeValue(v)}`);
return `{ ${entries.join(', ')} }`;
}

return String(value);
}

writeBlock(name: string, content: string[]): string[] {
if (content.length === 0) {
return [];
}

const lines: string[] = [];
lines.push(`${this.indent()}${name} {`);
lines.push(...content);
lines.push(`${this.indent()}}`);
return lines;
}

writeArgument(key: string, value: HclValue): string {
return `${this.indent()}${key} = ${this.writeValue(value)}`;
}
}
23 changes: 23 additions & 0 deletions src/components/TerraformConfig/hcl/core/hclConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const HCL_CONFIG = {
BLOCK_FIELDS: new Set([
'tls_config',
'basic_auth',
'query_fields',
'variables',
'fail_if_header_matches_regexp',
'fail_if_header_not_matches_regexp',
'validate_answer_rrs',
'validate_authority_rrs',
'validate_additional_rrs',
'query_response'
]),
INDENT_SIZE: 2,
ESCAPE_CHARS: {
'\\': '\\\\',
'"': '\\"',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'${': '$$${',
},
};
13 changes: 13 additions & 0 deletions src/components/TerraformConfig/hcl/core/hclTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type HclValue = string | number | boolean | null | undefined | any[] | Record<string, any>;

export type FormatterFunction = (settings: Record<string, HclValue>, writer: HclWriterInterface) => string[];

export interface HclWriterInterface {
readonly indentLevel: number;
readonly indentSize: number;
indent(): string;
child(): HclWriterInterface;
writeValue(value: HclValue): string;
writeBlock(name: string, content: string[]): string[];
writeArgument(key: string, value: HclValue): string;
}
70 changes: 70 additions & 0 deletions src/components/TerraformConfig/hcl/core/hclUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { HCL_CONFIG } from './hclConfig';
import { HclValue, HclWriterInterface } from './hclTypes';

export function isHclObject(value: HclValue): value is Record<string, HclValue> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

export function isHclArray(value: HclValue): value is HclValue[] {
return Array.isArray(value);
}

export function isEmptyArray(value: HclValue): boolean {
return isHclArray(value) && value.length === 0;
}

export function isEmptyObject(value: HclValue): boolean {
return isHclObject(value) && Object.keys(value).length === 0;
}

export function shouldRenderAsBlock(key: string, value: HclValue): boolean {
return HCL_CONFIG.BLOCK_FIELDS.has(key) && (isHclObject(value) || isHclArray(value));
}

export function renderFieldAsBlock(key: string, value: HclValue, writer: HclWriterInterface): string[] {
const lines: string[] = [];

if (isHclArray(value)) {
value.forEach((item) => {
if (isHclObject(item)) {
const entries = Object.entries(item).filter(([_, v]) => v !== null && v !== undefined);
if (entries.length === 0) {
return;
}

const blockContent: string[] = [];
const blockWriter = writer.child();

entries.forEach(([objKey, objValue]) => {
blockContent.push(blockWriter.writeArgument(objKey, objValue as HclValue));
});

if (blockContent.length > 0) {
lines.push(...writer.writeBlock(key, blockContent));
}
}
});
return lines;
}

if (isHclObject(value)) {
const entries = Object.entries(value).filter(([_, v]) => v !== null && v !== undefined);

if (entries.length === 0) {
return lines;
}

const blockContent: string[] = [];
const blockWriter = writer.child();

entries.forEach(([objKey, objValue]) => {
blockContent.push(blockWriter.writeArgument(objKey, objValue as HclValue));
});

if (blockContent.length > 0) {
lines.push(...writer.writeBlock(key, blockContent));
}
}

return lines;
}
34 changes: 34 additions & 0 deletions src/components/TerraformConfig/hcl/formatters/baseFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { HclValue, HclWriterInterface } from '../core/hclTypes';
import { isEmptyArray, isEmptyObject,renderFieldAsBlock, shouldRenderAsBlock } from '../core/hclUtils';

export function formatSettingsToHcl(
settings: Record<string, HclValue>,
writer: HclWriterInterface,
specialHandlers?: Record<string, (value: HclValue, writer: HclWriterInterface) => string[]>
): string[] {
const lines: string[] = [];
const childWriter = writer.child();

Object.entries(settings).forEach(([key, value]) => {
if (value === null || value === undefined) {
return;
}

if (specialHandlers && specialHandlers[key]) {
lines.push(...specialHandlers[key](value, childWriter));
return;
}

if (shouldRenderAsBlock(key, value)) {
if (isEmptyArray(value) || isEmptyObject(value)) {
return;
}
lines.push(...renderFieldAsBlock(key, value, childWriter));
return;
}

lines.push(childWriter.writeArgument(key, value as HclValue));
});

return lines;
}
31 changes: 31 additions & 0 deletions src/components/TerraformConfig/hcl/formatters/grpcFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { HclValue, HclWriterInterface } from '../core/hclTypes';
import { isHclObject } from '../core/hclUtils';
import { formatSettingsToHcl } from './baseFormatter';

export function formatGrpcSettingsToHcl(
grpcSettings: Record<string, HclValue>,
writer: HclWriterInterface
): string[] {
const specialHandlers = {
tls_config: (tlsConfig: HclValue, writer: HclWriterInterface) => {
if (isHclObject(tlsConfig)) {
const entries = Object.entries(tlsConfig).filter(([_, v]) => v !== null && v !== undefined);
if (entries.length === 0) {
return [];
}

const tlsLines: string[] = [];
const tlsWriter = writer.child();

entries.forEach(([tlsKey, tlsValue]) => {
tlsLines.push(tlsWriter.writeArgument(tlsKey, tlsValue as HclValue));
});

return writer.writeBlock('tls_config', tlsLines);
}
return [];
},
};

return formatSettingsToHcl(grpcSettings, writer, specialHandlers);
}
Loading