@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
package/src/pseudo.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PseudoParameter, createPseudoParameters } from "@intentius/chant/pseudo-parameter";
|
|
2
|
+
|
|
3
|
+
export { PseudoParameter };
|
|
4
|
+
|
|
5
|
+
export const { ProjectId, Region, Zone } =
|
|
6
|
+
createPseudoParameters({
|
|
7
|
+
ProjectId: "GCP::ProjectId",
|
|
8
|
+
Region: "GCP::Region",
|
|
9
|
+
Zone: "GCP::Zone",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GCP namespace containing all pseudo-parameters.
|
|
14
|
+
*/
|
|
15
|
+
export const GCP = {
|
|
16
|
+
ProjectId,
|
|
17
|
+
Region,
|
|
18
|
+
Zone,
|
|
19
|
+
} as const;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { gcpSerializer } from "./serializer";
|
|
3
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
4
|
+
import {
|
|
5
|
+
defaultLabels,
|
|
6
|
+
defaultAnnotations,
|
|
7
|
+
DEFAULT_LABELS_MARKER,
|
|
8
|
+
DEFAULT_ANNOTATIONS_MARKER,
|
|
9
|
+
} from "./default-labels";
|
|
10
|
+
|
|
11
|
+
// ── Mock helpers ────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function mockResource(
|
|
14
|
+
entityType: string,
|
|
15
|
+
props: Record<string, unknown>,
|
|
16
|
+
): any {
|
|
17
|
+
return {
|
|
18
|
+
[DECLARABLE_MARKER]: true,
|
|
19
|
+
lexicon: "gcp",
|
|
20
|
+
entityType,
|
|
21
|
+
kind: "resource",
|
|
22
|
+
props,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mockProperty(
|
|
27
|
+
entityType: string,
|
|
28
|
+
props: Record<string, unknown>,
|
|
29
|
+
): any {
|
|
30
|
+
return {
|
|
31
|
+
[DECLARABLE_MARKER]: true,
|
|
32
|
+
lexicon: "gcp",
|
|
33
|
+
entityType,
|
|
34
|
+
kind: "property",
|
|
35
|
+
props,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Tests ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("gcpSerializer", () => {
|
|
42
|
+
test("name is gcp", () => {
|
|
43
|
+
expect(gcpSerializer.name).toBe("gcp");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("rulePrefix is WGC", () => {
|
|
47
|
+
expect(gcpSerializer.rulePrefix).toBe("WGC");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("empty entities produce empty string", () => {
|
|
51
|
+
const result = gcpSerializer.serialize(new Map());
|
|
52
|
+
expect(result).toBe("");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("single resource produces valid YAML with apiVersion/kind/metadata/spec", () => {
|
|
56
|
+
const entities = new Map<string, any>();
|
|
57
|
+
entities.set(
|
|
58
|
+
"myBucket",
|
|
59
|
+
mockResource("GCP::Storage::Bucket", {
|
|
60
|
+
location: "US",
|
|
61
|
+
storageClass: "STANDARD",
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const result = gcpSerializer.serialize(entities);
|
|
66
|
+
expect(result).toContain("apiVersion:");
|
|
67
|
+
expect(result).toContain("kind:");
|
|
68
|
+
expect(result).toContain("metadata:");
|
|
69
|
+
expect(result).toContain("spec:");
|
|
70
|
+
expect(result).toContain("location: US");
|
|
71
|
+
expect(result).toContain("storageClass: STANDARD");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("metadata.name auto-generated from logical name (camelCase→kebab-case)", () => {
|
|
75
|
+
const entities = new Map<string, any>();
|
|
76
|
+
entities.set(
|
|
77
|
+
"myDataBucket",
|
|
78
|
+
mockResource("GCP::Storage::Bucket", {
|
|
79
|
+
location: "US",
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const result = gcpSerializer.serialize(entities);
|
|
84
|
+
expect(result).toContain("name: my-data-bucket");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("explicit metadata.name preserved", () => {
|
|
88
|
+
const entities = new Map<string, any>();
|
|
89
|
+
entities.set(
|
|
90
|
+
"myBucket",
|
|
91
|
+
mockResource("GCP::Storage::Bucket", {
|
|
92
|
+
metadata: { name: "custom-bucket-name" },
|
|
93
|
+
location: "US",
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const result = gcpSerializer.serialize(entities);
|
|
98
|
+
expect(result).toContain("name: custom-bucket-name");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("all GCP resources use spec (no specless types)", () => {
|
|
102
|
+
const entities = new Map<string, any>();
|
|
103
|
+
entities.set(
|
|
104
|
+
"instance",
|
|
105
|
+
mockResource("GCP::Compute::Instance", {
|
|
106
|
+
machineType: "e2-medium",
|
|
107
|
+
zone: "us-central1-a",
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const result = gcpSerializer.serialize(entities);
|
|
112
|
+
expect(result).toContain("spec:");
|
|
113
|
+
expect(result).toContain("machineType: e2-medium");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("multi-resource entities joined by ---", () => {
|
|
117
|
+
const entities = new Map<string, any>();
|
|
118
|
+
entities.set(
|
|
119
|
+
"bucket",
|
|
120
|
+
mockResource("GCP::Storage::Bucket", {
|
|
121
|
+
location: "US",
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
entities.set(
|
|
125
|
+
"instance",
|
|
126
|
+
mockResource("GCP::Compute::Instance", {
|
|
127
|
+
machineType: "e2-medium",
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const result = gcpSerializer.serialize(entities);
|
|
132
|
+
expect(result).toContain("---");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("default labels merged into metadata.labels", () => {
|
|
136
|
+
const entities = new Map<string, any>();
|
|
137
|
+
entities.set("labels", defaultLabels({ env: "prod" }));
|
|
138
|
+
entities.set(
|
|
139
|
+
"bucket",
|
|
140
|
+
mockResource("GCP::Storage::Bucket", {
|
|
141
|
+
location: "US",
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const result = gcpSerializer.serialize(entities);
|
|
146
|
+
expect(result).toContain("env: prod");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("default annotations merged into metadata.annotations", () => {
|
|
150
|
+
const entities = new Map<string, any>();
|
|
151
|
+
entities.set("annot", defaultAnnotations({ "cnrm.cloud.google.com/project-id": "my-project" }));
|
|
152
|
+
entities.set(
|
|
153
|
+
"bucket",
|
|
154
|
+
mockResource("GCP::Storage::Bucket", {
|
|
155
|
+
location: "US",
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const result = gcpSerializer.serialize(entities);
|
|
160
|
+
expect(result).toContain("cnrm.cloud.google.com/project-id: my-project");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("explicit labels override default labels", () => {
|
|
164
|
+
const entities = new Map<string, any>();
|
|
165
|
+
entities.set("labels", defaultLabels({ env: "dev" }));
|
|
166
|
+
entities.set(
|
|
167
|
+
"bucket",
|
|
168
|
+
mockResource("GCP::Storage::Bucket", {
|
|
169
|
+
metadata: { labels: { env: "prod" } },
|
|
170
|
+
location: "US",
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const result = gcpSerializer.serialize(entities);
|
|
175
|
+
expect(result).toContain("env: prod");
|
|
176
|
+
// Should not contain "dev" since explicit overrides
|
|
177
|
+
const envLines = result.split("\n").filter((l: string) => l.includes("env:"));
|
|
178
|
+
expect(envLines.every((l: string) => !l.includes("dev"))).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("property entities skipped in output", () => {
|
|
182
|
+
const entities = new Map<string, any>();
|
|
183
|
+
entities.set("netConfig", mockProperty("GCP::Compute::NetworkConfig", { name: "config" }));
|
|
184
|
+
entities.set(
|
|
185
|
+
"instance",
|
|
186
|
+
mockResource("GCP::Compute::Instance", {
|
|
187
|
+
machineType: "e2-medium",
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const result = gcpSerializer.serialize(entities);
|
|
192
|
+
// Only one document — property entities should not appear as separate docs
|
|
193
|
+
expect(result.split("---").length).toBeLessThanOrEqual(2);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("DefaultLabels entities skipped in output", () => {
|
|
197
|
+
const entities = new Map<string, any>();
|
|
198
|
+
entities.set("labels", defaultLabels({ env: "prod" }));
|
|
199
|
+
|
|
200
|
+
const result = gcpSerializer.serialize(entities);
|
|
201
|
+
// defaultLabels alone should not produce any output
|
|
202
|
+
expect(result).toBe("");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("DefaultAnnotations entities skipped in output", () => {
|
|
206
|
+
const entities = new Map<string, any>();
|
|
207
|
+
entities.set("annotations", defaultAnnotations({ note: "hi" }));
|
|
208
|
+
|
|
209
|
+
const result = gcpSerializer.serialize(entities);
|
|
210
|
+
expect(result).toBe("");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("key ordering: apiVersion, kind, metadata, spec", () => {
|
|
214
|
+
const entities = new Map<string, any>();
|
|
215
|
+
entities.set(
|
|
216
|
+
"bucket",
|
|
217
|
+
mockResource("GCP::Storage::Bucket", {
|
|
218
|
+
location: "US",
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const result = gcpSerializer.serialize(entities);
|
|
223
|
+
const lines = result.split("\n");
|
|
224
|
+
const keyLines = lines.filter((l: string) => /^\w+:/.test(l));
|
|
225
|
+
const keys = keyLines.map((l: string) => l.split(":")[0]);
|
|
226
|
+
|
|
227
|
+
const apiIdx = keys.indexOf("apiVersion");
|
|
228
|
+
const kindIdx = keys.indexOf("kind");
|
|
229
|
+
const metaIdx = keys.indexOf("metadata");
|
|
230
|
+
const specIdx = keys.indexOf("spec");
|
|
231
|
+
|
|
232
|
+
expect(apiIdx).toBeLessThan(kindIdx);
|
|
233
|
+
expect(kindIdx).toBeLessThan(metaIdx);
|
|
234
|
+
expect(metaIdx).toBeLessThan(specIdx);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("fallback GVK derivation (GCP::Compute::Instance → compute.cnrm.cloud.google.com/v1beta1)", () => {
|
|
238
|
+
const entities = new Map<string, any>();
|
|
239
|
+
entities.set(
|
|
240
|
+
"myInstance",
|
|
241
|
+
mockResource("GCP::Compute::Instance", {
|
|
242
|
+
machineType: "e2-medium",
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const result = gcpSerializer.serialize(entities);
|
|
247
|
+
expect(result).toContain("compute.cnrm.cloud.google.com/v1beta1");
|
|
248
|
+
expect(result).toContain("ComputeInstance");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GCP Config Connector YAML serializer.
|
|
3
|
+
*
|
|
4
|
+
* Converts Chant declarables to multi-document K8s YAML output
|
|
5
|
+
* for Config Connector resources with apiVersion, kind, metadata, and spec.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
10
|
+
import { isPropertyDeclarable } from "@intentius/chant/declarable";
|
|
11
|
+
import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
12
|
+
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
13
|
+
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
14
|
+
import { emitYAML } from "@intentius/chant/yaml";
|
|
15
|
+
import { isDefaultLabels, isDefaultAnnotations, type DefaultLabels, type DefaultAnnotations } from "./default-labels";
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GVK mapping entry — loaded from generated lexicon-gcp.json.
|
|
21
|
+
*/
|
|
22
|
+
interface GVKEntry {
|
|
23
|
+
resourceType: string;
|
|
24
|
+
kind: "resource" | "property";
|
|
25
|
+
apiVersion?: string;
|
|
26
|
+
gvkKind?: string;
|
|
27
|
+
group?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let cachedGVKMap: Record<string, GVKEntry> | null = null;
|
|
31
|
+
|
|
32
|
+
function getGVKMap(): Record<string, GVKEntry> {
|
|
33
|
+
if (cachedGVKMap) return cachedGVKMap;
|
|
34
|
+
try {
|
|
35
|
+
cachedGVKMap = require("./generated/lexicon-gcp.json") as Record<string, GVKEntry>;
|
|
36
|
+
} catch {
|
|
37
|
+
cachedGVKMap = {};
|
|
38
|
+
}
|
|
39
|
+
return cachedGVKMap!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve entityType to apiVersion and kind.
|
|
44
|
+
*/
|
|
45
|
+
function resolveGVK(entityType: string): { apiVersion: string; kind: string } | null {
|
|
46
|
+
const gvkMap = getGVKMap();
|
|
47
|
+
|
|
48
|
+
for (const entry of Object.values(gvkMap)) {
|
|
49
|
+
if (entry.resourceType === entityType && entry.apiVersion && entry.gvkKind) {
|
|
50
|
+
return { apiVersion: entry.apiVersion, kind: entry.gvkKind };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback: derive from entity type string (GCP::Service::Kind → service.cnrm.cloud.google.com/v1beta1, Kind)
|
|
55
|
+
return deriveGVKFromType(entityType);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive GVK from a GCP type name.
|
|
60
|
+
* "GCP::Compute::Instance" → { apiVersion: "compute.cnrm.cloud.google.com/v1beta1", kind: "ComputeInstance" }
|
|
61
|
+
*/
|
|
62
|
+
function deriveGVKFromType(entityType: string): { apiVersion: string; kind: string } | null {
|
|
63
|
+
const parts = entityType.split("::");
|
|
64
|
+
if (parts.length !== 3 || parts[0] !== "GCP") return null;
|
|
65
|
+
|
|
66
|
+
const service = parts[1].toLowerCase();
|
|
67
|
+
const shortKind = parts[2];
|
|
68
|
+
const group = `${service}.cnrm.cloud.google.com`;
|
|
69
|
+
const apiVersion = `${group}/v1beta1`;
|
|
70
|
+
const kind = `${parts[1]}${shortKind}`;
|
|
71
|
+
|
|
72
|
+
return { apiVersion, kind };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* GCP visitor for the generic serializer walker.
|
|
77
|
+
*/
|
|
78
|
+
function gcpVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
|
|
79
|
+
return {
|
|
80
|
+
attrRef: (name, _attr) => {
|
|
81
|
+
// For Config Connector, references resolve to metadata.name
|
|
82
|
+
return name;
|
|
83
|
+
},
|
|
84
|
+
resourceRef: (name) => {
|
|
85
|
+
// Config Connector uses resourceRef pattern: { name: "resource-name" }
|
|
86
|
+
return { name };
|
|
87
|
+
},
|
|
88
|
+
propertyDeclarable: (entity, walk) => {
|
|
89
|
+
if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const props = entity.props as Record<string, unknown>;
|
|
93
|
+
const result: Record<string, unknown> = {};
|
|
94
|
+
for (const [key, value] of Object.entries(props)) {
|
|
95
|
+
if (value !== undefined) {
|
|
96
|
+
result[key] = walk(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Convert a value to YAML-compatible form using the walker.
|
|
106
|
+
*/
|
|
107
|
+
function toYAMLValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
|
|
108
|
+
return walkValue(value, entityNames, gcpVisitor(entityNames));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* GCP Config Connector YAML serializer implementation.
|
|
113
|
+
*/
|
|
114
|
+
export const gcpSerializer: Serializer = {
|
|
115
|
+
name: "gcp",
|
|
116
|
+
rulePrefix: "WGC",
|
|
117
|
+
|
|
118
|
+
serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[]): string {
|
|
119
|
+
// Build reverse map: entity → name
|
|
120
|
+
const entityNames = new Map<Declarable, string>();
|
|
121
|
+
for (const [name, entity] of entities) {
|
|
122
|
+
entityNames.set(entity, name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Collect default labels and annotations
|
|
126
|
+
let defaultLabelEntries: Record<string, unknown> = {};
|
|
127
|
+
let defaultAnnotationEntries: Record<string, unknown> = {};
|
|
128
|
+
|
|
129
|
+
for (const [, entity] of entities) {
|
|
130
|
+
if (isDefaultLabels(entity)) {
|
|
131
|
+
defaultLabelEntries = { ...defaultLabelEntries, ...(entity as DefaultLabels).labels };
|
|
132
|
+
}
|
|
133
|
+
if (isDefaultAnnotations(entity)) {
|
|
134
|
+
defaultAnnotationEntries = { ...defaultAnnotationEntries, ...(entity as DefaultAnnotations).annotations };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const documents: string[] = [];
|
|
139
|
+
|
|
140
|
+
for (const [name, entity] of entities) {
|
|
141
|
+
if (isPropertyDeclarable(entity)) continue;
|
|
142
|
+
if (isDefaultLabels(entity) || isDefaultAnnotations(entity)) continue;
|
|
143
|
+
|
|
144
|
+
const entityType = (entity as unknown as Record<string, unknown>).entityType as string;
|
|
145
|
+
const gvk = resolveGVK(entityType);
|
|
146
|
+
if (!gvk) continue;
|
|
147
|
+
|
|
148
|
+
const props = toYAMLValue(
|
|
149
|
+
(entity as unknown as Record<string, unknown>).props,
|
|
150
|
+
entityNames,
|
|
151
|
+
) as Record<string, unknown> | undefined;
|
|
152
|
+
|
|
153
|
+
if (!props) continue;
|
|
154
|
+
|
|
155
|
+
// Build the Config Connector manifest structure
|
|
156
|
+
const manifest: Record<string, unknown> = {
|
|
157
|
+
apiVersion: gvk.apiVersion,
|
|
158
|
+
kind: gvk.kind,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Build metadata
|
|
162
|
+
const metadata: Record<string, unknown> = props.metadata as Record<string, unknown> ?? {};
|
|
163
|
+
if (!metadata.name) {
|
|
164
|
+
metadata.name = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Merge default labels
|
|
168
|
+
if (Object.keys(defaultLabelEntries).length > 0) {
|
|
169
|
+
const existingLabels = (metadata.labels ?? {}) as Record<string, unknown>;
|
|
170
|
+
metadata.labels = { ...defaultLabelEntries, ...existingLabels };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Merge default annotations
|
|
174
|
+
if (Object.keys(defaultAnnotationEntries).length > 0) {
|
|
175
|
+
const existingAnnotations = (metadata.annotations ?? {}) as Record<string, unknown>;
|
|
176
|
+
metadata.annotations = { ...defaultAnnotationEntries, ...existingAnnotations };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
manifest.metadata = metadata;
|
|
180
|
+
|
|
181
|
+
// All Config Connector resources use spec
|
|
182
|
+
const spec: Record<string, unknown> = {};
|
|
183
|
+
for (const [key, value] of Object.entries(props)) {
|
|
184
|
+
if (key !== "metadata") {
|
|
185
|
+
spec[key] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (Object.keys(spec).length > 0) {
|
|
189
|
+
manifest.spec = spec;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const yamlDoc = emitK8sManifest(manifest);
|
|
193
|
+
documents.push(yamlDoc);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return documents.join("\n---\n");
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Emit a key-value pair as YAML.
|
|
202
|
+
*/
|
|
203
|
+
function emitKeyValue(key: string, value: unknown): string {
|
|
204
|
+
const yamlStr = emitYAML(value, 1);
|
|
205
|
+
if (yamlStr.startsWith("\n")) {
|
|
206
|
+
return `${key}:${yamlStr}`;
|
|
207
|
+
}
|
|
208
|
+
return `${key}: ${yamlStr}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Emit a Config Connector manifest object as YAML.
|
|
213
|
+
* Preserves key ordering: apiVersion, kind, metadata, spec.
|
|
214
|
+
*/
|
|
215
|
+
function emitK8sManifest(manifest: Record<string, unknown>): string {
|
|
216
|
+
const orderedKeys = ["apiVersion", "kind", "metadata", "spec"];
|
|
217
|
+
const lines: string[] = [];
|
|
218
|
+
|
|
219
|
+
for (const key of orderedKeys) {
|
|
220
|
+
if (manifest[key] !== undefined) {
|
|
221
|
+
lines.push(emitKeyValue(key, manifest[key]));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const [key, value] of Object.entries(manifest)) {
|
|
226
|
+
if (!orderedKeys.includes(key) && value !== undefined) {
|
|
227
|
+
lines.push(emitKeyValue(key, value));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return lines.join("\n") + "\n";
|
|
232
|
+
}
|