@intentius/chant-lexicon-gcp 0.0.15
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/dist/integrity.json +36 -0
- package/dist/manifest.json +12 -0
- package/dist/meta.json +10919 -0
- package/dist/rules/gcp-helpers.ts +117 -0
- package/dist/rules/hardcoded-project.ts +58 -0
- package/dist/rules/hardcoded-region.ts +56 -0
- package/dist/rules/public-iam.ts +43 -0
- package/dist/rules/wgc101.ts +56 -0
- package/dist/rules/wgc102.ts +35 -0
- package/dist/rules/wgc103.ts +45 -0
- package/dist/rules/wgc104.ts +42 -0
- package/dist/rules/wgc105.ts +46 -0
- package/dist/rules/wgc106.ts +36 -0
- package/dist/rules/wgc107.ts +39 -0
- package/dist/rules/wgc108.ts +41 -0
- package/dist/rules/wgc109.ts +39 -0
- package/dist/rules/wgc110.ts +38 -0
- package/dist/rules/wgc111.ts +54 -0
- package/dist/rules/wgc112.ts +56 -0
- package/dist/rules/wgc113.ts +42 -0
- package/dist/rules/wgc201.ts +36 -0
- package/dist/rules/wgc202.ts +39 -0
- package/dist/rules/wgc203.ts +44 -0
- package/dist/rules/wgc204.ts +39 -0
- package/dist/rules/wgc301.ts +34 -0
- package/dist/rules/wgc302.ts +34 -0
- package/dist/rules/wgc303.ts +37 -0
- package/dist/skills/chant-gcp-patterns.md +367 -0
- package/dist/skills/chant-gcp-security.md +276 -0
- package/dist/skills/chant-gcp.md +108 -0
- package/dist/types/index.d.ts +26529 -0
- package/package.json +35 -0
- package/src/actions/index.ts +52 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +820 -0
- package/src/codegen/generate-cli.ts +24 -0
- package/src/codegen/generate.ts +252 -0
- package/src/codegen/naming.test.ts +49 -0
- package/src/codegen/naming.ts +132 -0
- package/src/codegen/package.ts +66 -0
- package/src/composites/cloud-function.ts +117 -0
- package/src/composites/cloud-run-service.ts +124 -0
- package/src/composites/cloud-sql-instance.ts +126 -0
- package/src/composites/composites.test.ts +432 -0
- package/src/composites/gcs-bucket.ts +111 -0
- package/src/composites/gke-cluster.ts +125 -0
- package/src/composites/index.ts +20 -0
- package/src/composites/managed-certificate.ts +79 -0
- package/src/composites/private-service.ts +95 -0
- package/src/composites/pubsub-pipeline.ts +102 -0
- package/src/composites/secure-project.ts +128 -0
- package/src/composites/vpc-network.ts +165 -0
- package/src/coverage.test.ts +27 -0
- package/src/coverage.ts +51 -0
- package/src/default-labels.test.ts +111 -0
- package/src/default-labels.ts +93 -0
- package/src/generated/index.d.ts +26529 -0
- package/src/generated/index.ts +1723 -0
- package/src/generated/lexicon-gcp.json +10919 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +125 -0
- package/src/import/generator.ts +82 -0
- package/src/import/parser.test.ts +167 -0
- package/src/import/parser.ts +80 -0
- package/src/import/roundtrip.test.ts +66 -0
- package/src/index.ts +54 -0
- package/src/lint/post-synth/gcp-helpers.ts +117 -0
- package/src/lint/post-synth/index.ts +20 -0
- package/src/lint/post-synth/post-synth.test.ts +693 -0
- package/src/lint/post-synth/wgc101.ts +56 -0
- package/src/lint/post-synth/wgc102.ts +35 -0
- package/src/lint/post-synth/wgc103.ts +45 -0
- package/src/lint/post-synth/wgc104.ts +42 -0
- package/src/lint/post-synth/wgc105.ts +46 -0
- package/src/lint/post-synth/wgc106.ts +36 -0
- package/src/lint/post-synth/wgc107.ts +39 -0
- package/src/lint/post-synth/wgc108.ts +41 -0
- package/src/lint/post-synth/wgc109.ts +39 -0
- package/src/lint/post-synth/wgc110.ts +38 -0
- package/src/lint/post-synth/wgc111.ts +54 -0
- package/src/lint/post-synth/wgc112.ts +56 -0
- package/src/lint/post-synth/wgc113.ts +42 -0
- package/src/lint/post-synth/wgc201.ts +36 -0
- package/src/lint/post-synth/wgc202.ts +39 -0
- package/src/lint/post-synth/wgc203.ts +44 -0
- package/src/lint/post-synth/wgc204.ts +39 -0
- package/src/lint/post-synth/wgc301.ts +34 -0
- package/src/lint/post-synth/wgc302.ts +34 -0
- package/src/lint/post-synth/wgc303.ts +37 -0
- package/src/lint/rules/hardcoded-project.ts +58 -0
- package/src/lint/rules/hardcoded-region.ts +56 -0
- package/src/lint/rules/index.ts +3 -0
- package/src/lint/rules/public-iam.ts +43 -0
- package/src/lint/rules/rules.test.ts +63 -0
- package/src/lsp/completions.test.ts +67 -0
- package/src/lsp/completions.ts +17 -0
- package/src/lsp/hover.test.ts +66 -0
- package/src/lsp/hover.ts +54 -0
- package/src/package-cli.ts +24 -0
- package/src/plugin.test.ts +250 -0
- package/src/plugin.ts +405 -0
- package/src/pseudo.test.ts +40 -0
- package/src/pseudo.ts +19 -0
- package/src/serializer.test.ts +250 -0
- package/src/serializer.ts +232 -0
- package/src/skills/chant-gcp-patterns.md +367 -0
- package/src/skills/chant-gcp-security.md +276 -0
- package/src/skills/chant-gcp.md +108 -0
- package/src/spec/fetch.test.ts +16 -0
- package/src/spec/fetch.ts +121 -0
- package/src/spec/parse.test.ts +163 -0
- package/src/spec/parse.ts +432 -0
- package/src/testdata/compute-instance.yaml +93 -0
- package/src/testdata/iam-policy-member.yaml +66 -0
- package/src/testdata/manifests/compute-instance.yaml +18 -0
- package/src/testdata/manifests/full-app.yaml +34 -0
- package/src/testdata/manifests/storage-bucket.yaml +12 -0
- package/src/testdata/storage-bucket.yaml +100 -0
- package/src/validate-cli.ts +13 -0
- package/src/validate.test.ts +38 -0
- package/src/validate.ts +30 -0
- package/src/variables.ts +15 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { GcpGenerator } from "./generator";
|
|
3
|
+
|
|
4
|
+
const generator = new GcpGenerator();
|
|
5
|
+
|
|
6
|
+
function makeIR(resources: any[]) {
|
|
7
|
+
return { resources, parameters: [], outputs: [] };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("GcpGenerator", () => {
|
|
11
|
+
test("generates valid TypeScript from IR", () => {
|
|
12
|
+
const ir = makeIR([
|
|
13
|
+
{
|
|
14
|
+
logicalName: "my-bucket",
|
|
15
|
+
type: "GCP::Storage::Bucket",
|
|
16
|
+
properties: {
|
|
17
|
+
metadata: { name: "my-bucket" },
|
|
18
|
+
location: "US",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
]);
|
|
22
|
+
const result = generator.generate(ir);
|
|
23
|
+
expect(result).toContain("import");
|
|
24
|
+
expect(result).toContain("Bucket");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("correct import source (@intentius/chant-lexicon-gcp)", () => {
|
|
28
|
+
const ir = makeIR([
|
|
29
|
+
{
|
|
30
|
+
logicalName: "my-bucket",
|
|
31
|
+
type: "GCP::Storage::Bucket",
|
|
32
|
+
properties: { location: "US" },
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
const result = generator.generate(ir);
|
|
36
|
+
expect(result).toContain('from "@intentius/chant-lexicon-gcp"');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("multiple resources produce multiple exports", () => {
|
|
40
|
+
const ir = makeIR([
|
|
41
|
+
{
|
|
42
|
+
logicalName: "my-bucket",
|
|
43
|
+
type: "GCP::Storage::Bucket",
|
|
44
|
+
properties: { location: "US" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
logicalName: "my-vm",
|
|
48
|
+
type: "GCP::Compute::Instance",
|
|
49
|
+
properties: { machineType: "e2-medium" },
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
const result = generator.generate(ir);
|
|
53
|
+
expect(result).toContain("export const myBucket");
|
|
54
|
+
expect(result).toContain("export const myVm");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("camelCase variable names from kebab-case logical names", () => {
|
|
58
|
+
const ir = makeIR([
|
|
59
|
+
{
|
|
60
|
+
logicalName: "my-data-bucket",
|
|
61
|
+
type: "GCP::Storage::Bucket",
|
|
62
|
+
properties: { location: "US" },
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
const result = generator.generate(ir);
|
|
66
|
+
expect(result).toContain("export const myDataBucket");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("empty IR produces minimal output", () => {
|
|
70
|
+
const ir = makeIR([]);
|
|
71
|
+
const result = generator.generate(ir);
|
|
72
|
+
// Should still produce valid TypeScript, even if no resources
|
|
73
|
+
expect(typeof result).toBe("string");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("nested object formatting", () => {
|
|
77
|
+
const ir = makeIR([
|
|
78
|
+
{
|
|
79
|
+
logicalName: "my-bucket",
|
|
80
|
+
type: "GCP::Storage::Bucket",
|
|
81
|
+
properties: {
|
|
82
|
+
location: "US",
|
|
83
|
+
versioning: { enabled: true },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
const result = generator.generate(ir);
|
|
88
|
+
expect(result).toContain("versioning:");
|
|
89
|
+
expect(result).toContain("enabled: true");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("uses new Constructor() syntax", () => {
|
|
93
|
+
const ir = makeIR([
|
|
94
|
+
{
|
|
95
|
+
logicalName: "my-bucket",
|
|
96
|
+
type: "GCP::Storage::Bucket",
|
|
97
|
+
properties: { location: "US" },
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
const result = generator.generate(ir);
|
|
101
|
+
expect(result).toContain("new Bucket(");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("sorts imports alphabetically", () => {
|
|
105
|
+
const ir = makeIR([
|
|
106
|
+
{
|
|
107
|
+
logicalName: "vm",
|
|
108
|
+
type: "GCP::Compute::Instance",
|
|
109
|
+
properties: { machineType: "e2-medium" },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
logicalName: "bucket",
|
|
113
|
+
type: "GCP::Storage::Bucket",
|
|
114
|
+
properties: { location: "US" },
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
const result = generator.generate(ir);
|
|
118
|
+
const importLine = result.split("\n").find((l: string) => l.startsWith("import"));
|
|
119
|
+
expect(importLine).toBeDefined();
|
|
120
|
+
// Bucket should come before Instance alphabetically
|
|
121
|
+
const bucketIdx = importLine!.indexOf("Bucket");
|
|
122
|
+
const instanceIdx = importLine!.indexOf("Instance");
|
|
123
|
+
expect(bucketIdx).toBeLessThan(instanceIdx);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Connector TypeScript generator.
|
|
3
|
+
*
|
|
4
|
+
* Converts import IR from the parser into typed chant TypeScript code.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TypeScriptGenerator, TemplateIR } from "@intentius/chant/import/generator";
|
|
8
|
+
|
|
9
|
+
export class GcpGenerator implements TypeScriptGenerator {
|
|
10
|
+
generate(ir: TemplateIR): string {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
const imports = new Set<string>();
|
|
13
|
+
|
|
14
|
+
// Collect imports
|
|
15
|
+
for (const resource of ir.resources) {
|
|
16
|
+
const parts = resource.type.split("::");
|
|
17
|
+
if (parts.length >= 3) {
|
|
18
|
+
// Use the short name for the import
|
|
19
|
+
imports.add(parts[2]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (imports.size > 0) {
|
|
24
|
+
lines.push(
|
|
25
|
+
`import { ${[...imports].sort().join(", ")} } from "@intentius/chant-lexicon-gcp";`,
|
|
26
|
+
);
|
|
27
|
+
lines.push("");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Generate resource declarations
|
|
31
|
+
for (const resource of ir.resources) {
|
|
32
|
+
const parts = resource.type.split("::");
|
|
33
|
+
const className = parts.length >= 3 ? parts[2] : resource.type;
|
|
34
|
+
const varName = camelCase(resource.logicalName);
|
|
35
|
+
|
|
36
|
+
lines.push(`export const ${varName} = new ${className}(${formatProps(resource.properties, 0)});`);
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function camelCase(str: string): string {
|
|
45
|
+
return str
|
|
46
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
47
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatProps(props: Record<string, unknown>, indent: number): string {
|
|
51
|
+
const entries = Object.entries(props);
|
|
52
|
+
if (entries.length === 0) return "{}";
|
|
53
|
+
|
|
54
|
+
const pad = " ".repeat(indent + 1);
|
|
55
|
+
const closePad = " ".repeat(indent);
|
|
56
|
+
|
|
57
|
+
const lines = entries.map(([key, value]) => {
|
|
58
|
+
return `${pad}${key}: ${formatValue(value, indent + 1)},`;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return `{\n${lines.join("\n")}\n${closePad}}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatValue(value: unknown, indent: number): string {
|
|
65
|
+
if (value === null || value === undefined) return "undefined";
|
|
66
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
67
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
if (value.length === 0) return "[]";
|
|
71
|
+
const pad = " ".repeat(indent + 1);
|
|
72
|
+
const closePad = " ".repeat(indent);
|
|
73
|
+
const items = value.map((v) => `${pad}${formatValue(v, indent + 1)},`);
|
|
74
|
+
return `[\n${items.join("\n")}\n${closePad}]`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value === "object") {
|
|
78
|
+
return formatProps(value as Record<string, unknown>, indent);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return String(value);
|
|
82
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { GcpParser } from "./parser";
|
|
3
|
+
|
|
4
|
+
const parser = new GcpParser();
|
|
5
|
+
|
|
6
|
+
describe("GcpParser", () => {
|
|
7
|
+
test("empty YAML returns empty resources", () => {
|
|
8
|
+
const ir = parser.parse("");
|
|
9
|
+
expect(ir.resources).toEqual([]);
|
|
10
|
+
expect(ir.parameters).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("single ComputeInstance parses correctly", () => {
|
|
14
|
+
const yaml = `
|
|
15
|
+
apiVersion: compute.cnrm.cloud.google.com/v1beta1
|
|
16
|
+
kind: ComputeInstance
|
|
17
|
+
metadata:
|
|
18
|
+
name: my-vm
|
|
19
|
+
spec:
|
|
20
|
+
machineType: e2-medium
|
|
21
|
+
zone: us-central1-a
|
|
22
|
+
`;
|
|
23
|
+
const ir = parser.parse(yaml);
|
|
24
|
+
expect(ir.resources.length).toBe(1);
|
|
25
|
+
const r = ir.resources[0];
|
|
26
|
+
expect(r.type).toBe("GCP::Compute::Instance");
|
|
27
|
+
expect(r.properties.machineType).toBe("e2-medium");
|
|
28
|
+
expect(r.properties.zone).toBe("us-central1-a");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("multi-doc YAML produces multiple resources", () => {
|
|
32
|
+
const yaml = `
|
|
33
|
+
apiVersion: storage.cnrm.cloud.google.com/v1beta1
|
|
34
|
+
kind: StorageBucket
|
|
35
|
+
metadata:
|
|
36
|
+
name: bucket
|
|
37
|
+
spec:
|
|
38
|
+
location: US
|
|
39
|
+
---
|
|
40
|
+
apiVersion: compute.cnrm.cloud.google.com/v1beta1
|
|
41
|
+
kind: ComputeInstance
|
|
42
|
+
metadata:
|
|
43
|
+
name: vm
|
|
44
|
+
spec:
|
|
45
|
+
machineType: e2-medium
|
|
46
|
+
`;
|
|
47
|
+
const ir = parser.parse(yaml);
|
|
48
|
+
expect(ir.resources.length).toBe(2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("apiVersion+kind maps to GCP type name", () => {
|
|
52
|
+
const yaml = `
|
|
53
|
+
apiVersion: storage.cnrm.cloud.google.com/v1beta1
|
|
54
|
+
kind: StorageBucket
|
|
55
|
+
metadata:
|
|
56
|
+
name: test
|
|
57
|
+
spec:
|
|
58
|
+
location: US
|
|
59
|
+
`;
|
|
60
|
+
const ir = parser.parse(yaml);
|
|
61
|
+
expect(ir.resources[0].type).toBe("GCP::Storage::Bucket");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("IAM resource maps correctly", () => {
|
|
65
|
+
const yaml = `
|
|
66
|
+
apiVersion: iam.cnrm.cloud.google.com/v1beta1
|
|
67
|
+
kind: IAMPolicyMember
|
|
68
|
+
metadata:
|
|
69
|
+
name: binding
|
|
70
|
+
spec:
|
|
71
|
+
member: user:test@example.com
|
|
72
|
+
role: roles/viewer
|
|
73
|
+
`;
|
|
74
|
+
const ir = parser.parse(yaml);
|
|
75
|
+
expect(ir.resources[0].type).toBe("GCP::Iam::PolicyMember");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("non-Config Connector resources ignored", () => {
|
|
79
|
+
const yaml = `
|
|
80
|
+
apiVersion: apps/v1
|
|
81
|
+
kind: Deployment
|
|
82
|
+
metadata:
|
|
83
|
+
name: my-app
|
|
84
|
+
spec:
|
|
85
|
+
replicas: 1
|
|
86
|
+
`;
|
|
87
|
+
const ir = parser.parse(yaml);
|
|
88
|
+
expect(ir.resources).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("metadata.name extracted as logicalName", () => {
|
|
92
|
+
const yaml = `
|
|
93
|
+
apiVersion: compute.cnrm.cloud.google.com/v1beta1
|
|
94
|
+
kind: ComputeNetwork
|
|
95
|
+
metadata:
|
|
96
|
+
name: my-network
|
|
97
|
+
spec:
|
|
98
|
+
autoCreateSubnetworks: false
|
|
99
|
+
`;
|
|
100
|
+
const ir = parser.parse(yaml);
|
|
101
|
+
expect((ir.resources[0] as any).logicalName).toBe("my-network");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("properties include metadata and spec fields, exclude apiVersion/kind", () => {
|
|
105
|
+
const yaml = `
|
|
106
|
+
apiVersion: storage.cnrm.cloud.google.com/v1beta1
|
|
107
|
+
kind: StorageBucket
|
|
108
|
+
metadata:
|
|
109
|
+
name: test
|
|
110
|
+
annotations:
|
|
111
|
+
cnrm.cloud.google.com/project-id: my-project
|
|
112
|
+
spec:
|
|
113
|
+
location: US
|
|
114
|
+
storageClass: STANDARD
|
|
115
|
+
`;
|
|
116
|
+
const ir = parser.parse(yaml);
|
|
117
|
+
const props = ir.resources[0].properties;
|
|
118
|
+
expect((props as any).apiVersion).toBeUndefined();
|
|
119
|
+
expect((props as any).kind).toBeUndefined();
|
|
120
|
+
expect(props.metadata).toBeDefined();
|
|
121
|
+
expect(props.location).toBe("US");
|
|
122
|
+
expect(props.storageClass).toBe("STANDARD");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("parameters are always empty (GCP has no template parameters)", () => {
|
|
126
|
+
const yaml = `
|
|
127
|
+
apiVersion: compute.cnrm.cloud.google.com/v1beta1
|
|
128
|
+
kind: ComputeInstance
|
|
129
|
+
metadata:
|
|
130
|
+
name: test
|
|
131
|
+
spec:
|
|
132
|
+
machineType: e2-medium
|
|
133
|
+
`;
|
|
134
|
+
const ir = parser.parse(yaml);
|
|
135
|
+
expect(ir.parameters).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("mixed Config Connector and non-CC resources filters correctly", () => {
|
|
139
|
+
const yaml = `
|
|
140
|
+
apiVersion: apps/v1
|
|
141
|
+
kind: Deployment
|
|
142
|
+
metadata:
|
|
143
|
+
name: app
|
|
144
|
+
spec:
|
|
145
|
+
replicas: 1
|
|
146
|
+
---
|
|
147
|
+
apiVersion: storage.cnrm.cloud.google.com/v1beta1
|
|
148
|
+
kind: StorageBucket
|
|
149
|
+
metadata:
|
|
150
|
+
name: bucket
|
|
151
|
+
spec:
|
|
152
|
+
location: US
|
|
153
|
+
---
|
|
154
|
+
apiVersion: v1
|
|
155
|
+
kind: Service
|
|
156
|
+
metadata:
|
|
157
|
+
name: svc
|
|
158
|
+
spec:
|
|
159
|
+
ports:
|
|
160
|
+
- port: 80
|
|
161
|
+
`;
|
|
162
|
+
const ir = parser.parse(yaml);
|
|
163
|
+
// Only the StorageBucket should be parsed
|
|
164
|
+
expect(ir.resources.length).toBe(1);
|
|
165
|
+
expect(ir.resources[0].type).toBe("GCP::Storage::Bucket");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Connector YAML template parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses Config Connector YAML manifests into the import IR
|
|
5
|
+
* for conversion to chant TypeScript.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
TemplateParser,
|
|
10
|
+
TemplateIR,
|
|
11
|
+
ResourceIR,
|
|
12
|
+
} from "@intentius/chant/import/parser";
|
|
13
|
+
import { BaseValueParser } from "@intentius/chant/import/base-parser";
|
|
14
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
15
|
+
import { gcpTypeName } from "../spec/parse";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parser for Config Connector YAML manifests.
|
|
19
|
+
*/
|
|
20
|
+
export class GcpParser extends BaseValueParser implements TemplateParser {
|
|
21
|
+
protected dispatchIntrinsic(
|
|
22
|
+
_key: string,
|
|
23
|
+
_value: unknown,
|
|
24
|
+
_obj: Record<string, unknown>,
|
|
25
|
+
): unknown | null {
|
|
26
|
+
// Config Connector YAML has no intrinsic functions
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
parse(input: string): TemplateIR {
|
|
31
|
+
const resources: ResourceIR[] = [];
|
|
32
|
+
const documents = input
|
|
33
|
+
.split(/^---\s*$/m)
|
|
34
|
+
.map((d) => d.trim())
|
|
35
|
+
.filter((d) => d.length > 0);
|
|
36
|
+
|
|
37
|
+
for (const docStr of documents) {
|
|
38
|
+
const doc = parseYAML(docStr) as Record<string, unknown>;
|
|
39
|
+
if (!doc) continue;
|
|
40
|
+
|
|
41
|
+
const apiVersion = doc.apiVersion as string | undefined;
|
|
42
|
+
const kind = doc.kind as string | undefined;
|
|
43
|
+
if (!apiVersion || !kind) continue;
|
|
44
|
+
|
|
45
|
+
// Only handle Config Connector resources
|
|
46
|
+
if (!apiVersion.includes("cnrm.cloud.google.com")) continue;
|
|
47
|
+
|
|
48
|
+
const group = apiVersion.split("/")[0];
|
|
49
|
+
const typeName = gcpTypeName(group, kind);
|
|
50
|
+
|
|
51
|
+
const metadata = doc.metadata as Record<string, unknown> | undefined;
|
|
52
|
+
const spec = doc.spec as Record<string, unknown> | undefined;
|
|
53
|
+
|
|
54
|
+
const logicalName = (metadata?.name as string) ?? kind;
|
|
55
|
+
|
|
56
|
+
// Build properties from spec
|
|
57
|
+
const properties: Record<string, unknown> = {};
|
|
58
|
+
if (metadata) {
|
|
59
|
+
properties.metadata = this.parseValue(metadata);
|
|
60
|
+
}
|
|
61
|
+
if (spec) {
|
|
62
|
+
for (const [key, value] of Object.entries(spec)) {
|
|
63
|
+
properties[key] = this.parseValue(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resources.push({
|
|
68
|
+
logicalName,
|
|
69
|
+
type: typeName,
|
|
70
|
+
properties,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
resources,
|
|
76
|
+
parameters: [],
|
|
77
|
+
outputs: [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { GcpParser } from "./parser";
|
|
6
|
+
import { GcpGenerator } from "./generator";
|
|
7
|
+
|
|
8
|
+
const testdataDir = join(
|
|
9
|
+
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
10
|
+
"testdata",
|
|
11
|
+
"manifests",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const parser = new GcpParser();
|
|
15
|
+
const generator = new GcpGenerator();
|
|
16
|
+
|
|
17
|
+
describe("roundtrip: parse YAML → generate TypeScript", () => {
|
|
18
|
+
test("StorageBucket roundtrip", () => {
|
|
19
|
+
const yaml = readFileSync(join(testdataDir, "storage-bucket.yaml"), "utf-8");
|
|
20
|
+
const ir = parser.parse(yaml);
|
|
21
|
+
const ts = generator.generate(ir);
|
|
22
|
+
|
|
23
|
+
expect(ir.resources.length).toBe(1);
|
|
24
|
+
expect(ts).toContain("new Bucket");
|
|
25
|
+
expect(ts).toContain("export const");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("ComputeInstance roundtrip", () => {
|
|
29
|
+
const yaml = readFileSync(join(testdataDir, "compute-instance.yaml"), "utf-8");
|
|
30
|
+
const ir = parser.parse(yaml);
|
|
31
|
+
const ts = generator.generate(ir);
|
|
32
|
+
|
|
33
|
+
expect(ir.resources.length).toBe(1);
|
|
34
|
+
expect(ts).toContain("new Instance");
|
|
35
|
+
expect(ts).toContain("export const");
|
|
36
|
+
expect(ts).toContain("e2-medium");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("multi-doc full-app roundtrip", () => {
|
|
40
|
+
const yaml = readFileSync(join(testdataDir, "full-app.yaml"), "utf-8");
|
|
41
|
+
const ir = parser.parse(yaml);
|
|
42
|
+
expect(ir.resources.length).toBe(3); // StorageBucket + IAMPolicyMember + ComputeNetwork
|
|
43
|
+
|
|
44
|
+
const ts = generator.generate(ir);
|
|
45
|
+
expect(ts).toContain("Bucket");
|
|
46
|
+
expect(ts).toContain("PolicyMember");
|
|
47
|
+
expect(ts).toContain("Network");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("inline YAML roundtrip", () => {
|
|
51
|
+
const yaml = `
|
|
52
|
+
apiVersion: storage.cnrm.cloud.google.com/v1beta1
|
|
53
|
+
kind: StorageBucket
|
|
54
|
+
metadata:
|
|
55
|
+
name: inline-test
|
|
56
|
+
spec:
|
|
57
|
+
location: EU
|
|
58
|
+
storageClass: NEARLINE
|
|
59
|
+
`;
|
|
60
|
+
const ir = parser.parse(yaml);
|
|
61
|
+
const ts = generator.generate(ir);
|
|
62
|
+
|
|
63
|
+
expect(ts).toContain("new Bucket");
|
|
64
|
+
expect(ts).toContain("export const");
|
|
65
|
+
});
|
|
66
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Serializer
|
|
2
|
+
export { gcpSerializer } from "./serializer";
|
|
3
|
+
|
|
4
|
+
// Plugin
|
|
5
|
+
export { gcpPlugin } from "./plugin";
|
|
6
|
+
|
|
7
|
+
// Default labels & annotations
|
|
8
|
+
export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotations } from "./default-labels";
|
|
9
|
+
export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-labels";
|
|
10
|
+
|
|
11
|
+
// Pseudo-parameters
|
|
12
|
+
export { GCP, ProjectId, Region, Zone } from "./pseudo";
|
|
13
|
+
|
|
14
|
+
// Variables / annotation constants
|
|
15
|
+
export { GcpAnnotations } from "./variables";
|
|
16
|
+
|
|
17
|
+
// Generated entities — export everything from generated index
|
|
18
|
+
// After running `bun run generate`, this re-exports all Config Connector resource classes
|
|
19
|
+
export * from "./generated/index";
|
|
20
|
+
|
|
21
|
+
// Composites
|
|
22
|
+
export {
|
|
23
|
+
GkeCluster, CloudRunService, CloudSqlInstance, GcsBucket, VpcNetwork,
|
|
24
|
+
PubSubPipeline, CloudFunctionWithTrigger, PrivateService, ManagedCertificate, SecureProject,
|
|
25
|
+
} from "./composites/index";
|
|
26
|
+
export type {
|
|
27
|
+
GkeClusterProps, GkeClusterResult,
|
|
28
|
+
CloudRunServiceProps, CloudRunServiceResult,
|
|
29
|
+
CloudSqlInstanceProps, CloudSqlInstanceResult,
|
|
30
|
+
GcsBucketProps, GcsBucketResult,
|
|
31
|
+
VpcNetworkProps, VpcNetworkResult, VpcSubnet,
|
|
32
|
+
PubSubPipelineProps, PubSubPipelineResult,
|
|
33
|
+
CloudFunctionWithTriggerProps, CloudFunctionWithTriggerResult,
|
|
34
|
+
PrivateServiceProps, PrivateServiceResult,
|
|
35
|
+
ManagedCertificateProps, ManagedCertificateResult,
|
|
36
|
+
SecureProjectProps, SecureProjectResult,
|
|
37
|
+
} from "./composites/index";
|
|
38
|
+
|
|
39
|
+
// IAM role constants
|
|
40
|
+
export { StorageRoles, ComputeRoles, ContainerRoles, IAMRoles, SQLRoles, RunRoles, PubSubRoles } from "./actions/index";
|
|
41
|
+
|
|
42
|
+
// Spec utilities (for tooling)
|
|
43
|
+
export { fetchCRDBundle, getCachePath, clearCache, KCC_VERSION } from "./spec/fetch";
|
|
44
|
+
export { parseGcpCRD, gcpServiceName, gcpShortName, gcpTypeName, stripServicePrefix } from "./spec/parse";
|
|
45
|
+
export type { GcpParseResult, ParsedResource, ParsedProperty, ParsedPropertyType, ParsedEnum, GroupVersionKind } from "./spec/parse";
|
|
46
|
+
|
|
47
|
+
// Code generation pipeline
|
|
48
|
+
export { generate, writeGeneratedFiles } from "./codegen/generate";
|
|
49
|
+
export { packageLexicon } from "./codegen/package";
|
|
50
|
+
export type { PackageOptions, PackageResult } from "./codegen/package";
|
|
51
|
+
|
|
52
|
+
// LSP providers
|
|
53
|
+
export { gcpCompletions } from "./lsp/completions";
|
|
54
|
+
export { gcpHover } from "./lsp/hover";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for GCP Config Connector post-synthesis lint rules.
|
|
3
|
+
*
|
|
4
|
+
* Provides YAML parsing for multi-document Config Connector manifests
|
|
5
|
+
* and accessor utilities for common manifest fields.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
9
|
+
export { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A parsed Config Connector manifest (loosely typed).
|
|
13
|
+
*/
|
|
14
|
+
export interface GcpManifest {
|
|
15
|
+
apiVersion?: string;
|
|
16
|
+
kind?: string;
|
|
17
|
+
metadata?: {
|
|
18
|
+
name?: string;
|
|
19
|
+
namespace?: string;
|
|
20
|
+
labels?: Record<string, string>;
|
|
21
|
+
annotations?: Record<string, string>;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
spec?: Record<string, unknown>;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Split a multi-document YAML string on `---` boundaries and parse each
|
|
30
|
+
* document into a GcpManifest.
|
|
31
|
+
*/
|
|
32
|
+
export function parseGcpManifests(yaml: string): GcpManifest[] {
|
|
33
|
+
const documents = yaml.split(/^---\s*$/m);
|
|
34
|
+
const manifests: GcpManifest[] = [];
|
|
35
|
+
|
|
36
|
+
for (const doc of documents) {
|
|
37
|
+
const trimmed = doc.trim();
|
|
38
|
+
if (trimmed === "" || trimmed === "---") continue;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = parseYAML(trimmed);
|
|
41
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
42
|
+
manifests.push(parsed as GcpManifest);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Skip unparseable documents
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return manifests;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a manifest is a Config Connector resource
|
|
54
|
+
* (apiVersion contains cnrm.cloud.google.com).
|
|
55
|
+
*/
|
|
56
|
+
export function isConfigConnectorResource(manifest: GcpManifest): boolean {
|
|
57
|
+
return typeof manifest.apiVersion === "string" &&
|
|
58
|
+
manifest.apiVersion.includes("cnrm.cloud.google.com");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Safely extract the spec from a manifest.
|
|
63
|
+
*/
|
|
64
|
+
export function getSpec(manifest: GcpManifest): Record<string, unknown> | undefined {
|
|
65
|
+
return manifest.spec ?? undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Safely extract annotations from a manifest's metadata.
|
|
70
|
+
*/
|
|
71
|
+
export function getAnnotations(manifest: GcpManifest): Record<string, string> | undefined {
|
|
72
|
+
return manifest.metadata?.annotations ?? undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the resource name from metadata.
|
|
77
|
+
*/
|
|
78
|
+
export function getResourceName(manifest: GcpManifest): string {
|
|
79
|
+
return manifest.metadata?.name ?? "unknown";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Recursively walk a spec object looking for keys ending in `Ref`
|
|
84
|
+
* (e.g. `networkRef`, `topicRef`, `clusterRef`) that have a `name`
|
|
85
|
+
* sub-field. Returns the set of referenced names.
|
|
86
|
+
*
|
|
87
|
+
* Skips `external` refs (cross-project references outside the template).
|
|
88
|
+
*/
|
|
89
|
+
export function findResourceRefs(obj: unknown): Set<string> {
|
|
90
|
+
const refs = new Set<string>();
|
|
91
|
+
walkForRefs(obj, refs);
|
|
92
|
+
return refs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function walkForRefs(value: unknown, refs: Set<string>): void {
|
|
96
|
+
if (value === null || value === undefined) return;
|
|
97
|
+
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
for (const item of value) {
|
|
100
|
+
walkForRefs(item, refs);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof value === "object") {
|
|
106
|
+
const obj = value as Record<string, unknown>;
|
|
107
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
108
|
+
if (key.endsWith("Ref") && typeof val === "object" && val !== null) {
|
|
109
|
+
const refObj = val as Record<string, unknown>;
|
|
110
|
+
if (typeof refObj.name === "string" && !refObj.external) {
|
|
111
|
+
refs.add(refObj.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
walkForRefs(val, refs);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|