@intentius/chant-lexicon-gcp 0.124.35 → 0.124.37

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.
Files changed (39) hide show
  1. package/dist/api/read-client.d.ts +2 -1
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/api/read-client.js +2 -1
  4. package/dist/deep-observe.d.ts.map +1 -1
  5. package/dist/deep-observe.js +56 -11
  6. package/dist/describe-resources.d.ts +20 -6
  7. package/dist/describe-resources.d.ts.map +1 -1
  8. package/dist/describe-resources.js +40 -26
  9. package/dist/health.d.ts.map +1 -1
  10. package/dist/health.js +6 -9
  11. package/dist/integrity.json +2 -2
  12. package/dist/manifest.json +1 -1
  13. package/dist/op/activities/gcp-apply.d.ts +14 -1
  14. package/dist/op/activities/gcp-apply.d.ts.map +1 -1
  15. package/dist/op/activities/gcp-apply.js +49 -8
  16. package/dist/op/activities/gcp-compute.d.ts +10 -0
  17. package/dist/op/activities/gcp-compute.d.ts.map +1 -1
  18. package/dist/op/activities/gcp-compute.js +39 -3
  19. package/dist/op/activities/gcp-iam.d.ts.map +1 -1
  20. package/dist/op/activities/gcp-iam.js +2 -0
  21. package/dist/serializer.d.ts +18 -0
  22. package/dist/serializer.d.ts.map +1 -1
  23. package/dist/serializer.js +72 -16
  24. package/package.json +2 -2
  25. package/src/api/read-client.ts +2 -1
  26. package/src/deep-observe.test.ts +27 -1
  27. package/src/deep-observe.ts +52 -12
  28. package/src/describe-resources.test.ts +6 -5
  29. package/src/describe-resources.ts +43 -24
  30. package/src/health.ts +7 -10
  31. package/src/op/activities/gcp-apply.test.ts +48 -3
  32. package/src/op/activities/gcp-apply.ts +49 -9
  33. package/src/op/activities/gcp-compute.test.ts +94 -0
  34. package/src/op/activities/gcp-compute.ts +37 -2
  35. package/src/op/activities/gcp-iam.ts +2 -0
  36. package/src/project-config.test.ts +119 -0
  37. package/src/read-refs.test.ts +135 -0
  38. package/src/serializer.test.ts +26 -0
  39. package/src/serializer.ts +73 -15
package/src/serializer.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  import { createRequire } from "module";
9
9
  import { moduleSourceUrl } from "@intentius/chant/module-source";
10
10
  import type { Declarable } from "@intentius/chant/declarable";
11
- import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
11
+ import { isDeclarable, isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
12
+ import { AttrRef } from "@intentius/chant/attrref";
12
13
  import type { Serializer, SerializerResult, SerializeContext } from "@intentius/chant/serializer";
13
14
  import { ownershipEntries, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
14
15
  import type { LexiconOutput } from "@intentius/chant/lexicon-output";
@@ -84,15 +85,14 @@ function deriveGVKFromType(entityType: string): { apiVersion: string; kind: stri
84
85
  * GCP visitor for the generic serializer walker.
85
86
  */
86
87
  function gcpVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
88
+ // Config Connector resolves a `*Ref: { name }` by the target's metadata.name,
89
+ // which is its declared name or else the kebab-cased logical id (#4242).
90
+ const cnrmNames = new Map<string, string>();
91
+ for (const [entity, logical] of entityNames) cnrmNames.set(logical, cnrmName(logical, entity));
92
+ const resolve = (logical: string) => cnrmNames.get(logical) ?? logical;
87
93
  return {
88
- attrRef: (name, _attr) => {
89
- // For Config Connector, references resolve to metadata.name
90
- return name;
91
- },
92
- resourceRef: (name) => {
93
- // Config Connector uses resourceRef pattern: { name: "resource-name" }
94
- return { name };
95
- },
94
+ attrRef: (name, _attr) => resolve(name),
95
+ resourceRef: (name) => ({ name: resolve(name) }),
96
96
  propertyDeclarable: (entity, walk) => {
97
97
  if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
98
98
  return undefined;
@@ -109,6 +109,51 @@ function gcpVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
109
109
  };
110
110
  }
111
111
 
112
+ /**
113
+ * The metadata.name a resource is written with: a string `metadata.name` in its
114
+ * props, else its logical id in kebab case.
115
+ */
116
+ export function cnrmName(logical: string, entity: Declarable): string {
117
+ return cnrmNameOf(logical, isResourceDeclarable(entity) ? entity.props : undefined);
118
+ }
119
+
120
+ /** {@link cnrmName} from a resource's props, for callers holding props rather than the declarable. */
121
+ export function cnrmNameOf(logical: string, props: unknown): string {
122
+ const declared = (props as { metadata?: { name?: unknown } } | undefined)?.metadata?.name;
123
+ return typeof declared === "string" && declared.length > 0 ? declared : kebabName(logical);
124
+ }
125
+
126
+ /**
127
+ * Declared props with each reference to another resource replaced as the
128
+ * serializer writes it (#4251): an attribute reference (`neg.name`) becomes the
129
+ * target's metadata.name, and a resource placed whole becomes `{ name }`.
130
+ * `logicalOf` maps a target's props object to its logical id, for a reference
131
+ * that does not carry one. Other values, pseudo-parameters included, are kept.
132
+ */
133
+ export function resolveCnrmRefs(value: unknown, logicalOf: (props: unknown) => string | undefined): unknown {
134
+ if (value instanceof AttrRef) {
135
+ const parent = value.parent.deref() as Declarable | undefined;
136
+ const props = parent && isResourceDeclarable(parent) ? parent.props : undefined;
137
+ const logical = value.getLogicalName() ?? logicalOf(props);
138
+ return logical === undefined ? value : cnrmNameOf(logical, props);
139
+ }
140
+ if (Array.isArray(value)) return value.map((v) => resolveCnrmRefs(v, logicalOf));
141
+ if (value === null || typeof value !== "object") return value;
142
+ if (isDeclarable(value)) {
143
+ const props = isResourceDeclarable(value) ? value.props : undefined;
144
+ const logical = logicalOf(props);
145
+ return logical === undefined ? value : { name: cnrmNameOf(logical, props) };
146
+ }
147
+ if (INTRINSIC_MARKER in (value as object)) return value;
148
+ const out: Record<string, unknown> = {};
149
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) out[k] = resolveCnrmRefs(v, logicalOf);
150
+ return out;
151
+ }
152
+
153
+ function kebabName(logical: string): string {
154
+ return logical.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
155
+ }
156
+
112
157
  /**
113
158
  * Convert a value to YAML-compatible form using the walker.
114
159
  */
@@ -150,7 +195,11 @@ export const gcpSerializer: Serializer = {
150
195
  let defaultLabelEntries: Record<string, unknown> = context?.ownership
151
196
  ? { ...ownershipEntries(LABEL_OWNERSHIP_KEYS, context.ownership) }
152
197
  : {};
153
- let defaultAnnotationEntries: Record<string, unknown> = {};
198
+ // #4252: a member's project, once, in chant.config's `gcp.project`. It
199
+ // resolves GCP.ProjectId and is written as each resource's project-id
200
+ // annotation, which apply and the readers take ahead of the env vars.
201
+ const configProject = gcpConfigProject(context?.config);
202
+ let defaultAnnotationEntries: Record<string, unknown> = configProject ? { [PROJECT_ID_ANNOTATION]: configProject } : {};
154
203
 
155
204
  for (const [, entity] of entities) {
156
205
  if (isDefaultLabels(entity)) {
@@ -176,6 +225,7 @@ export const gcpSerializer: Serializer = {
176
225
  toYAMLValue(isResourceDeclarable(entity) ? entity.props : undefined, entityNames),
177
226
  name,
178
227
  unresolved,
228
+ configProject,
179
229
  ) as Record<string, unknown> | undefined;
180
230
 
181
231
  if (!props) continue;
@@ -189,7 +239,7 @@ export const gcpSerializer: Serializer = {
189
239
  // Build metadata
190
240
  const metadata: Record<string, unknown> = props.metadata as Record<string, unknown> ?? {};
191
241
  if (!metadata.name) {
192
- metadata.name = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
242
+ metadata.name = kebabName(name);
193
243
  }
194
244
 
195
245
  // Merge default labels
@@ -233,6 +283,14 @@ export const gcpSerializer: Serializer = {
233
283
  },
234
284
  };
235
285
 
286
+ const PROJECT_ID_ANNOTATION = "cnrm.cloud.google.com/project-id";
287
+
288
+ /** The `gcp.project` a chant.config declares, if any (#4252). */
289
+ export function gcpConfigProject(config: Record<string, unknown> | undefined): string | undefined {
290
+ const project = (config?.gcp as { project?: unknown } | undefined)?.project;
291
+ return typeof project === "string" && project.length > 0 ? project : undefined;
292
+ }
293
+
236
294
  /**
237
295
  * Pseudo-parameter → environment variable mapping.
238
296
  * Config Connector annotations must be plain strings, so PseudoParameters
@@ -257,20 +315,20 @@ interface UnresolvedPseudo {
257
315
  * the API as an object. A variable that isn't set is recorded, and the
258
316
  * build refuses with every one named.
259
317
  */
260
- function resolvePseudoParameters(value: unknown, entity: string, unresolved: UnresolvedPseudo[]): unknown {
261
- if (Array.isArray(value)) return value.map((v) => resolvePseudoParameters(v, entity, unresolved));
318
+ function resolvePseudoParameters(value: unknown, entity: string, unresolved: UnresolvedPseudo[], project?: string): unknown {
319
+ if (Array.isArray(value)) return value.map((v) => resolvePseudoParameters(v, entity, unresolved, project));
262
320
  if (value === null || typeof value !== "object") return value;
263
321
  const obj = value as Record<string, unknown>;
264
322
  const keys = Object.keys(obj);
265
323
  if (keys.length === 1 && keys[0] === "Ref" && typeof obj.Ref === "string" && obj.Ref in PSEUDO_ENV_MAP) {
266
324
  const { envVar } = PSEUDO_ENV_MAP[obj.Ref];
267
- const resolved = process.env[envVar];
325
+ const resolved = (obj.Ref === "GCP::ProjectId" ? project : undefined) ?? process.env[envVar];
268
326
  if (resolved) return resolved;
269
327
  unresolved.push({ entity, ref: obj.Ref, envVar });
270
328
  return value;
271
329
  }
272
330
  const out: Record<string, unknown> = {};
273
- for (const [k, v] of Object.entries(obj)) out[k] = resolvePseudoParameters(v, entity, unresolved);
331
+ for (const [k, v] of Object.entries(obj)) out[k] = resolvePseudoParameters(v, entity, unresolved, project);
274
332
  return out;
275
333
  }
276
334