@intentius/chant-lexicon-gcp 0.124.30 → 0.124.32
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/actions/index.js +45 -0
- package/dist/api/read-client.js +105 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +115 -0
- package/dist/codegen/generate-cli.js +17 -0
- package/dist/codegen/generate.js +214 -0
- package/dist/codegen/naming.js +164 -0
- package/dist/codegen/package.js +43 -0
- package/dist/composites/catalog.js +1245 -0
- package/dist/composites/cloud-function.js +65 -0
- package/dist/composites/cloud-run-service.js +71 -0
- package/dist/composites/cloud-sql-instance.js +85 -0
- package/dist/composites/gcs-bucket.js +64 -0
- package/dist/composites/gke-cluster.js +113 -0
- package/dist/composites/gke-crdb-region.js +137 -0
- package/dist/composites/gpu-node-pool.js +74 -0
- package/dist/composites/index.js +15 -0
- package/dist/composites/landing-zone.js +89 -0
- package/dist/composites/managed-certificate.js +46 -0
- package/dist/composites/memorystore-redis.js +52 -0
- package/dist/composites/multi-region-vpc.js +108 -0
- package/dist/composites/private-service.js +50 -0
- package/dist/composites/pubsub-pipeline.js +63 -0
- package/dist/composites/secure-project.js +85 -0
- package/dist/composites/vpc-network.js +113 -0
- package/dist/coverage.js +28 -0
- package/dist/deep-observe-hooks.js +152 -0
- package/dist/deep-observe.js +273 -0
- package/dist/default-labels.js +67 -0
- package/dist/describe-resources.js +264 -0
- package/dist/detect.js +16 -0
- package/dist/export-resources.js +59 -0
- package/dist/generated/index.js +1804 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/governance.js +98 -0
- package/dist/import/generator.js +68 -0
- package/dist/import/live-export.js +73 -0
- package/dist/import/parser.js +61 -0
- package/dist/index.js +36 -0
- package/dist/integrity.json +3 -3
- package/dist/lint/audit-catalog.js +38 -0
- package/dist/lint/audit-lineage.js +63 -0
- package/dist/lint/post-synth/gcp-helpers.js +91 -0
- package/dist/lint/post-synth/index.js +54 -0
- package/dist/lint/post-synth/schema-registry.d.ts.map +1 -1
- package/dist/lint/post-synth/schema-registry.js +69 -0
- package/dist/lint/post-synth/wgc101.js +47 -0
- package/dist/lint/post-synth/wgc102.js +29 -0
- package/dist/lint/post-synth/wgc103.js +37 -0
- package/dist/lint/post-synth/wgc104.js +34 -0
- package/dist/lint/post-synth/wgc105.js +39 -0
- package/dist/lint/post-synth/wgc106.js +30 -0
- package/dist/lint/post-synth/wgc107.js +33 -0
- package/dist/lint/post-synth/wgc108.js +34 -0
- package/dist/lint/post-synth/wgc109.js +33 -0
- package/dist/lint/post-synth/wgc110.js +32 -0
- package/dist/lint/post-synth/wgc111.js +47 -0
- package/dist/lint/post-synth/wgc112.js +46 -0
- package/dist/lint/post-synth/wgc113.js +35 -0
- package/dist/lint/post-synth/wgc201.js +30 -0
- package/dist/lint/post-synth/wgc202.js +33 -0
- package/dist/lint/post-synth/wgc203.js +35 -0
- package/dist/lint/post-synth/wgc204.js +33 -0
- package/dist/lint/post-synth/wgc301.js +28 -0
- package/dist/lint/post-synth/wgc302.js +28 -0
- package/dist/lint/post-synth/wgc303.js +29 -0
- package/dist/lint/post-synth/wgc401.js +50 -0
- package/dist/lint/post-synth/wgc402.js +46 -0
- package/dist/lint/post-synth/wgc403.js +74 -0
- package/dist/lint/post-synth/wgc501.js +63 -0
- package/dist/lint/post-synth/wgc502.js +37 -0
- package/dist/lint/post-synth/wgc503.js +49 -0
- package/dist/lint/rules/hardcoded-project.js +51 -0
- package/dist/lint/rules/hardcoded-region.js +51 -0
- package/dist/lint/rules/index.js +3 -0
- package/dist/lint/rules/public-iam.js +37 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +15 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +46 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/floci-gcp.js +49 -0
- package/dist/op/activities/gcp-apply.js +618 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +50 -0
- package/dist/ownership.js +38 -0
- package/dist/package-cli.js +20 -0
- package/dist/plugin.js +371 -0
- package/dist/pseudo.js +15 -0
- package/dist/rules/schema-registry.ts +2 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +233 -0
- package/dist/spec/fetch.js +107 -0
- package/dist/spec/parse.js +323 -0
- package/dist/validate-cli.js +11 -0
- package/dist/validate.js +24 -0
- package/dist/variables.js +15 -0
- package/package.json +8 -8
- package/src/codegen/package.ts +2 -2
- package/src/lint/post-synth/schema-registry.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/serializer.ts +2 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
2
|
+
import { GCP_RESOURCE_OWNERSHIP_KEYS } from "./ownership.js";
|
|
3
|
+
import { resolveGVK } from "./serializer.js";
|
|
4
|
+
/**
|
|
5
|
+
* Fields GCP populates on essentially anything, wherever they appear.
|
|
6
|
+
*
|
|
7
|
+
* Pruned on BOTH sides and regardless of whether source declared them: these
|
|
8
|
+
* are server-assigned identity and bookkeeping, and a user who writes one is
|
|
9
|
+
* writing something the API will overwrite anyway.
|
|
10
|
+
*/
|
|
11
|
+
export const GCP_READ_ONLY_NAMES = new Set([
|
|
12
|
+
"etag",
|
|
13
|
+
"selfLink",
|
|
14
|
+
"id",
|
|
15
|
+
"projectId",
|
|
16
|
+
"projectNumber",
|
|
17
|
+
"timeCreated",
|
|
18
|
+
"updated",
|
|
19
|
+
"createTime",
|
|
20
|
+
"updateTime",
|
|
21
|
+
"generation",
|
|
22
|
+
"metageneration",
|
|
23
|
+
"uid",
|
|
24
|
+
"uniqueId",
|
|
25
|
+
"email",
|
|
26
|
+
"state",
|
|
27
|
+
"reconciling",
|
|
28
|
+
"observedGeneration",
|
|
29
|
+
// Cloud Run's server-computed service surface (v2 GetService).
|
|
30
|
+
"uri",
|
|
31
|
+
"urls",
|
|
32
|
+
"conditions",
|
|
33
|
+
"terminalCondition",
|
|
34
|
+
"latestReadyRevision",
|
|
35
|
+
"latestCreatedRevision",
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Per-kind values GCP fills in when the request omits them, keyed by the
|
|
39
|
+
* index-erased pattern from the tree root (the declared-props shape
|
|
40
|
+
* `./deep-observe.ts` reshapes the live payload into — spec fields at the
|
|
41
|
+
* root, so `storageClass`, not `spec.storageClass`).
|
|
42
|
+
*
|
|
43
|
+
* Unlike {@link GCP_READ_ONLY_NAMES} these are only noise where **source never
|
|
44
|
+
* declared the property** — a default somebody explicitly wrote is a fact worth
|
|
45
|
+
* diffing, and pruning it would hide a real change away from it. The
|
|
46
|
+
* `counterpart === "absent"` gate below is what enforces that, and it is the
|
|
47
|
+
* same rule the AWS and Azure hooks apply.
|
|
48
|
+
*/
|
|
49
|
+
export const GCP_SERVICE_DEFAULTS = {
|
|
50
|
+
StorageBucket: {
|
|
51
|
+
storageClass: "STANDARD",
|
|
52
|
+
locationType: "multi-region",
|
|
53
|
+
publicAccessPrevention: "inherited",
|
|
54
|
+
uniformBucketLevelAccess: false,
|
|
55
|
+
"softDeletePolicy.retentionDurationSeconds": "604800",
|
|
56
|
+
},
|
|
57
|
+
PubSubTopic: {
|
|
58
|
+
"messageStoragePolicy.enforceInTransit": false,
|
|
59
|
+
},
|
|
60
|
+
PubSubSubscription: {
|
|
61
|
+
ackDeadlineSeconds: 10,
|
|
62
|
+
"expirationPolicy.ttl": "2678400s",
|
|
63
|
+
enableMessageOrdering: false,
|
|
64
|
+
enableExactlyOnceDelivery: false,
|
|
65
|
+
retainAckedMessages: false,
|
|
66
|
+
detached: false,
|
|
67
|
+
"retryPolicy.minimumBackoff": "10s",
|
|
68
|
+
},
|
|
69
|
+
RunService: {
|
|
70
|
+
ingress: "INGRESS_TRAFFIC_ALL",
|
|
71
|
+
launchStage: "GA",
|
|
72
|
+
},
|
|
73
|
+
IAMServiceAccount: {
|
|
74
|
+
disabled: false,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* chant's own ownership marker, in both vocabularies it is written in: the
|
|
79
|
+
* serializer stamps k8s-style label keys on the CNRM object
|
|
80
|
+
* (`app.kubernetes.io/managed-by`), while `gcpApply` stamps the GCP-legal
|
|
81
|
+
* equivalents on the live resource (`managed-by` — GCP label keys allow no `/`
|
|
82
|
+
* or `.`). See `./ownership.ts` for why the two surfaces differ. A declared
|
|
83
|
+
* tree carries the first family, a live payload can carry either (the applier
|
|
84
|
+
* forwards the manifest's labels where a kind accepts labels at all), and
|
|
85
|
+
* neither is drift: it is chant showing its own signature to itself — the same
|
|
86
|
+
* correction #1301 made for AWS.
|
|
87
|
+
*/
|
|
88
|
+
const CHANT_OWNERSHIP_LABEL_PATTERNS = new Set([LABEL_OWNERSHIP_KEYS, GCP_RESOURCE_OWNERSHIP_KEYS].flatMap((keys) => [
|
|
89
|
+
`metadata.labels.${keys.managedBy}`,
|
|
90
|
+
`metadata.labels.${keys.stack}`,
|
|
91
|
+
`metadata.labels.${keys.env}`,
|
|
92
|
+
]));
|
|
93
|
+
/** Declared-side CNRM machinery a REST payload never echoes (see module doc). */
|
|
94
|
+
const CNRM_ENVELOPE_PATTERNS = new Set(["projectRef", "resourceID"]);
|
|
95
|
+
const CNRM_ANNOTATION_PREFIX = "metadata.annotations.cnrm.cloud.google.com/";
|
|
96
|
+
/** Last dotted segment of a path pattern — `spec.foo.etag` -> `etag`. */
|
|
97
|
+
function lastSegment(pattern) {
|
|
98
|
+
const i = pattern.lastIndexOf(".");
|
|
99
|
+
return i === -1 ? pattern : pattern.slice(i + 1);
|
|
100
|
+
}
|
|
101
|
+
/** A `{}` with nothing pruned out of it, as GCP returns for an unset message. */
|
|
102
|
+
function isEmptyObject(value) {
|
|
103
|
+
return (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* GCP's static deep-observation noise rules, applied by core to the declared
|
|
107
|
+
* tree and the live tree alike so the two are compared in the same shape.
|
|
108
|
+
*/
|
|
109
|
+
export const gcpDeepNormalizationHooks = {
|
|
110
|
+
prune(node) {
|
|
111
|
+
// Server-assigned, wherever it appears, on either side.
|
|
112
|
+
if (GCP_READ_ONLY_NAMES.has(lastSegment(node.pattern)))
|
|
113
|
+
return true;
|
|
114
|
+
// chant's own ownership marker is not drift (see the set's doc).
|
|
115
|
+
if (CHANT_OWNERSHIP_LABEL_PATTERNS.has(node.pattern))
|
|
116
|
+
return true;
|
|
117
|
+
// CNRM machinery on the declared side: instructions to the controller, not
|
|
118
|
+
// resource state, and no REST payload ever carries them.
|
|
119
|
+
if (CNRM_ENVELOPE_PATTERNS.has(node.pattern))
|
|
120
|
+
return true;
|
|
121
|
+
if (node.pattern.startsWith(CNRM_ANNOTATION_PREFIX))
|
|
122
|
+
return true;
|
|
123
|
+
// Everything below subtracts provider-populated values from the live tree,
|
|
124
|
+
// and only where source never declared the property. `counterpart` is a
|
|
125
|
+
// tri-state; only `absent` licenses it.
|
|
126
|
+
if (node.side !== "live" || node.counterpart !== "absent")
|
|
127
|
+
return false;
|
|
128
|
+
// An undeclared `null` or `{}` is GCP's spelling of "unset" — a field the
|
|
129
|
+
// API includes in the payload without there being any configuration in it
|
|
130
|
+
// (a service account's `description: null`, Cloud Run's
|
|
131
|
+
// `binaryAuthorization: {}`).
|
|
132
|
+
if (node.value === null || isEmptyObject(node.value))
|
|
133
|
+
return true;
|
|
134
|
+
const kindDefaults = GCP_SERVICE_DEFAULTS[shortKind(node.entityType)];
|
|
135
|
+
if (!kindDefaults)
|
|
136
|
+
return false;
|
|
137
|
+
return Object.prototype.hasOwnProperty.call(kindDefaults, node.pattern) && kindDefaults[node.pattern] === node.value;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* `GCP::Pubsub::Topic` -> `PubSubTopic`, the key `GCP_SERVICE_DEFAULTS` uses —
|
|
142
|
+
* the CNRM kind, resolved through the serializer's GVK map because the naive
|
|
143
|
+
* `${service}${shortKind}` concatenation miscases 4 of the 6 appliable kinds
|
|
144
|
+
* (`PubsubTopic`, `IamServiceAccount`, …) and silently matches no defaults.
|
|
145
|
+
*/
|
|
146
|
+
function shortKind(entityType) {
|
|
147
|
+
const resolved = resolveGVK(entityType);
|
|
148
|
+
if (resolved)
|
|
149
|
+
return resolved.kind;
|
|
150
|
+
const parts = entityType.split("::");
|
|
151
|
+
return parts.length === 3 ? `${parts[1]}${parts[2]}` : entityType;
|
|
152
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GCP deep observation (#1087, re-landed on direct REST by #1209/#1210) — the
|
|
3
|
+
* GCP row of the deep-observe contract (#1014).
|
|
4
|
+
*
|
|
5
|
+
* ## The reader is the applier's transport, pointed at reads
|
|
6
|
+
*
|
|
7
|
+
* chant applies GCP cluster-free over direct REST (#706), and since #1209 it
|
|
8
|
+
* observes the same way: every GET here is built by the same
|
|
9
|
+
* `ResourceMapper.plan().getUrl` the applier writes through (see
|
|
10
|
+
* ./api/read-client.ts), so reader and applier cannot disagree about where a
|
|
11
|
+
* resource lives. `GCP_ENDPOINT_URL` redirects the whole reader at a local
|
|
12
|
+
* floci-gcp; unset means real GCP, exactly as it does for the applier.
|
|
13
|
+
* Coverage is the applier's own dispatch table — a kind with no mapper cannot
|
|
14
|
+
* be applied either, and reports NOT-OBSERVED with `unsupported-kind` (#1089)
|
|
15
|
+
* rather than being dropped.
|
|
16
|
+
*
|
|
17
|
+
* ## Two shapes, one diff
|
|
18
|
+
*
|
|
19
|
+
* The declared side is a CNRM spec as a declarable authors it (`{ metadata:
|
|
20
|
+
* { name }, location, storageClass }`); the REST APIs return their own shapes
|
|
21
|
+
* (`name` is usually the full resource path, a bucket's `iamConfiguration`
|
|
22
|
+
* nests what CNRM flattens, a subscription's `topic` is a path where CNRM
|
|
23
|
+
* writes `topicRef`). Diffed raw, every such field drifts twice: once as
|
|
24
|
+
* `declared -> <absent>` and once as `<undeclared> -> live`.
|
|
25
|
+
* {@link restToCnrmShape} plus the per-kind {@link CNRM_REFINERS} put the live
|
|
26
|
+
* payload into the declared vocabulary before the diff runs — the same move
|
|
27
|
+
* the AWS row makes with its per-type `toModel` (#1207/#1269), and it lives
|
|
28
|
+
* next to the read for the same reason: translation is the price of the
|
|
29
|
+
* richer read, and it does not belong inside the diff.
|
|
30
|
+
*
|
|
31
|
+
* ## Noise is a static table, not an ownership walk
|
|
32
|
+
*
|
|
33
|
+
* The kubectl-era reader pruned by CNRM `metadata.managedFields`. A REST
|
|
34
|
+
* payload never says who wrote a field, so the noise rules are static
|
|
35
|
+
* (./deep-observe-hooks.ts): server-assigned names, chant's own ownership
|
|
36
|
+
* labels, CNRM-only declared fields, and per-kind provider defaults gated on
|
|
37
|
+
* "source never declared it". Core applies the same hooks to both trees.
|
|
38
|
+
*
|
|
39
|
+
* Since #2160 part of that table has a substrate-independent answer behind it:
|
|
40
|
+
* core derives a claimed-field set from the declaration's own props, and a
|
|
41
|
+
* live value on a path outside it is reported as unclaimed rather than as
|
|
42
|
+
* drift. The table is not obsolete — `prune` is still the right tool for a
|
|
43
|
+
* field GCP populates on every object of a kind, and for putting the two trees
|
|
44
|
+
* in the same vocabulary — but the entries that exist only to say "nobody
|
|
45
|
+
* declared this" now have a general mechanism underneath them.
|
|
46
|
+
*
|
|
47
|
+
* ## The build-path boundary
|
|
48
|
+
*
|
|
49
|
+
* `gcpPlugin.ts` reaches this file only via `await import("./deep-observe.js")`
|
|
50
|
+
* inside `observeResourcesDeep` — never statically — so `chant build` never
|
|
51
|
+
* resolves the live transport just to synthesize a template.
|
|
52
|
+
* `deepNormalizationHooks` is plain data (./deep-observe-hooks.ts) and is
|
|
53
|
+
* imported statically from `plugin.ts`, because core normalizes the
|
|
54
|
+
* *declared* tree with it whether or not a live read ever happens.
|
|
55
|
+
*/
|
|
56
|
+
import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
|
|
57
|
+
import { hasOwnershipMarker } from "@intentius/chant/ownership";
|
|
58
|
+
import { deriveGVK, manifestProjectAnnotations, resolveReadProject } from "./describe-resources.js";
|
|
59
|
+
import { getResource, mapperForKind, GcpReadError, isNotFound } from "./api/read-client.js";
|
|
60
|
+
import { gcpDeepNormalizationHooks } from "./deep-observe-hooks.js";
|
|
61
|
+
/** The labels the applier stamps — see describe-resources.ts. */
|
|
62
|
+
const GCP_OWNERSHIP_LABEL_KEYS = {
|
|
63
|
+
managedBy: "managed-by",
|
|
64
|
+
stack: "chant-stack",
|
|
65
|
+
env: "chant-env",
|
|
66
|
+
};
|
|
67
|
+
// Re-exported so a dynamic importer of this module (plugin.ts's
|
|
68
|
+
// `observeResourcesDeep`, a test) can get the reader and its hooks from one
|
|
69
|
+
// place. `plugin.ts`'s own `deepNormalizationHooks` field imports the hooks
|
|
70
|
+
// separately, directly from `./deep-observe-hooks` — see the module doc.
|
|
71
|
+
export { gcpDeepNormalizationHooks };
|
|
72
|
+
/**
|
|
73
|
+
* Reshape a GCP REST body into the shape the declared props are written in:
|
|
74
|
+
* identity and labels under `metadata`, everything else at the root — a CNRM
|
|
75
|
+
* spec as a chant declarable authors it (`new StorageBucket({ metadata,
|
|
76
|
+
* location, storageClass })`). Kind-specific vocabulary differences on top of
|
|
77
|
+
* it are {@link CNRM_REFINERS}' job. Verified against floci-gcp: diffed raw, a
|
|
78
|
+
* bucket that matched its declaration exactly reported every field as drift
|
|
79
|
+
* twice, once per shape.
|
|
80
|
+
*/
|
|
81
|
+
export function restToCnrmShape(body) {
|
|
82
|
+
const metadata = {};
|
|
83
|
+
const rest = {};
|
|
84
|
+
for (const [key, value] of Object.entries(body)) {
|
|
85
|
+
if (key === "name" || key === "labels" || key === "annotations")
|
|
86
|
+
metadata[key] = value;
|
|
87
|
+
else
|
|
88
|
+
rest[key] = value;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...(Object.keys(metadata).length ? { metadata } : {}),
|
|
92
|
+
...rest,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** The live payload minus the fields that live outside `properties` on
|
|
96
|
+
* {@link DeepResourceObservation}. A REST body has no `apiVersion`, but `kind`
|
|
97
|
+
* shows up on some (GCS returns `storage#bucket`), so both are dropped. */
|
|
98
|
+
function propertiesTreeOf(obj) {
|
|
99
|
+
const { apiVersion: _apiVersion, kind: _kind, ...rest } = obj;
|
|
100
|
+
return rest;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Per-kind REST -> CNRM vocabulary mapping, applied after the generic
|
|
104
|
+
* {@link restToCnrmShape}. Each entry translates exactly the fields where the
|
|
105
|
+
* REST API and the CNRM schema disagree about names or nesting — kept next to
|
|
106
|
+
* the kind, like the applier's forward mapping in gcp-apply.ts, so adding a
|
|
107
|
+
* field means touching one kind in one file per direction.
|
|
108
|
+
*/
|
|
109
|
+
const CNRM_REFINERS = {
|
|
110
|
+
StorageBucket(tree) {
|
|
111
|
+
// CNRM flattens GCS's `iamConfiguration` envelope.
|
|
112
|
+
const iam = tree.iamConfiguration;
|
|
113
|
+
if (iam) {
|
|
114
|
+
if (iam.uniformBucketLevelAccess?.enabled !== undefined) {
|
|
115
|
+
tree.uniformBucketLevelAccess = iam.uniformBucketLevelAccess.enabled;
|
|
116
|
+
}
|
|
117
|
+
if (iam.publicAccessPrevention !== undefined)
|
|
118
|
+
tree.publicAccessPrevention = iam.publicAccessPrevention;
|
|
119
|
+
delete tree.iamConfiguration;
|
|
120
|
+
}
|
|
121
|
+
// `lifecycle.rule` -> `lifecycleRule`, the inverse of bucketInsertBody.
|
|
122
|
+
const lifecycle = tree.lifecycle;
|
|
123
|
+
if (lifecycle) {
|
|
124
|
+
if (lifecycle.rule !== undefined)
|
|
125
|
+
tree.lifecycleRule = lifecycle.rule;
|
|
126
|
+
delete tree.lifecycle;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
PubSubSubscription(tree, ctx) {
|
|
130
|
+
const topic = tree.topic;
|
|
131
|
+
if (typeof topic !== "string")
|
|
132
|
+
return;
|
|
133
|
+
// The declared side writes a `topicRef`; a same-project path maps to the
|
|
134
|
+
// `name` form the manifest overwhelmingly uses, anything else to `external`.
|
|
135
|
+
const prefix = `projects/${ctx.project}/topics/`;
|
|
136
|
+
tree.topicRef = topic.startsWith(prefix) ? { name: topic.slice(prefix.length) } : { external: topic };
|
|
137
|
+
delete tree.topic;
|
|
138
|
+
},
|
|
139
|
+
SecretManagerSecret(tree) {
|
|
140
|
+
// CNRM spells automatic replication as a boolean; the REST API as `{}`.
|
|
141
|
+
const replication = tree.replication;
|
|
142
|
+
if (replication && typeof replication.automatic === "object" && replication.automatic !== null) {
|
|
143
|
+
replication.automatic = true;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
IAMServiceAccount(tree, ctx) {
|
|
147
|
+
// The REST identity is the derived email; the declared name is the account
|
|
148
|
+
// id it was derived from (see gcpServiceAccountMapper).
|
|
149
|
+
const name = tree.metadata?.name;
|
|
150
|
+
if (typeof name === "string" && name.endsWith(`@${ctx.project}.iam.gserviceaccount.com`)) {
|
|
151
|
+
tree.metadata.name = name.slice(0, name.indexOf("@"));
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
RunService(tree, ctx) {
|
|
155
|
+
// The v2 payload carries no `location` field — it lives in the resource
|
|
156
|
+
// name. The declared side writes it as `location`.
|
|
157
|
+
const m = ctx.restName ? /\/locations\/([^/]+)\//.exec(ctx.restName) : null;
|
|
158
|
+
if (m)
|
|
159
|
+
tree.location = m[1];
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* A REST payload as a CNRM-shaped property tree, in the declared side's
|
|
164
|
+
* vocabulary. The generic reshape runs first, then the full resource path is
|
|
165
|
+
* shortened to the declared `metadata.name` (`projects/p/topics/t` -> `t`),
|
|
166
|
+
* then the kind's own refiner translates what the two schemas name
|
|
167
|
+
* differently.
|
|
168
|
+
*/
|
|
169
|
+
export function toCnrmTree(kind, body, ctx) {
|
|
170
|
+
const tree = restToCnrmShape(propertiesTreeOf(body));
|
|
171
|
+
const restName = typeof body.name === "string" ? body.name : undefined;
|
|
172
|
+
const name = tree.metadata?.name;
|
|
173
|
+
if (typeof name === "string" && name.includes("/")) {
|
|
174
|
+
tree.metadata.name = name.slice(name.lastIndexOf("/") + 1);
|
|
175
|
+
}
|
|
176
|
+
CNRM_REFINERS[kind]?.(tree, { ...ctx, restName });
|
|
177
|
+
return tree;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Read the live property tree for each declared entity over the applier's
|
|
181
|
+
* REST transport, normalized into the declared CNRM shape (see the module
|
|
182
|
+
* doc). A connect failure becomes NOT-OBSERVED for every declared entity
|
|
183
|
+
* rather than an empty result, and a 404 is a real absence the thin read
|
|
184
|
+
* already reports.
|
|
185
|
+
*/
|
|
186
|
+
export async function observeResourcesDeepGcp(options) {
|
|
187
|
+
const resources = {};
|
|
188
|
+
const unobserved = {};
|
|
189
|
+
const endpoint = process.env.GCP_ENDPOINT_URL;
|
|
190
|
+
const manifestProjects = manifestProjectAnnotations(options.buildOutput);
|
|
191
|
+
const reads = [...options.entities].map(async ([entityName, { entityType, props }]) => {
|
|
192
|
+
const gvk = deriveGVK(entityType);
|
|
193
|
+
if (!gvk || !mapperForKind(gvk.kind)) {
|
|
194
|
+
unobserved[entityName] = {
|
|
195
|
+
type: entityType,
|
|
196
|
+
reason: "unsupported-kind",
|
|
197
|
+
detail: gvk
|
|
198
|
+
? `no REST mapper for ${gvk.kind} — chant cannot apply this kind either`
|
|
199
|
+
: `cannot derive a GCP kind from ${entityType}`,
|
|
200
|
+
};
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const metadata = props.metadata;
|
|
204
|
+
const name = metadata?.name;
|
|
205
|
+
if (!name) {
|
|
206
|
+
unobserved[entityName] = {
|
|
207
|
+
type: entityType,
|
|
208
|
+
reason: "read-failed",
|
|
209
|
+
detail: "declared entity has no metadata.name to query by",
|
|
210
|
+
};
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let client;
|
|
214
|
+
try {
|
|
215
|
+
client = {
|
|
216
|
+
project: resolveReadProject(gvk.kind, name, metadata, manifestProjects),
|
|
217
|
+
...(endpoint ? { endpoint } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
unobserved[entityName] = {
|
|
222
|
+
type: entityType,
|
|
223
|
+
reason: "no-binding",
|
|
224
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
225
|
+
};
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const obj = await getResource(client, gvk.kind, name, props);
|
|
230
|
+
const labels = obj.labels;
|
|
231
|
+
// owned filter: withhold what does not carry chant's marker. Withheld is
|
|
232
|
+
// not absent (#1089). Where the payload has no labels at all there is
|
|
233
|
+
// nothing to filter on, so the resource passes through — the same
|
|
234
|
+
// detect-only degradation the thin path takes.
|
|
235
|
+
if (options.owned && labels != null && !hasOwnershipMarker(labels, GCP_OWNERSHIP_LABEL_KEYS)) {
|
|
236
|
+
unobserved[entityName] = {
|
|
237
|
+
type: entityType,
|
|
238
|
+
reason: "filtered",
|
|
239
|
+
detail: "live resource carries no chant ownership marker and --owned was requested",
|
|
240
|
+
};
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
resources[entityName] = {
|
|
244
|
+
type: entityType,
|
|
245
|
+
physicalId: obj.id ?? obj.selfLink ?? obj.name,
|
|
246
|
+
properties: normalizeDeepProperties(toCnrmTree(gvk.kind, obj, { project: client.project }), {
|
|
247
|
+
entityType,
|
|
248
|
+
side: "live",
|
|
249
|
+
// The static table is the whole prune here — there is no per-resource
|
|
250
|
+
// ownership pass, because a REST payload carries no field ownership
|
|
251
|
+
// to drive one (see ./deep-observe-hooks.ts). What the payload cannot
|
|
252
|
+
// say, the declaration does: core classifies against the claimed-field
|
|
253
|
+
// set above this reader (#2160).
|
|
254
|
+
hooks: gcpDeepNormalizationHooks,
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
// A 404 is a real absence, same as the thin read — recorded there, not
|
|
260
|
+
// restated here. Anything else proves nothing and is a hole (#1089).
|
|
261
|
+
if (isNotFound(err))
|
|
262
|
+
return;
|
|
263
|
+
const status = err instanceof GcpReadError ? err.status : undefined;
|
|
264
|
+
unobserved[entityName] = {
|
|
265
|
+
type: entityType,
|
|
266
|
+
reason: status === 401 || status === 403 ? "no-credentials" : "read-failed",
|
|
267
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
await Promise.all(reads);
|
|
272
|
+
return deepObservation(resources, unobserved);
|
|
273
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Labels & Annotations — declares project-wide labels/annotations
|
|
3
|
+
* for all GCP Config Connector resources.
|
|
4
|
+
*
|
|
5
|
+
* When a project exports a `defaultLabels(...)` or `defaultAnnotations(...)`
|
|
6
|
+
* declaration, the serializer automatically injects them into every resource's
|
|
7
|
+
* metadata at synthesis time. Explicit labels/annotations on individual
|
|
8
|
+
* resources take precedence.
|
|
9
|
+
*/
|
|
10
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
11
|
+
export const DEFAULT_LABELS_MARKER = Symbol.for("chant.gcp.defaultLabels");
|
|
12
|
+
export const DEFAULT_ANNOTATIONS_MARKER = Symbol.for("chant.gcp.defaultAnnotations");
|
|
13
|
+
export function isDefaultLabels(value) {
|
|
14
|
+
return (typeof value === "object" &&
|
|
15
|
+
value !== null &&
|
|
16
|
+
DEFAULT_LABELS_MARKER in value &&
|
|
17
|
+
value[DEFAULT_LABELS_MARKER] === true);
|
|
18
|
+
}
|
|
19
|
+
export function isDefaultAnnotations(value) {
|
|
20
|
+
return (typeof value === "object" &&
|
|
21
|
+
value !== null &&
|
|
22
|
+
DEFAULT_ANNOTATIONS_MARKER in value &&
|
|
23
|
+
value[DEFAULT_ANNOTATIONS_MARKER] === true);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Declare project-wide default labels for all GCP Config Connector resources.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { defaultLabels } from "@intentius/chant-lexicon-gcp";
|
|
31
|
+
*
|
|
32
|
+
* export const labels = defaultLabels({
|
|
33
|
+
* "app.kubernetes.io/managed-by": "chant",
|
|
34
|
+
* "env": "production",
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function defaultLabels(labels) {
|
|
39
|
+
return {
|
|
40
|
+
[DEFAULT_LABELS_MARKER]: true,
|
|
41
|
+
[DECLARABLE_MARKER]: true,
|
|
42
|
+
lexicon: "gcp",
|
|
43
|
+
entityType: "chant:gcp:defaultLabels",
|
|
44
|
+
labels,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Declare project-wide default annotations for all GCP Config Connector resources.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { defaultAnnotations, GCP } from "@intentius/chant-lexicon-gcp";
|
|
53
|
+
*
|
|
54
|
+
* export const annotations = defaultAnnotations({
|
|
55
|
+
* "cnrm.cloud.google.com/project-id": GCP.ProjectId,
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function defaultAnnotations(annotations) {
|
|
60
|
+
return {
|
|
61
|
+
[DEFAULT_ANNOTATIONS_MARKER]: true,
|
|
62
|
+
[DECLARABLE_MARKER]: true,
|
|
63
|
+
lexicon: "gcp",
|
|
64
|
+
entityType: "chant:gcp:defaultAnnotations",
|
|
65
|
+
annotations,
|
|
66
|
+
};
|
|
67
|
+
}
|