@intentius/chant-lexicon-gcp 0.37.2 → 0.38.0
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/api/read-client.d.ts +71 -0
- package/dist/api/read-client.d.ts.map +1 -0
- package/dist/deep-observe-hooks.d.ts +41 -30
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +24 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +65 -39
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/floci-gcp.d.ts +12 -1
- package/dist/op/activities/floci-gcp.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api/read-client.ts +129 -0
- package/src/deep-observe-hooks.ts +112 -60
- package/src/deep-observe.test.ts +124 -478
- package/src/deep-observe.ts +94 -74
- package/src/describe-resources.test.ts +119 -206
- package/src/describe-resources.ts +153 -111
- package/src/lifecycle-integration.test.ts +32 -18
- package/src/op/activities/floci-gcp.test.ts +1 -1
- package/src/op/activities/floci-gcp.ts +22 -5
- package/src/plugin.ts +4 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GCP read transport (#1209) — the applier's own REST client, pointed at the
|
|
3
|
+
* read side.
|
|
4
|
+
*
|
|
5
|
+
* GCP applies cluster-free over direct REST (#706) and, until this, observed
|
|
6
|
+
* through a Config Connector cluster: `kubectl get <cnrm-gvk> -o json` per
|
|
7
|
+
* entity, on both the thin and the deep path. That split is what #1085's
|
|
8
|
+
* principle forbids — a lexicon should be observed on the transport it is
|
|
9
|
+
* applied with, or the two can disagree about what a resource even is — and it
|
|
10
|
+
* is what made GCP the heaviest of the three O slots.
|
|
11
|
+
*
|
|
12
|
+
* ## The URL comes from the applier, not from here
|
|
13
|
+
*
|
|
14
|
+
* Every GET is built by the same `ResourceMapper.plan()` the applier uses, and
|
|
15
|
+
* this module reads `plan().getUrl` rather than composing its own URL. That is
|
|
16
|
+
* deliberate: a reader that builds its own paths is a second, silently
|
|
17
|
+
* divergent opinion about where a resource lives, and the first symptom is a
|
|
18
|
+
* read that reports absent for something the applier just wrote. Reusing the
|
|
19
|
+
* mapper makes the two structurally incapable of disagreeing.
|
|
20
|
+
*
|
|
21
|
+
* ## What that bounds
|
|
22
|
+
*
|
|
23
|
+
* Coverage is exactly the applier's dispatch table (`MAPPERS`) — StorageBucket,
|
|
24
|
+
* PubSubTopic, PubSubSubscription, SecretManagerSecret, IAMServiceAccount,
|
|
25
|
+
* RunService. The kubectl path could fetch any CNRM kind the cluster knew
|
|
26
|
+
* about, so this is narrower on paper. It is not narrower in practice for
|
|
27
|
+
* anything chant can act on: a kind with no mapper cannot be applied either, so
|
|
28
|
+
* observing it produced a live tree nothing could ever reconcile against.
|
|
29
|
+
*
|
|
30
|
+
* A kind outside the table reports NOT-OBSERVED with `unsupported-kind` rather
|
|
31
|
+
* than being dropped, so the estate says "chant did not look at this" instead of
|
|
32
|
+
* quietly implying it is not there. Widening the table is #1209's stated
|
|
33
|
+
* non-goal and belongs with the applier.
|
|
34
|
+
*/
|
|
35
|
+
import { type ResourceMapper } from "../op/activities/gcp-apply.js";
|
|
36
|
+
/** Where to read, and as whom. */
|
|
37
|
+
export interface GcpReadClientOptions {
|
|
38
|
+
/** Endpoint override for every kind (floci-gcp `http://localhost:4588`). Omit for real GCP. */
|
|
39
|
+
endpoint?: string;
|
|
40
|
+
/** GCP project the resources live in. */
|
|
41
|
+
project: string;
|
|
42
|
+
/** Injectable HTTP, so tests never touch the network. */
|
|
43
|
+
http?: GcpReadHttp;
|
|
44
|
+
}
|
|
45
|
+
/** Injectable HTTP client — mirrors the applier's `GcpHttp`. */
|
|
46
|
+
export type GcpReadHttp = (method: string, url: string) => Promise<{
|
|
47
|
+
status: number;
|
|
48
|
+
text: string;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* A read that failed for a reason the caller must classify — carries the HTTP
|
|
52
|
+
* status so a 404 (absent) is told apart from a 401/403 (no credentials) and a
|
|
53
|
+
* 5xx/network error (could not look). Mirrors `AzureReadError`.
|
|
54
|
+
*/
|
|
55
|
+
export declare class GcpReadError extends Error {
|
|
56
|
+
readonly status?: number | undefined;
|
|
57
|
+
constructor(message: string, status?: number | undefined);
|
|
58
|
+
}
|
|
59
|
+
/** A 404 — the resource is genuinely absent, which is an answer, not a failure. */
|
|
60
|
+
export declare function isNotFound(err: unknown): boolean;
|
|
61
|
+
/** The mapper for a CNRM kind, or undefined when chant cannot act on it. */
|
|
62
|
+
export declare function mapperForKind(kind: string): ResourceMapper | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* GET one resource on the applier's transport.
|
|
65
|
+
*
|
|
66
|
+
* `declared` is the entity's own declared spec, which is what `plan()` needs to
|
|
67
|
+
* build a URL — the same input the applier hands it. Returns the parsed body,
|
|
68
|
+
* or throws {@link GcpReadError} carrying the status.
|
|
69
|
+
*/
|
|
70
|
+
export declare function getResource(client: GcpReadClientOptions, kind: string, name: string, declared?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
71
|
+
//# sourceMappingURL=read-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5F,kCAAkC;AAClC,MAAM,WAAW,oBAAoB;IACnC,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAOrG;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,KAAK;IAGnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;gBADxB,OAAO,EAAE,MAAM,EACN,MAAM,CAAC,EAAE,MAAM,YAAA;CAK3B;AAED,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEhD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEtE;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAqClC"}
|
|
@@ -1,39 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GCP deep-observation noise rules (#1209) — for REST payloads, not Config
|
|
3
|
+
* Connector objects.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* the k8s lexicon's rules into `lexicons/k8s/src/deep-observe-hooks.ts`: this
|
|
6
|
-
* file must be safe to import from `plugin.ts` at module load time, because
|
|
7
|
-
* `LexiconPlugin.deepNormalizationHooks` is plain data core reads to
|
|
8
|
-
* normalize the *declared* tree — the half of the contract that runs whether
|
|
9
|
-
* or not a cluster is ever touched (`lifecycle diff` without `--live`,
|
|
10
|
-
* `chant build`, tests that only exercise normalization). `./deep-observe.ts`
|
|
11
|
-
* imports `./describe-resources.ts` for the live kubectl transport, which
|
|
12
|
-
* pulls in `node:child_process`; nothing this file exports may pull that in,
|
|
13
|
-
* directly or transitively, so `chant build` never resolves a process-
|
|
14
|
-
* spawning module just to normalize a declared tree.
|
|
5
|
+
* ## What this replaced, and why it had to
|
|
15
6
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
7
|
+
* Until #1209 the deep reader read Config Connector custom resources through
|
|
8
|
+
* kubectl, and suppressed noise two ways: this module's static rules (the k8s
|
|
9
|
+
* object envelope plus CNRM's own observed-state annotations) and, in
|
|
10
|
+
* `./deep-observe.ts`, a per-resource prune driven by `metadata.managedFields`
|
|
11
|
+
* — Kubernetes records which manager owns which field, so everything the CNRM
|
|
12
|
+
* controller owned was subtracted before the diff ran.
|
|
13
|
+
*
|
|
14
|
+
* Reading GCP's own REST APIs removes both. **A GCP payload never says who
|
|
15
|
+
* wrote a field**, so there is no managed-fields attribution to prune by, and
|
|
16
|
+
* it is not a Kubernetes object, so the k8s envelope and CNRM annotation rules
|
|
17
|
+
* match nothing in it. Audited every rule that was here: all of them were
|
|
18
|
+
* CNRM-shaped, none applies.
|
|
19
|
+
*
|
|
20
|
+
* So the noise story is re-answered rather than re-pointed, in the shape the
|
|
21
|
+
* other two cloud lexicons already use — a static table naming what the
|
|
22
|
+
* provider populates. It is hand-maintained, which is the honest cost: a
|
|
23
|
+
* server-set field nobody has listed reads as drift until it is. That is
|
|
24
|
+
* visible and fixable, where an over-broad rule silently hides real drift.
|
|
21
25
|
*/
|
|
22
|
-
import type { DeepNormalizationHooks } from "@intentius/chant/
|
|
26
|
+
import type { DeepNormalizationHooks } from "@intentius/chant/deep-observation";
|
|
23
27
|
/**
|
|
24
|
-
* GCP
|
|
25
|
-
* applied to the *declared* tree by core (`gcpPlugin.deepNormalizationHooks`)
|
|
26
|
-
* exactly like the other three rows, and layered under the per-resource
|
|
27
|
-
* managed-fields prune in `./deep-observe.ts` for the live tree.
|
|
28
|
+
* Fields GCP populates on essentially anything, wherever they appear.
|
|
28
29
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
*
|
|
30
|
+
* Pruned on BOTH sides and regardless of whether source declared them: these
|
|
31
|
+
* are server-assigned identity and bookkeeping, and a user who writes one is
|
|
32
|
+
* writing something the API will overwrite anyway.
|
|
33
|
+
*/
|
|
34
|
+
export declare const GCP_READ_ONLY_NAMES: ReadonlySet<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Per-kind values GCP fills in when the request omits them.
|
|
37
|
+
*
|
|
38
|
+
* Unlike {@link GCP_READ_ONLY_NAMES} these are only noise where **source never
|
|
39
|
+
* declared the property** — a default somebody explicitly wrote is a fact worth
|
|
40
|
+
* diffing, and pruning it would hide a real change away from it. The
|
|
41
|
+
* `counterpart === "absent"` gate below is what enforces that, and it is the
|
|
42
|
+
* same rule the AWS and Azure hooks apply.
|
|
43
|
+
*/
|
|
44
|
+
export declare const GCP_SERVICE_DEFAULTS: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
45
|
+
/**
|
|
46
|
+
* GCP's static deep-observation noise rules, applied by core to the declared
|
|
47
|
+
* tree and the live tree alike so the two are compared in the same shape.
|
|
37
48
|
*/
|
|
38
49
|
export declare const gcpDeepNormalizationHooks: DeepNormalizationHooks;
|
|
39
50
|
//# sourceMappingURL=deep-observe-hooks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deep-observe-hooks.d.ts","sourceRoot":"","sources":["../src/deep-observe-hooks.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"deep-observe-hooks.d.ts","sourceRoot":"","sources":["../src/deep-observe-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAY,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAE1F;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAelD,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CA4B5F,CAAC;AAeF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,sBAiBvC,CAAC"}
|
package/dist/deep-observe.d.ts
CHANGED
|
@@ -129,6 +129,30 @@ export interface GcpDeepObserveOptions {
|
|
|
129
129
|
stack?: string;
|
|
130
130
|
owned?: boolean;
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Reshape a GCP REST body into the CNRM shape the declared source is written in.
|
|
134
|
+
*
|
|
135
|
+
* This is the half of the port that is not transport (#1209). chant's GCP
|
|
136
|
+
* source declares Config Connector custom resources — `{ metadata: { name },
|
|
137
|
+
* spec: { location, storageClass } }` — while the REST APIs return their own
|
|
138
|
+
* flat shape, `{ name, location, storageClass }`. Diffing one against the other
|
|
139
|
+
* makes every field drift twice: once as `spec.location: US -> <absent>` and
|
|
140
|
+
* again as `location: <undeclared> -> US`.
|
|
141
|
+
*
|
|
142
|
+
* Verified against floci-gcp before this existed: a bucket that matched its
|
|
143
|
+
* declaration exactly reported **7 property drifts**, all of them shape.
|
|
144
|
+
*
|
|
145
|
+
* chant has met this before. #1207 records it for AWS: Cloud Control returns
|
|
146
|
+
* the CloudFormation resource model and lines up for free, while the EC2 API
|
|
147
|
+
* returns the EC2 shape and needs mapping onto the declared shape before the
|
|
148
|
+
* diff can compare. GCP is the EC2 case.
|
|
149
|
+
*
|
|
150
|
+
* The mapping is CNRM's own convention rather than a per-kind table: identity
|
|
151
|
+
* and labels live under `metadata`, everything else is `spec`. That holds for
|
|
152
|
+
* every kind the applier can write, and a per-kind table would be a second
|
|
153
|
+
* place to forget a field.
|
|
154
|
+
*/
|
|
155
|
+
export declare function restToCnrmShape(body: Record<string, unknown>): Record<string, unknown>;
|
|
132
156
|
/**
|
|
133
157
|
* Read the live property tree for each declared entity via `kubectl get
|
|
134
158
|
* <kind>.<group> -o json`, pruning by `metadata.managedFields` (see the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoHG;AAEH,OAAO,KAAK,EAEV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoHG;AAEH,OAAO,KAAK,EAEV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAajE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWtF;AAUD;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA0F5G"}
|
|
@@ -1,50 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GCP thin observation (#1209) — presence and scrubbed outputs, read over the
|
|
3
|
+
* applier's own REST transport.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* chant applies GCP cluster-free over direct REST (#706). Until this, it
|
|
6
|
+
* observed through a Config Connector cluster — `kubectl get <cnrm-gvk> -o
|
|
7
|
+
* json` per entity — which is the split #1085's principle forbids: a lexicon
|
|
8
|
+
* observed on a different transport than it is applied with can disagree with
|
|
9
|
+
* itself about what a resource even is, and needs a GKE cluster to answer a
|
|
10
|
+
* question about a bucket.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* Every GET here is built by the same `ResourceMapper` the applier uses (see
|
|
13
|
+
* ./api/read-client.ts), so reader and applier cannot diverge about where a
|
|
14
|
+
* resource lives.
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
* Everything else the kubectl call can fail with (auth, an unreachable API
|
|
14
|
-
* server, an entity type with no derivable GVK) is NOT-OBSERVED (#1089), so a
|
|
15
|
-
* read that never happened cannot become a proposed `create`.
|
|
16
|
+
* ## What changed in the answers, not just the transport
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* project's K8s entities would use against the same cluster.
|
|
18
|
+
* **Status.** Config Connector encodes state as a `Ready` condition, which a
|
|
19
|
+
* GCP REST payload has no equivalent of — a bucket simply exists. So a
|
|
20
|
+
* successful GET is `PRESENT` unless the body carries a recognisable state of
|
|
21
|
+
* its own (Cloud Run's `status.conditions`, an explicit `state`). `PRESENT` is
|
|
22
|
+
* the same sentinel the Azure reader emits for the same reason.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* **Ownership.** CNRM carried chant's marker as a k8s label. The REST payloads
|
|
25
|
+
* carry `labels` only on kinds that have them (a bucket does; a Pub/Sub topic
|
|
26
|
+
* does not), so ownership is `unknown` where there is nothing to read — and
|
|
27
|
+
* `--owned` degrades to detect-only rather than withholding everything it
|
|
28
|
+
* cannot prove, the same posture the AWS thin path takes when
|
|
29
|
+
* `describe-stack-resources` returns no tags.
|
|
30
|
+
*
|
|
31
|
+
* **Coverage.** kubectl could fetch any CNRM kind the cluster knew about; REST
|
|
32
|
+
* reaches the kinds the applier has a mapper for. That is narrower on paper and
|
|
33
|
+
* not in practice: a kind with no mapper cannot be applied either, so observing
|
|
34
|
+
* it produced a live tree nothing could reconcile against. Anything outside the
|
|
35
|
+
* table reports NOT-OBSERVED with `unsupported-kind` (#1089) rather than being
|
|
36
|
+
* dropped.
|
|
28
37
|
*/
|
|
29
38
|
import type { ObservationResult } from "@intentius/chant/lexicon";
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
39
|
+
/** The parts of a GCP REST payload this reader looks at. Every field is
|
|
40
|
+
* optional because they vary by kind — a bucket has `id` and `labels`, a
|
|
41
|
+
* Pub/Sub topic has neither. */
|
|
42
|
+
interface GcpRestResponse {
|
|
43
|
+
id?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
selfLink?: string;
|
|
46
|
+
labels?: Record<string, string> | null;
|
|
47
|
+
updated?: string;
|
|
48
|
+
timeCreated?: string;
|
|
49
|
+
state?: string;
|
|
50
|
+
status?: {
|
|
51
|
+
conditions?: Array<{
|
|
52
|
+
type?: string;
|
|
53
|
+
status?: string;
|
|
54
|
+
state?: string;
|
|
55
|
+
reason?: string;
|
|
56
|
+
}>;
|
|
57
|
+
[k: string]: unknown;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
48
60
|
/**
|
|
49
61
|
* Mirror of `lexicons/gcp/src/serializer.ts:deriveGVKFromType` — keeping the
|
|
50
62
|
* derivation logic local so describeResources can compute the kubectl resource
|
|
@@ -54,6 +66,19 @@ export declare function deriveGVK(entityType: string): {
|
|
|
54
66
|
group: string;
|
|
55
67
|
kind: string;
|
|
56
68
|
} | null;
|
|
69
|
+
/**
|
|
70
|
+
* Status from a REST payload.
|
|
71
|
+
*
|
|
72
|
+
* Most GCP resources have no status at all — a bucket that answers a GET simply
|
|
73
|
+
* exists — so `PRESENT` is the honest answer and the common one. Where a kind
|
|
74
|
+
* does carry state (Cloud Run's `status.conditions`, a `state` enum), that is
|
|
75
|
+
* reported instead.
|
|
76
|
+
*
|
|
77
|
+
* `PRESENT` is deliberately the same sentinel the Azure reader emits for a
|
|
78
|
+
* resource with no `provisioningState`: one word, meaning "read it back, it is
|
|
79
|
+
* there, there is nothing richer to say".
|
|
80
|
+
*/
|
|
81
|
+
export declare function statusFromRest(obj: GcpRestResponse): string;
|
|
57
82
|
export declare function describeResources(options: {
|
|
58
83
|
environment: string;
|
|
59
84
|
buildOutput: string;
|
|
@@ -64,4 +89,5 @@ export declare function describeResources(options: {
|
|
|
64
89
|
}>;
|
|
65
90
|
owned?: boolean;
|
|
66
91
|
}): Promise<ObservationResult>;
|
|
92
|
+
export {};
|
|
67
93
|
//# sourceMappingURL=describe-resources.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAsC,MAAM,0BAA0B,CAAC;AAqBtG;;gCAEgC;AAChC,UAAU,eAAe;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE;QACP,UAAU,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACxF,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASpF;AAUD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAY3D;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAmH7B"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "13863f1d8cea5d465ec62a46a03f663f99410213d240931a81278ece85f7486b",
|
|
5
5
|
"meta.json": "475f10909854d059a2e2d4fb541c058803422e38b9c64bd4d6858c0623eedb8c",
|
|
6
6
|
"types/index.d.ts": "5433b54c04b57d6fda8f3d81ef92ed0aca87f7e8e5dd3ea92d8de6e62a3ccfab",
|
|
7
7
|
"rules/hardcoded-project.ts": "228631d3159e1ffcce2359c66073d4ae59bb0285f378e46e446f416aec50481c",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"skills/chant-gcp-patterns.md": "a7ef31c1eb2f7244d3f73952c300472ef94c1eb09bd7a1003281b89299b6b704",
|
|
38
38
|
"skills/chant-gcp-gke.md": "be277019da9a722c851e47cd2dfb9c9536668948c3535fb20db7697e934c4e2b"
|
|
39
39
|
},
|
|
40
|
-
"composite": "
|
|
40
|
+
"composite": "13fc3388cd69bbb0f029a77554c0c8b9295f78e934e9ec67b65c8a516f58485a"
|
|
41
41
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type EmulatorCapability, type EmulatorSpec } from "@intentius/chant/op";
|
|
1
2
|
export interface FlociGcpUpArgs {
|
|
2
3
|
/** Container name. Default: `chant-floci-gcp`. */
|
|
3
4
|
name?: string;
|
|
4
5
|
/** Host port mapped to the emulator's `:4588`. Default: `4588`. */
|
|
5
6
|
port?: number;
|
|
6
|
-
/** Image. Default: `floci/floci-gcp
|
|
7
|
+
/** Image. Default: the pinned `floci/floci-gcp` tag. */
|
|
7
8
|
image?: string;
|
|
8
9
|
/** Readiness timeout in ms. Default: `60000`. */
|
|
9
10
|
timeoutMs?: number;
|
|
@@ -14,6 +15,16 @@ export interface FlociGcpDownArgs {
|
|
|
14
15
|
/** Container name to remove. Default: `chant-floci-gcp`. */
|
|
15
16
|
name?: string;
|
|
16
17
|
}
|
|
18
|
+
export declare const FLOCI_GCP_SPEC: EmulatorSpec;
|
|
19
|
+
/**
|
|
20
|
+
* The gcp plugin's emulator capability (#1345).
|
|
21
|
+
*
|
|
22
|
+
* `env` is deliberately empty: `gcpApply` reaches the emulator through an
|
|
23
|
+
* explicit `endpoint` argument rather than an ambient variable, so there is no
|
|
24
|
+
* var to inject and claiming one would be worse than claiming none. The
|
|
25
|
+
* endpoint is still reported by `chant emulator up --json`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const FLOCI_GCP_EMULATOR: EmulatorCapability;
|
|
17
28
|
export declare const flociGcpExistsCommand: (name: string) => string;
|
|
18
29
|
export declare const flociGcpRmCommand: (name: string) => string;
|
|
19
30
|
export declare const flociGcpHealthUrl: (port: number) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floci-gcp.d.ts","sourceRoot":"","sources":["../../../src/op/activities/floci-gcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"floci-gcp.d.ts","sourceRoot":"","sources":["../../../src/op/activities/floci-gcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEpG,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAKD,eAAO,MAAM,cAAc,EAAE,YAM5B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,kBAGhC,CAAC;AAIF,eAAO,MAAM,qBAAqB,0BAAoB,CAAC;AACvD,eAAO,MAAM,iBAAiB,0BAAgB,CAAC;AAC/C,eAAO,MAAM,iBAAiB,0BAAgB,CAAC;AAC/C,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,0BAAe,CAAC;AAC7C,eAAO,MAAM,kBAAkB,GAAI,OAAM,cAAmB,KAAG,MAA8B,CAAC;AAE9F,+EAA+E;AAC/E,eAAO,MAAM,UAAU,GAAI,OAAM,cAAmB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CACnF,CAAC;AAEvB,6EAA6E;AAC7E,eAAO,MAAM,YAAY,GAAI,OAAM,gBAAqB,EAAE,SAAS,WAAW,KAAG,OAAO,CAAC,IAAI,CACrE,CAAC"}
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAqC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAqC,MAAM,0BAA0B,CAAC;AAuBjG,eAAO,MAAM,SAAS,EAAE,aAyXvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-gcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"typescript": "^5.9.3"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@intentius/chant": "^0.
|
|
79
|
+
"@intentius/chant": "^0.38.0",
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GCP read transport (#1209) — the applier's own REST client, pointed at the
|
|
3
|
+
* read side.
|
|
4
|
+
*
|
|
5
|
+
* GCP applies cluster-free over direct REST (#706) and, until this, observed
|
|
6
|
+
* through a Config Connector cluster: `kubectl get <cnrm-gvk> -o json` per
|
|
7
|
+
* entity, on both the thin and the deep path. That split is what #1085's
|
|
8
|
+
* principle forbids — a lexicon should be observed on the transport it is
|
|
9
|
+
* applied with, or the two can disagree about what a resource even is — and it
|
|
10
|
+
* is what made GCP the heaviest of the three O slots.
|
|
11
|
+
*
|
|
12
|
+
* ## The URL comes from the applier, not from here
|
|
13
|
+
*
|
|
14
|
+
* Every GET is built by the same `ResourceMapper.plan()` the applier uses, and
|
|
15
|
+
* this module reads `plan().getUrl` rather than composing its own URL. That is
|
|
16
|
+
* deliberate: a reader that builds its own paths is a second, silently
|
|
17
|
+
* divergent opinion about where a resource lives, and the first symptom is a
|
|
18
|
+
* read that reports absent for something the applier just wrote. Reusing the
|
|
19
|
+
* mapper makes the two structurally incapable of disagreeing.
|
|
20
|
+
*
|
|
21
|
+
* ## What that bounds
|
|
22
|
+
*
|
|
23
|
+
* Coverage is exactly the applier's dispatch table (`MAPPERS`) — StorageBucket,
|
|
24
|
+
* PubSubTopic, PubSubSubscription, SecretManagerSecret, IAMServiceAccount,
|
|
25
|
+
* RunService. The kubectl path could fetch any CNRM kind the cluster knew
|
|
26
|
+
* about, so this is narrower on paper. It is not narrower in practice for
|
|
27
|
+
* anything chant can act on: a kind with no mapper cannot be applied either, so
|
|
28
|
+
* observing it produced a live tree nothing could ever reconcile against.
|
|
29
|
+
*
|
|
30
|
+
* A kind outside the table reports NOT-OBSERVED with `unsupported-kind` rather
|
|
31
|
+
* than being dropped, so the estate says "chant did not look at this" instead of
|
|
32
|
+
* quietly implying it is not there. Widening the table is #1209's stated
|
|
33
|
+
* non-goal and belongs with the applier.
|
|
34
|
+
*/
|
|
35
|
+
import { MAPPERS, type GcpResource, type ResourceMapper } from "../op/activities/gcp-apply";
|
|
36
|
+
|
|
37
|
+
/** Where to read, and as whom. */
|
|
38
|
+
export interface GcpReadClientOptions {
|
|
39
|
+
/** Endpoint override for every kind (floci-gcp `http://localhost:4588`). Omit for real GCP. */
|
|
40
|
+
endpoint?: string;
|
|
41
|
+
/** GCP project the resources live in. */
|
|
42
|
+
project: string;
|
|
43
|
+
/** Injectable HTTP, so tests never touch the network. */
|
|
44
|
+
http?: GcpReadHttp;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Injectable HTTP client — mirrors the applier's `GcpHttp`. */
|
|
48
|
+
export type GcpReadHttp = (method: string, url: string) => Promise<{ status: number; text: string }>;
|
|
49
|
+
|
|
50
|
+
const defaultHttp: GcpReadHttp = async (method, url) => {
|
|
51
|
+
const res = await fetch(url, { method });
|
|
52
|
+
return { status: res.status, text: await res.text() };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A read that failed for a reason the caller must classify — carries the HTTP
|
|
57
|
+
* status so a 404 (absent) is told apart from a 401/403 (no credentials) and a
|
|
58
|
+
* 5xx/network error (could not look). Mirrors `AzureReadError`.
|
|
59
|
+
*/
|
|
60
|
+
export class GcpReadError extends Error {
|
|
61
|
+
constructor(
|
|
62
|
+
message: string,
|
|
63
|
+
readonly status?: number,
|
|
64
|
+
) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "GcpReadError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A 404 — the resource is genuinely absent, which is an answer, not a failure. */
|
|
71
|
+
export function isNotFound(err: unknown): boolean {
|
|
72
|
+
return err instanceof GcpReadError && err.status === 404;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The mapper for a CNRM kind, or undefined when chant cannot act on it. */
|
|
76
|
+
export function mapperForKind(kind: string): ResourceMapper | undefined {
|
|
77
|
+
return MAPPERS[kind];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* GET one resource on the applier's transport.
|
|
82
|
+
*
|
|
83
|
+
* `declared` is the entity's own declared spec, which is what `plan()` needs to
|
|
84
|
+
* build a URL — the same input the applier hands it. Returns the parsed body,
|
|
85
|
+
* or throws {@link GcpReadError} carrying the status.
|
|
86
|
+
*/
|
|
87
|
+
export async function getResource(
|
|
88
|
+
client: GcpReadClientOptions,
|
|
89
|
+
kind: string,
|
|
90
|
+
name: string,
|
|
91
|
+
declared?: Record<string, unknown>,
|
|
92
|
+
): Promise<Record<string, unknown>> {
|
|
93
|
+
const mapper = mapperForKind(kind);
|
|
94
|
+
if (!mapper) throw new GcpReadError(`no REST mapper for kind ${kind}`);
|
|
95
|
+
|
|
96
|
+
const base = (client.endpoint ?? mapper.defaultHost).replace(/\/$/, "");
|
|
97
|
+
const resource: GcpResource = {
|
|
98
|
+
kind,
|
|
99
|
+
metadata: { name },
|
|
100
|
+
spec: (declared?.spec as Record<string, unknown> | undefined) ?? declared ?? {},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let getUrl: string;
|
|
104
|
+
try {
|
|
105
|
+
getUrl = mapper.plan(resource, { base, project: client.project }).getUrl;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// A mapper that cannot plan from the declared spec cannot produce a URL, so
|
|
108
|
+
// there is nothing to read — a hole, never an absence.
|
|
109
|
+
throw new GcpReadError(`could not build a read URL for ${kind}/${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const http = client.http ?? defaultHttp;
|
|
113
|
+
let res: { status: number; text: string };
|
|
114
|
+
try {
|
|
115
|
+
res = await http("GET", getUrl);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new GcpReadError(err instanceof Error ? err.message : String(err));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (res.status === 404) throw new GcpReadError(`${kind}/${name} not found`, 404);
|
|
121
|
+
if (res.status < 200 || res.status >= 300) {
|
|
122
|
+
throw new GcpReadError(`GET ${kind}/${name} failed with ${res.status}: ${res.text.slice(0, 200)}`, res.status);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(res.text) as Record<string, unknown>;
|
|
126
|
+
} catch {
|
|
127
|
+
throw new GcpReadError(`GET ${kind}/${name} returned a non-JSON body`, res.status);
|
|
128
|
+
}
|
|
129
|
+
}
|