@intentius/chant-lexicon-k8s 0.44.3 → 0.44.5
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/crd/parser.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +47 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/group-namespace.d.ts +46 -0
- package/dist/group-namespace.d.ts.map +1 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/crd/parser.ts +2 -67
- package/src/describe-resources.test.ts +276 -2
- package/src/describe-resources.ts +68 -1
- package/src/group-namespace.test.ts +64 -0
- package/src/group-namespace.ts +79 -0
- package/src/import/parser.ts +7 -6
- package/src/kustomize/root.test.ts +3 -1
- package/src/spec/parse.ts +6 -14
package/dist/crd/parser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/crd/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAwD,MAAM,eAAe,CAAC;AAC1G,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/crd/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAwD,MAAM,eAAe,CAAC;AAC1G,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAKvC;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAqB1D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,EAAE,CAoD5D"}
|
|
@@ -62,6 +62,34 @@ import { type K8sConnector } from "./api/connect.js";
|
|
|
62
62
|
*/
|
|
63
63
|
export declare function statusFromObject(obj: K8sObject): string;
|
|
64
64
|
export declare function unhappyConditions(obj: K8sObject): string[] | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The revisions a GitOps controller writes onto its own object (#1632).
|
|
67
|
+
*
|
|
68
|
+
* Conditions lag. A Flux Kustomization mid-rollout still reports the previous
|
|
69
|
+
* reconcile's `Ready=True` for as long as the new one takes, so a consumer
|
|
70
|
+
* reading only conditions calls a half-applied estate converged. The
|
|
71
|
+
* controller's own definition of converged is a revision comparison:
|
|
72
|
+
* `lastAppliedRevision` against `lastAttemptedRevision` on the same object
|
|
73
|
+
* (attempted-but-not-applied is in flight), and a Kustomization's
|
|
74
|
+
* `lastAppliedRevision` against its source's `artifact.revision` (the
|
|
75
|
+
* cross-object half, which a consumer joins once both ends are on the wire).
|
|
76
|
+
* None of the three reached an observed entity's attributes before this.
|
|
77
|
+
*
|
|
78
|
+
* Read by path rather than by kind, which is the module's standing rule
|
|
79
|
+
* everywhere else: `status.lastAppliedRevision` / `status.lastAttemptedRevision`
|
|
80
|
+
* are what kustomize-controller and helm-controller write, and
|
|
81
|
+
* `status.artifact.revision` is what every source kind (GitRepository,
|
|
82
|
+
* OCIRepository, Bucket, HelmChart) writes. An object that has none of them —
|
|
83
|
+
* every non-GitOps kind — contributes nothing, so no kind table has to name
|
|
84
|
+
* which kinds those are, and a CRD speaking the same convention is carried for
|
|
85
|
+
* free. `artifactRevision` flattens the one nested path so every revision on
|
|
86
|
+
* the wire is a plain string next to the others.
|
|
87
|
+
*/
|
|
88
|
+
export declare function revisionAttributes(obj: K8sObject): {
|
|
89
|
+
lastAppliedRevision?: string;
|
|
90
|
+
lastAttemptedRevision?: string;
|
|
91
|
+
artifactRevision?: string;
|
|
92
|
+
};
|
|
65
93
|
export interface DescribeResourcesOptions {
|
|
66
94
|
environment: string;
|
|
67
95
|
buildOutput: string;
|
|
@@ -73,6 +101,25 @@ export interface DescribeResourcesOptions {
|
|
|
73
101
|
owned?: boolean;
|
|
74
102
|
/** Directory whose `chant.config.ts` carries the cluster binding. Defaults to cwd. */
|
|
75
103
|
cwd?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Namespace to read a namespaced entity from when it declares none (#1629).
|
|
106
|
+
*
|
|
107
|
+
* A GitOps estate splits the binding from the objects: the control-plane
|
|
108
|
+
* project declares a Kustomization with `spec.targetNamespace: app-b`, the
|
|
109
|
+
* app project declares bare Deployments and Services with no
|
|
110
|
+
* `metadata.namespace` at all — the controller stamps it at apply time. The
|
|
111
|
+
* live read of the app project had no way to be told that, so it fell
|
|
112
|
+
* through to the client's `default` and reported an estate that is running
|
|
113
|
+
* as entirely absent (#1620's `queried` line is what made that visible).
|
|
114
|
+
*
|
|
115
|
+
* A DEFAULT, never a rewrite: an entity with an explicit
|
|
116
|
+
* `metadata.namespace` reads from the namespace it declares, override or
|
|
117
|
+
* not — that declaration is the estate's own statement of where the object
|
|
118
|
+
* lives, and overruling it would make the option a way to read the wrong
|
|
119
|
+
* object rather than the right one. Cluster-scoped kinds are untouched:
|
|
120
|
+
* they have no namespace to default.
|
|
121
|
+
*/
|
|
122
|
+
namespace?: string;
|
|
76
123
|
}
|
|
77
124
|
export declare function describeResources(options: DescribeResourcesOptions, connect?: K8sConnector): Promise<ObservationResult>;
|
|
78
125
|
//# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAwD,MAAM,0BAA0B,CAAC;AAGxH,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAmBvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CA2CvD;AA4FD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CAetE;AAQD,MAAM,WAAW,wBAAwB;IACvC,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;IAChB,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAwD,MAAM,0BAA0B,CAAC;AAGxH,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAmBvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CA2CvD;AA4FD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CAetE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,GAAG;IAClD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAUA;AAQD,MAAM,WAAW,wBAAwB;IACvC,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;IAChB,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,EACjC,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CA6J5B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API group → `K8s::{Namespace}::{Kind}` namespace segment — the ONE copy.
|
|
3
|
+
*
|
|
4
|
+
* Four surfaces build entityTypes from a group string: CRD codegen
|
|
5
|
+
* (`crd/parser.ts`), the swagger pass (`spec/parse.ts`), live discovery
|
|
6
|
+
* (`describe-resources.ts` via `gvkToTypeName`), and YAML import
|
|
7
|
+
* (`import/parser.ts`). Until #1628 each had its own copy of the rule and only
|
|
8
|
+
* CRD codegen consulted the overrides, so a Flux Kustomization arriving through
|
|
9
|
+
* a kustomize root or `chant import` was typed `K8s::Kustomize::Kustomization`
|
|
10
|
+
* — a name that matches nothing in operations.json (NOT-OBSERVED live) and
|
|
11
|
+
* nothing downstream that dispatches on kind. Every path resolves here now;
|
|
12
|
+
* the declared, rendered, imported and discovered spellings of a kind cannot
|
|
13
|
+
* skew.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Group names whose first segment doesn't yield the conventional namespace.
|
|
17
|
+
* "argoproj.io" → "Argo" (not "Argoproj") to match the Argo CD vocabulary
|
|
18
|
+
* and the ArgoAppFor / ArgoAppSetForRegions composites.
|
|
19
|
+
*
|
|
20
|
+
* The Flux toolkit spreads across five `*.toolkit.fluxcd.io` groups plus the
|
|
21
|
+
* Flux Operator's `fluxcd.controlplane.io`; all six collapse to a single `Flux`
|
|
22
|
+
* namespace so a GitRepository and a Kustomization read as `K8s::Flux::*`
|
|
23
|
+
* siblings rather than scattering to Source / Kustomize / Helm / Notification /
|
|
24
|
+
* Image / Fluxcd. (`helm.toolkit.fluxcd.io` → `Helm` would also collide
|
|
25
|
+
* confusingly with the separate helm lexicon.)
|
|
26
|
+
*
|
|
27
|
+
* KubeMicroVM's `lambda.aws.amazon.com` would take `Lambda` by the
|
|
28
|
+
* first-segment rule, which reads as AWS Lambda proper and would sit
|
|
29
|
+
* confusingly beside the aws lexicon's real Lambda functions. These are
|
|
30
|
+
* Kubernetes CRs belonging to a community operator, so they take the
|
|
31
|
+
* operator's own name. `MicroVM` alone was the alternative and was rejected
|
|
32
|
+
* for stuttering: `K8s::MicroVM::MicroVM`.
|
|
33
|
+
*
|
|
34
|
+
* Note the official AWS controller uses a different group,
|
|
35
|
+
* `lambdamicrovms.services.k8s.aws`, so it can coexist here and will want its
|
|
36
|
+
* own entry rather than sharing this one.
|
|
37
|
+
*/
|
|
38
|
+
export declare const GROUP_NAMESPACE_OVERRIDES: Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* Normalize an API group to its PascalCase namespace segment.
|
|
41
|
+
* "" → "Core", overrides first ("argoproj.io" → "Argo"), then the first
|
|
42
|
+
* dot-segment kebab→PascalCased: "cert-manager.io" → "CertManager",
|
|
43
|
+
* "rbac.authorization.k8s.io" → "Rbac", "apps" → "Apps".
|
|
44
|
+
*/
|
|
45
|
+
export declare function namespaceSegmentForGroup(group: string): string;
|
|
46
|
+
//# sourceMappingURL=group-namespace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-namespace.d.ts","sourceRoot":"","sources":["../src/group-namespace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAuB5D,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS9D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAc,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAc,MAAM,gCAAgC,CAAC;AAyH7F;;;;;GAKG;AACH,qBAAa,SAAU,YAAW,cAAc;IAC9C,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU;IAsClC,OAAO,CAAC,aAAa;CA8BtB"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "8c679fcec79d5351a4019ac14bb0b2665f1570b67015247cda5cec6f3192c94e",
|
|
5
5
|
"meta.json": "f568fb8a0cbd4adf041baccaa3e1164c2e3c5c0090186236f887f0084f5521a8",
|
|
6
6
|
"types/index.d.ts": "ef2c9df6a4d1ebfbe33f96c094fbbe3fc7909522ea38e086666fbf3c8ac477fd",
|
|
7
7
|
"rules/argo-appset-single-project.ts": "afa9f310753aa2d475f35012b13135b2dfaebed2a35d44edbe185ebc07673674",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"skills/chant-k8s-argo.md": "b1a0b826559d8c5033a479c5781efaf650320f0aee4419d8841170bd3393cea5",
|
|
55
55
|
"skills/chant-k8s-flux.md": "9670efc2e9854bd14686b5adeb707396b9030a4dcaee6529b30abf56efe2c50c"
|
|
56
56
|
},
|
|
57
|
-
"composite": "
|
|
57
|
+
"composite": "4770ee5c6ebbc3ccfeaf324c13675ae19c299ae88f5b957dcb8c7a551321306f"
|
|
58
58
|
}
|
package/dist/manifest.json
CHANGED
package/dist/spec/parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/spec/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/spec/parse.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-k8s",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.5",
|
|
4
4
|
"description": "Kubernetes 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
|
"@types/js-yaml": "^4.0.9"
|
|
77
77
|
},
|
|
78
78
|
"optionalDependencies": {
|
|
79
|
-
"@intentius/chant-k8s-client": "^0.44.
|
|
79
|
+
"@intentius/chant-k8s-client": "^0.44.5"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@intentius/chant": "*",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"zod": "^4.3.6",
|
|
87
|
-
"@intentius/chant": "^0.44.
|
|
87
|
+
"@intentius/chant": "^0.44.5",
|
|
88
88
|
"typescript": "^5.9.3"
|
|
89
89
|
}
|
|
90
90
|
}
|
package/src/crd/parser.ts
CHANGED
|
@@ -9,75 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import type { K8sParseResult, ParsedProperty, ParsedPropertyType, GroupVersionKind } from "../spec/parse";
|
|
11
11
|
import type { CRDSpec } from "./types";
|
|
12
|
+
import { namespaceSegmentForGroup } from "../group-namespace";
|
|
12
13
|
import type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
|
|
13
14
|
import { loadAll } from "js-yaml";
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* Group names whose first segment doesn't yield the conventional namespace.
|
|
17
|
-
* "argoproj.io" → "Argo" (not "Argoproj") to match the Argo CD vocabulary
|
|
18
|
-
* and the ArgoAppFor / ArgoAppSetForRegions composites.
|
|
19
|
-
*
|
|
20
|
-
* The Flux toolkit spreads across five `*.toolkit.fluxcd.io` groups plus the
|
|
21
|
-
* Flux Operator's `fluxcd.controlplane.io`; all six collapse to a single `Flux`
|
|
22
|
-
* namespace so a GitRepository and a Kustomization read as `K8s::Flux::*`
|
|
23
|
-
* siblings rather than scattering to Source / Kustomize / Helm / Notification /
|
|
24
|
-
* Image / Fluxcd. (`helm.toolkit.fluxcd.io` → `Helm` would also collide
|
|
25
|
-
* confusingly with the separate helm lexicon.)
|
|
26
|
-
*
|
|
27
|
-
* KubeMicroVM's `lambda.aws.amazon.com` would take `Lambda` by the
|
|
28
|
-
* first-segment rule, which reads as AWS Lambda proper and would sit
|
|
29
|
-
* confusingly beside the aws lexicon's real Lambda functions. These are
|
|
30
|
-
* Kubernetes CRs belonging to a community operator, so they take the
|
|
31
|
-
* operator's own name. `MicroVM` alone was the alternative and was rejected
|
|
32
|
-
* for stuttering: `K8s::MicroVM::MicroVM`.
|
|
33
|
-
*
|
|
34
|
-
* Note the official AWS controller uses a different group,
|
|
35
|
-
* `lambdamicrovms.services.k8s.aws`, so it can coexist here and will want its
|
|
36
|
-
* own entry rather than sharing this one.
|
|
37
|
-
*/
|
|
38
|
-
const GROUP_NAMESPACE_OVERRIDES: Record<string, string> = {
|
|
39
|
-
"argoproj.io": "Argo",
|
|
40
|
-
"source.toolkit.fluxcd.io": "Flux",
|
|
41
|
-
"kustomize.toolkit.fluxcd.io": "Flux",
|
|
42
|
-
"helm.toolkit.fluxcd.io": "Flux",
|
|
43
|
-
"notification.toolkit.fluxcd.io": "Flux",
|
|
44
|
-
"image.toolkit.fluxcd.io": "Flux",
|
|
45
|
-
"fluxcd.controlplane.io": "Flux",
|
|
46
|
-
"lambda.aws.amazon.com": "KubeMicroVM",
|
|
47
|
-
// CNPG and its barman-cloud plugin ship under two groups but are one thing to
|
|
48
|
-
// an author: a Cluster's `plugins[]` names an ObjectStore. The first-segment
|
|
49
|
-
// rule would scatter them into `Postgresql` and `Barmancloud`.
|
|
50
|
-
"postgresql.cnpg.io": "Cnpg",
|
|
51
|
-
"barmancloud.cnpg.io": "Cnpg",
|
|
52
|
-
// Not `Secrets`: `K8s::Secrets::InfisicalSecret` reads like a core Secret,
|
|
53
|
-
// and `K8s::Core::Secret` is right there to be confused with.
|
|
54
|
-
"secrets.infisical.com": "Infisical",
|
|
55
|
-
// k3s's bundled controllers ship under two groups but are one thing to an
|
|
56
|
-
// author — the k3s auto-deploy surface. The first-segment rule would give
|
|
57
|
-
// `K8s::Helm::HelmChart`, which reads like it belongs to the helm lexicon,
|
|
58
|
-
// and would split HelmChart from the Addon that tracks its deployment.
|
|
59
|
-
"helm.cattle.io": "K3s",
|
|
60
|
-
"k3s.cattle.io": "K3s",
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Normalize a CRD group to a PascalCase namespace segment.
|
|
65
|
-
* "cert-manager.io" → "CertManager"
|
|
66
|
-
* "monitoring.coreos.com" → "Monitoring"
|
|
67
|
-
* "argoproj.io" → "Argo" (override)
|
|
68
|
-
*/
|
|
69
|
-
function normalizeGroupName(group: string): string {
|
|
70
|
-
const override = GROUP_NAMESPACE_OVERRIDES[group];
|
|
71
|
-
if (override) return override;
|
|
72
|
-
// Take the first segment before the first dot
|
|
73
|
-
const firstSegment = group.split(".")[0];
|
|
74
|
-
// Convert kebab-case to PascalCase
|
|
75
|
-
return firstSegment
|
|
76
|
-
.split("-")
|
|
77
|
-
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
78
|
-
.join("");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
16
|
/**
|
|
82
17
|
* Parse a CRD YAML document string into K8sParseResult entries.
|
|
83
18
|
* Returns one result per served version in the CRD.
|
|
@@ -111,7 +46,7 @@ export function parseCRD(content: string): K8sParseResult[] {
|
|
|
111
46
|
*/
|
|
112
47
|
export function parseCRDSpec(spec: CRDSpec): K8sParseResult[] {
|
|
113
48
|
const results: K8sParseResult[] = [];
|
|
114
|
-
const groupNs =
|
|
49
|
+
const groupNs = namespaceSegmentForGroup(spec.group);
|
|
115
50
|
|
|
116
51
|
// Find the storage version (the canonical version)
|
|
117
52
|
const storageVersion = spec.versions.find((v) => v.storage && v.served);
|
|
@@ -15,7 +15,7 @@ vi.mock("@intentius/chant/config", () => ({
|
|
|
15
15
|
loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
const { describeResources, statusFromObject, unhappyConditions } = await import("./describe-resources");
|
|
18
|
+
const { describeResources, statusFromObject, unhappyConditions, revisionAttributes } = await import("./describe-resources");
|
|
19
19
|
const { fakeCluster, objectKey, ownedObject } = await import("./api/fake-cluster");
|
|
20
20
|
const { defaultK8sConnector } = await import("./api/connect");
|
|
21
21
|
const { fakeKubeconfig, statusBody } = await import("@intentius/chant-k8s-client/testing");
|
|
@@ -731,7 +731,7 @@ describe("k8s describeResources", () => {
|
|
|
731
731
|
);
|
|
732
732
|
|
|
733
733
|
expect(result.resources["microvm-demo/kmv-dev-a-vm-42sgf"]).toMatchObject({
|
|
734
|
-
type: "K8s::
|
|
734
|
+
type: "K8s::KubeMicroVM::MicroVM",
|
|
735
735
|
physicalId: "vm-uid",
|
|
736
736
|
ownerChain: { root: "declared", entity: "workloadReplicaSet" },
|
|
737
737
|
});
|
|
@@ -1399,3 +1399,277 @@ describe("unhappyConditions (#1401)", () => {
|
|
|
1399
1399
|
expect(unhappyConditions({} as never)).toBeUndefined();
|
|
1400
1400
|
});
|
|
1401
1401
|
});
|
|
1402
|
+
|
|
1403
|
+
describe("revisionAttributes — the GitOps convergence fields (#1632)", () => {
|
|
1404
|
+
test("carries a Kustomization's applied and attempted revisions", () => {
|
|
1405
|
+
expect(
|
|
1406
|
+
revisionAttributes({
|
|
1407
|
+
status: {
|
|
1408
|
+
lastAppliedRevision: "main@sha1:1a2b3c4d",
|
|
1409
|
+
lastAttemptedRevision: "main@sha1:5e6f7a8b",
|
|
1410
|
+
conditions: [{ type: "Ready", status: "True" }],
|
|
1411
|
+
},
|
|
1412
|
+
} as never),
|
|
1413
|
+
).toMatchObject({ lastAppliedRevision: "main@sha1:1a2b3c4d", lastAttemptedRevision: "main@sha1:5e6f7a8b" });
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
test("flattens a source kind's status.artifact.revision", () => {
|
|
1417
|
+
expect(revisionAttributes({ status: { artifact: { revision: "main@sha1:1a2b3c4d" } } } as never)).toMatchObject({
|
|
1418
|
+
artifactRevision: "main@sha1:1a2b3c4d",
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
test("says nothing for an object that carries none of them", () => {
|
|
1423
|
+
// Every field undefined, so `pruneUndefined` drops all three and the
|
|
1424
|
+
// attributes of a Deployment or a Service are byte-for-byte what they were.
|
|
1425
|
+
expect(revisionAttributes({ status: { readyReplicas: 1, replicas: 1 } } as never)).toEqual({
|
|
1426
|
+
lastAppliedRevision: undefined,
|
|
1427
|
+
lastAttemptedRevision: undefined,
|
|
1428
|
+
artifactRevision: undefined,
|
|
1429
|
+
});
|
|
1430
|
+
expect(revisionAttributes({} as never)).toEqual({
|
|
1431
|
+
lastAppliedRevision: undefined,
|
|
1432
|
+
lastAttemptedRevision: undefined,
|
|
1433
|
+
artifactRevision: undefined,
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
test("ignores non-string and empty revisions", () => {
|
|
1438
|
+
expect(
|
|
1439
|
+
revisionAttributes({ status: { lastAppliedRevision: "", lastAttemptedRevision: 7, artifact: { revision: null } } } as never),
|
|
1440
|
+
).toEqual({ lastAppliedRevision: undefined, lastAttemptedRevision: undefined, artifactRevision: undefined });
|
|
1441
|
+
});
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
describe("observed attributes carry the Flux revisions (#1632)", () => {
|
|
1445
|
+
const applied = "main@sha1:1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b";
|
|
1446
|
+
const attempted = "main@sha1:5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d";
|
|
1447
|
+
|
|
1448
|
+
test("a Kustomization mid-rollout reports applied != attempted even with Ready=True", async () => {
|
|
1449
|
+
const kustomization = {
|
|
1450
|
+
apiVersion: "kustomize.toolkit.fluxcd.io/v1",
|
|
1451
|
+
kind: "Kustomization",
|
|
1452
|
+
metadata: { name: "apps", namespace: "flux-system", uid: "ks-uid", resourceVersion: "42" },
|
|
1453
|
+
status: {
|
|
1454
|
+
lastAppliedRevision: applied,
|
|
1455
|
+
lastAttemptedRevision: attempted,
|
|
1456
|
+
conditions: [{ type: "Ready", status: "True", reason: "ReconciliationSucceeded" }],
|
|
1457
|
+
},
|
|
1458
|
+
};
|
|
1459
|
+
const source = {
|
|
1460
|
+
apiVersion: "source.toolkit.fluxcd.io/v1",
|
|
1461
|
+
kind: "GitRepository",
|
|
1462
|
+
metadata: { name: "infra", namespace: "flux-system", uid: "gr-uid" },
|
|
1463
|
+
status: {
|
|
1464
|
+
artifact: { revision: attempted, path: "gitrepository/flux-system/infra/x.tar.gz" },
|
|
1465
|
+
conditions: [{ type: "Ready", status: "True" }],
|
|
1466
|
+
},
|
|
1467
|
+
};
|
|
1468
|
+
const cluster = fakeCluster({
|
|
1469
|
+
objects: {
|
|
1470
|
+
[objectKey("kustomize.toolkit.fluxcd.io/v1", "Kustomization", "apps", "flux-system")]: kustomization,
|
|
1471
|
+
[objectKey("source.toolkit.fluxcd.io/v1", "GitRepository", "infra", "flux-system")]: source,
|
|
1472
|
+
},
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
const result = await describeResources(
|
|
1476
|
+
{
|
|
1477
|
+
environment: "prod",
|
|
1478
|
+
buildOutput: "",
|
|
1479
|
+
entityNames: ["apps", "infra"],
|
|
1480
|
+
entities: makeEntities([
|
|
1481
|
+
{
|
|
1482
|
+
name: "apps",
|
|
1483
|
+
entityType: "K8s::Flux::Kustomization",
|
|
1484
|
+
props: { metadata: { name: "apps", namespace: "flux-system" } },
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
name: "infra",
|
|
1488
|
+
entityType: "K8s::Flux::GitRepository",
|
|
1489
|
+
props: { metadata: { name: "infra", namespace: "flux-system" } },
|
|
1490
|
+
},
|
|
1491
|
+
]),
|
|
1492
|
+
},
|
|
1493
|
+
cluster.connector,
|
|
1494
|
+
);
|
|
1495
|
+
|
|
1496
|
+
// The per-node half: attempted is ahead of applied, so this object is
|
|
1497
|
+
// mid-flight however cheerful its Ready condition is.
|
|
1498
|
+
expect(result.resources.apps?.attributes).toMatchObject({
|
|
1499
|
+
lastAppliedRevision: applied,
|
|
1500
|
+
lastAttemptedRevision: attempted,
|
|
1501
|
+
});
|
|
1502
|
+
// The cross-node half: the source's artifact revision, which a consumer
|
|
1503
|
+
// joins against the Kustomization's applied revision.
|
|
1504
|
+
expect(result.resources.infra?.attributes).toMatchObject({ artifactRevision: attempted });
|
|
1505
|
+
expect(result.resources.apps?.status).toBe("READY");
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
test("a HelmRelease's revisions ride the same path, and other kinds are untouched", async () => {
|
|
1509
|
+
const helmRelease = {
|
|
1510
|
+
apiVersion: "helm.toolkit.fluxcd.io/v2",
|
|
1511
|
+
kind: "HelmRelease",
|
|
1512
|
+
metadata: { name: "podinfo", namespace: "prod", uid: "hr-uid" },
|
|
1513
|
+
status: { lastAppliedRevision: "6.5.4", lastAttemptedRevision: "6.5.4", conditions: [{ type: "Ready", status: "True" }] },
|
|
1514
|
+
};
|
|
1515
|
+
const cluster = fakeCluster({
|
|
1516
|
+
objects: {
|
|
1517
|
+
[objectKey("helm.toolkit.fluxcd.io/v2", "HelmRelease", "podinfo", "prod")]: helmRelease,
|
|
1518
|
+
[objectKey("apps/v1", "Deployment", "web", "prod")]: web,
|
|
1519
|
+
},
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
const result = await describeResources(
|
|
1523
|
+
{
|
|
1524
|
+
environment: "prod",
|
|
1525
|
+
buildOutput: "",
|
|
1526
|
+
entityNames: ["podinfo", "web"],
|
|
1527
|
+
entities: makeEntities([
|
|
1528
|
+
{
|
|
1529
|
+
name: "podinfo",
|
|
1530
|
+
entityType: "K8s::Flux::HelmRelease",
|
|
1531
|
+
props: { metadata: { name: "podinfo", namespace: "prod" } },
|
|
1532
|
+
},
|
|
1533
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
|
|
1534
|
+
]),
|
|
1535
|
+
},
|
|
1536
|
+
cluster.connector,
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
expect(result.resources.podinfo?.attributes).toMatchObject({
|
|
1540
|
+
lastAppliedRevision: "6.5.4",
|
|
1541
|
+
lastAttemptedRevision: "6.5.4",
|
|
1542
|
+
});
|
|
1543
|
+
// The string wire form for every other kind is what it was — no empty
|
|
1544
|
+
// revision keys appear on a Deployment (#1401's shape, unchanged).
|
|
1545
|
+
expect(result.resources.web?.attributes).toEqual({
|
|
1546
|
+
namespace: "prod",
|
|
1547
|
+
labels: { app: "web" },
|
|
1548
|
+
resourceVersion: "7",
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// The behold#221 shape (#1629): the estate's control-plane project declares
|
|
1554
|
+
// the Kustomization carrying `spec.targetNamespace`, and the app project
|
|
1555
|
+
// declares bare objects the controller stamps at apply time. Reading the app
|
|
1556
|
+
// project alone, `metadata.namespace` is absent everywhere and the read
|
|
1557
|
+
// defaults to `default` — a running estate reported entirely absent. The
|
|
1558
|
+
// caller that knows the binding can now say where to look.
|
|
1559
|
+
describe("a per-read namespace override for entities that declare none (#1629)", () => {
|
|
1560
|
+
const inAppB = {
|
|
1561
|
+
apiVersion: "apps/v1",
|
|
1562
|
+
kind: "Deployment",
|
|
1563
|
+
metadata: { name: "web", namespace: "app-b", uid: "uid-b", resourceVersion: "11" },
|
|
1564
|
+
status: { readyReplicas: 1, replicas: 1 },
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
test("replaces the `default` fallback for a namespaced entity that declares no namespace", async () => {
|
|
1568
|
+
const cluster = fakeCluster({ objects: { [objectKey("apps/v1", "Deployment", "web", "app-b")]: inAppB } });
|
|
1569
|
+
const result = await describeResources(
|
|
1570
|
+
{
|
|
1571
|
+
environment: "prod",
|
|
1572
|
+
buildOutput: "",
|
|
1573
|
+
entityNames: ["web"],
|
|
1574
|
+
entities: makeEntities([
|
|
1575
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web" } } },
|
|
1576
|
+
]),
|
|
1577
|
+
namespace: "app-b",
|
|
1578
|
+
},
|
|
1579
|
+
cluster.connector,
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1582
|
+
expect(result.resources.web).toMatchObject({ type: "K8s::Apps::Deployment", physicalId: "uid-b" });
|
|
1583
|
+
// The `queried` diagnosis line (#1620) reflects the overridden address —
|
|
1584
|
+
// whatever the read did, the report says where it looked.
|
|
1585
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/app-b/deployments/web");
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
test("an explicitly declared namespace still wins — the override is a default, not a rewrite", async () => {
|
|
1589
|
+
const cluster = fakeCluster({
|
|
1590
|
+
objects: {
|
|
1591
|
+
[objectKey("apps/v1", "Deployment", "web", "prod")]: web,
|
|
1592
|
+
[objectKey("apps/v1", "Deployment", "web", "app-b")]: inAppB,
|
|
1593
|
+
},
|
|
1594
|
+
});
|
|
1595
|
+
const result = await describeResources(
|
|
1596
|
+
{
|
|
1597
|
+
environment: "prod",
|
|
1598
|
+
buildOutput: "",
|
|
1599
|
+
entityNames: ["web"],
|
|
1600
|
+
entities: makeEntities([
|
|
1601
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
|
|
1602
|
+
]),
|
|
1603
|
+
namespace: "app-b",
|
|
1604
|
+
},
|
|
1605
|
+
cluster.connector,
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1608
|
+
// Both objects exist; the declared namespace decides which one was read.
|
|
1609
|
+
expect(result.resources.web).toMatchObject({ physicalId: "uid-1" });
|
|
1610
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/prod/deployments/web");
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
test("without the override the read still defaults to `default` — nothing changed for a caller that passes none", async () => {
|
|
1614
|
+
const cluster = fakeCluster({ objects: { [objectKey("apps/v1", "Deployment", "web", "app-b")]: inAppB } });
|
|
1615
|
+
const result = await describeResources(
|
|
1616
|
+
{
|
|
1617
|
+
environment: "prod",
|
|
1618
|
+
buildOutput: "",
|
|
1619
|
+
entityNames: ["web"],
|
|
1620
|
+
entities: makeEntities([
|
|
1621
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web" } } },
|
|
1622
|
+
]),
|
|
1623
|
+
},
|
|
1624
|
+
cluster.connector,
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
expect(result.resources).toEqual({});
|
|
1628
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/default/deployments/web");
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
test("a cluster-scoped kind is untouched — it has no namespace to default", async () => {
|
|
1632
|
+
const ns = { apiVersion: "v1", kind: "Namespace", metadata: { name: "app-b", uid: "ns-uid" } };
|
|
1633
|
+
const cluster = fakeCluster({ objects: { [objectKey("v1", "Namespace", "app-b")]: ns } });
|
|
1634
|
+
const result = await describeResources(
|
|
1635
|
+
{
|
|
1636
|
+
environment: "prod",
|
|
1637
|
+
buildOutput: "",
|
|
1638
|
+
entityNames: ["appB"],
|
|
1639
|
+
entities: makeEntities([
|
|
1640
|
+
{ name: "appB", entityType: "K8s::Core::Namespace", props: { metadata: { name: "app-b" } } },
|
|
1641
|
+
]),
|
|
1642
|
+
namespace: "app-b",
|
|
1643
|
+
},
|
|
1644
|
+
cluster.connector,
|
|
1645
|
+
);
|
|
1646
|
+
|
|
1647
|
+
expect(result.resources.appB).toMatchObject({ physicalId: "ns-uid" });
|
|
1648
|
+
expect(result.queried?.appB).toBe("/api/v1/namespaces/app-b");
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
test("the override reaches every declared entity, not just the first", async () => {
|
|
1652
|
+
const svc = { apiVersion: "v1", kind: "Service", metadata: { name: "web-svc", namespace: "app-b", uid: "svc-b" } };
|
|
1653
|
+
const cluster = fakeCluster({
|
|
1654
|
+
objects: {
|
|
1655
|
+
[objectKey("apps/v1", "Deployment", "web", "app-b")]: inAppB,
|
|
1656
|
+
[objectKey("v1", "Service", "web-svc", "app-b")]: svc,
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
const result = await describeResources(
|
|
1660
|
+
{
|
|
1661
|
+
environment: "prod",
|
|
1662
|
+
buildOutput: "",
|
|
1663
|
+
entityNames: ["web", "webSvc"],
|
|
1664
|
+
entities: makeEntities([
|
|
1665
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web" } } },
|
|
1666
|
+
{ name: "webSvc", entityType: "K8s::Core::Service", props: { metadata: { name: "web-svc" } } },
|
|
1667
|
+
]),
|
|
1668
|
+
namespace: "app-b",
|
|
1669
|
+
},
|
|
1670
|
+
cluster.connector,
|
|
1671
|
+
);
|
|
1672
|
+
|
|
1673
|
+
expect(Object.keys(result.resources).sort()).toEqual(["web", "webSvc"]);
|
|
1674
|
+
});
|
|
1675
|
+
});
|
|
@@ -233,6 +233,45 @@ export function unhappyConditions(obj: K8sObject): string[] | undefined {
|
|
|
233
233
|
return out.length > 0 ? out : undefined;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* The revisions a GitOps controller writes onto its own object (#1632).
|
|
238
|
+
*
|
|
239
|
+
* Conditions lag. A Flux Kustomization mid-rollout still reports the previous
|
|
240
|
+
* reconcile's `Ready=True` for as long as the new one takes, so a consumer
|
|
241
|
+
* reading only conditions calls a half-applied estate converged. The
|
|
242
|
+
* controller's own definition of converged is a revision comparison:
|
|
243
|
+
* `lastAppliedRevision` against `lastAttemptedRevision` on the same object
|
|
244
|
+
* (attempted-but-not-applied is in flight), and a Kustomization's
|
|
245
|
+
* `lastAppliedRevision` against its source's `artifact.revision` (the
|
|
246
|
+
* cross-object half, which a consumer joins once both ends are on the wire).
|
|
247
|
+
* None of the three reached an observed entity's attributes before this.
|
|
248
|
+
*
|
|
249
|
+
* Read by path rather than by kind, which is the module's standing rule
|
|
250
|
+
* everywhere else: `status.lastAppliedRevision` / `status.lastAttemptedRevision`
|
|
251
|
+
* are what kustomize-controller and helm-controller write, and
|
|
252
|
+
* `status.artifact.revision` is what every source kind (GitRepository,
|
|
253
|
+
* OCIRepository, Bucket, HelmChart) writes. An object that has none of them —
|
|
254
|
+
* every non-GitOps kind — contributes nothing, so no kind table has to name
|
|
255
|
+
* which kinds those are, and a CRD speaking the same convention is carried for
|
|
256
|
+
* free. `artifactRevision` flattens the one nested path so every revision on
|
|
257
|
+
* the wire is a plain string next to the others.
|
|
258
|
+
*/
|
|
259
|
+
export function revisionAttributes(obj: K8sObject): {
|
|
260
|
+
lastAppliedRevision?: string;
|
|
261
|
+
lastAttemptedRevision?: string;
|
|
262
|
+
artifactRevision?: string;
|
|
263
|
+
} {
|
|
264
|
+
const status = obj.status as
|
|
265
|
+
| { lastAppliedRevision?: unknown; lastAttemptedRevision?: unknown; artifact?: { revision?: unknown } }
|
|
266
|
+
| undefined;
|
|
267
|
+
const str = (v: unknown): string | undefined => (typeof v === "string" && v.length > 0 ? v : undefined);
|
|
268
|
+
return {
|
|
269
|
+
lastAppliedRevision: str(status?.lastAppliedRevision),
|
|
270
|
+
lastAttemptedRevision: str(status?.lastAttemptedRevision),
|
|
271
|
+
artifactRevision: str(status?.artifact?.revision),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
236
275
|
interface Declared {
|
|
237
276
|
entityName: string;
|
|
238
277
|
entityType: string;
|
|
@@ -247,6 +286,25 @@ export interface DescribeResourcesOptions {
|
|
|
247
286
|
owned?: boolean;
|
|
248
287
|
/** Directory whose `chant.config.ts` carries the cluster binding. Defaults to cwd. */
|
|
249
288
|
cwd?: string;
|
|
289
|
+
/**
|
|
290
|
+
* Namespace to read a namespaced entity from when it declares none (#1629).
|
|
291
|
+
*
|
|
292
|
+
* A GitOps estate splits the binding from the objects: the control-plane
|
|
293
|
+
* project declares a Kustomization with `spec.targetNamespace: app-b`, the
|
|
294
|
+
* app project declares bare Deployments and Services with no
|
|
295
|
+
* `metadata.namespace` at all — the controller stamps it at apply time. The
|
|
296
|
+
* live read of the app project had no way to be told that, so it fell
|
|
297
|
+
* through to the client's `default` and reported an estate that is running
|
|
298
|
+
* as entirely absent (#1620's `queried` line is what made that visible).
|
|
299
|
+
*
|
|
300
|
+
* A DEFAULT, never a rewrite: an entity with an explicit
|
|
301
|
+
* `metadata.namespace` reads from the namespace it declares, override or
|
|
302
|
+
* not — that declaration is the estate's own statement of where the object
|
|
303
|
+
* lives, and overruling it would make the option a way to read the wrong
|
|
304
|
+
* object rather than the right one. Cluster-scoped kinds are untouched:
|
|
305
|
+
* they have no namespace to default.
|
|
306
|
+
*/
|
|
307
|
+
namespace?: string;
|
|
250
308
|
}
|
|
251
309
|
|
|
252
310
|
export async function describeResources(
|
|
@@ -331,11 +389,17 @@ export async function describeResources(
|
|
|
331
389
|
return;
|
|
332
390
|
}
|
|
333
391
|
|
|
392
|
+
// The declared namespace wins; the caller's override only fills the hole a
|
|
393
|
+
// declaration left, and only for a kind that HAS a namespace (#1629). With
|
|
394
|
+
// neither, the client defaults to `default` exactly as it did.
|
|
395
|
+
const namespace =
|
|
396
|
+
metadata?.namespace ?? (operation.scope === "Namespaced" ? options.namespace : undefined);
|
|
397
|
+
|
|
334
398
|
const ref = {
|
|
335
399
|
apiVersion: operation.apiVersion,
|
|
336
400
|
kind: operation.kind,
|
|
337
401
|
name,
|
|
338
|
-
...(
|
|
402
|
+
...(namespace ? { namespace } : {}),
|
|
339
403
|
};
|
|
340
404
|
|
|
341
405
|
// Resolve the address BEFORE the read, so every verdict below — present,
|
|
@@ -379,6 +443,9 @@ export async function describeResources(
|
|
|
379
443
|
resourceVersion: obj.metadata?.resourceVersion,
|
|
380
444
|
// What the object's own controller says is wrong with it (#1401).
|
|
381
445
|
conditions: unhappyConditions(obj),
|
|
446
|
+
// What a GitOps controller says it has applied and is applying
|
|
447
|
+
// (#1632) — the convergence signal conditions cannot carry.
|
|
448
|
+
...revisionAttributes(obj),
|
|
382
449
|
}),
|
|
383
450
|
};
|
|
384
451
|
} catch (err) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { namespaceSegmentForGroup } from "./group-namespace";
|
|
3
|
+
import { renderedManifestEntity } from "./kustomize/rendered-entity";
|
|
4
|
+
import { K8sParser } from "./import/parser";
|
|
5
|
+
|
|
6
|
+
describe("namespaceSegmentForGroup", () => {
|
|
7
|
+
test("the empty core group is Core", () => {
|
|
8
|
+
expect(namespaceSegmentForGroup("")).toBe("Core");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("a single-segment group PascalCases", () => {
|
|
12
|
+
expect(namespaceSegmentForGroup("apps")).toBe("Apps");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("only the first dot-segment counts", () => {
|
|
16
|
+
expect(namespaceSegmentForGroup("rbac.authorization.k8s.io")).toBe("Rbac");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("a hyphenated first segment kebab→Pascals", () => {
|
|
20
|
+
expect(namespaceSegmentForGroup("cert-manager.io")).toBe("CertManager");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("argoproj.io takes the Argo override", () => {
|
|
24
|
+
expect(namespaceSegmentForGroup("argoproj.io")).toBe("Argo");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("the toolkit groups collapse to Flux", () => {
|
|
28
|
+
expect(namespaceSegmentForGroup("kustomize.toolkit.fluxcd.io")).toBe("Flux");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// #1628: rendered and imported entityTypes used to bypass the overrides, so a
|
|
33
|
+
// Flux CR arriving through a kustomize root or `chant import` was typed to a
|
|
34
|
+
// name that matches nothing in operations.json.
|
|
35
|
+
describe("the shared rule reaches the render and import paths", () => {
|
|
36
|
+
test("a rendered Flux Kustomization is typed K8s::Flux::Kustomization", () => {
|
|
37
|
+
const entity = renderedManifestEntity(
|
|
38
|
+
{
|
|
39
|
+
apiVersion: "kustomize.toolkit.fluxcd.io/v1",
|
|
40
|
+
kind: "Kustomization",
|
|
41
|
+
metadata: { name: "apps", namespace: "flux-system" },
|
|
42
|
+
spec: { path: "./apps/prod", prune: true },
|
|
43
|
+
},
|
|
44
|
+
"clusters/prod",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(entity?.entityType).toBe("K8s::Flux::Kustomization");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("an imported Argo Application is typed K8s::Argo::Application", () => {
|
|
51
|
+
const ir = new K8sParser().parse(`
|
|
52
|
+
apiVersion: argoproj.io/v1alpha1
|
|
53
|
+
kind: Application
|
|
54
|
+
metadata:
|
|
55
|
+
name: guestbook
|
|
56
|
+
namespace: argocd
|
|
57
|
+
spec:
|
|
58
|
+
project: default
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
expect(ir.resources.length).toBe(1);
|
|
62
|
+
expect(ir.resources[0].type).toBe("K8s::Argo::Application");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API group → `K8s::{Namespace}::{Kind}` namespace segment — the ONE copy.
|
|
3
|
+
*
|
|
4
|
+
* Four surfaces build entityTypes from a group string: CRD codegen
|
|
5
|
+
* (`crd/parser.ts`), the swagger pass (`spec/parse.ts`), live discovery
|
|
6
|
+
* (`describe-resources.ts` via `gvkToTypeName`), and YAML import
|
|
7
|
+
* (`import/parser.ts`). Until #1628 each had its own copy of the rule and only
|
|
8
|
+
* CRD codegen consulted the overrides, so a Flux Kustomization arriving through
|
|
9
|
+
* a kustomize root or `chant import` was typed `K8s::Kustomize::Kustomization`
|
|
10
|
+
* — a name that matches nothing in operations.json (NOT-OBSERVED live) and
|
|
11
|
+
* nothing downstream that dispatches on kind. Every path resolves here now;
|
|
12
|
+
* the declared, rendered, imported and discovered spellings of a kind cannot
|
|
13
|
+
* skew.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Group names whose first segment doesn't yield the conventional namespace.
|
|
18
|
+
* "argoproj.io" → "Argo" (not "Argoproj") to match the Argo CD vocabulary
|
|
19
|
+
* and the ArgoAppFor / ArgoAppSetForRegions composites.
|
|
20
|
+
*
|
|
21
|
+
* The Flux toolkit spreads across five `*.toolkit.fluxcd.io` groups plus the
|
|
22
|
+
* Flux Operator's `fluxcd.controlplane.io`; all six collapse to a single `Flux`
|
|
23
|
+
* namespace so a GitRepository and a Kustomization read as `K8s::Flux::*`
|
|
24
|
+
* siblings rather than scattering to Source / Kustomize / Helm / Notification /
|
|
25
|
+
* Image / Fluxcd. (`helm.toolkit.fluxcd.io` → `Helm` would also collide
|
|
26
|
+
* confusingly with the separate helm lexicon.)
|
|
27
|
+
*
|
|
28
|
+
* KubeMicroVM's `lambda.aws.amazon.com` would take `Lambda` by the
|
|
29
|
+
* first-segment rule, which reads as AWS Lambda proper and would sit
|
|
30
|
+
* confusingly beside the aws lexicon's real Lambda functions. These are
|
|
31
|
+
* Kubernetes CRs belonging to a community operator, so they take the
|
|
32
|
+
* operator's own name. `MicroVM` alone was the alternative and was rejected
|
|
33
|
+
* for stuttering: `K8s::MicroVM::MicroVM`.
|
|
34
|
+
*
|
|
35
|
+
* Note the official AWS controller uses a different group,
|
|
36
|
+
* `lambdamicrovms.services.k8s.aws`, so it can coexist here and will want its
|
|
37
|
+
* own entry rather than sharing this one.
|
|
38
|
+
*/
|
|
39
|
+
export const GROUP_NAMESPACE_OVERRIDES: Record<string, string> = {
|
|
40
|
+
"argoproj.io": "Argo",
|
|
41
|
+
"source.toolkit.fluxcd.io": "Flux",
|
|
42
|
+
"kustomize.toolkit.fluxcd.io": "Flux",
|
|
43
|
+
"helm.toolkit.fluxcd.io": "Flux",
|
|
44
|
+
"notification.toolkit.fluxcd.io": "Flux",
|
|
45
|
+
"image.toolkit.fluxcd.io": "Flux",
|
|
46
|
+
"fluxcd.controlplane.io": "Flux",
|
|
47
|
+
"lambda.aws.amazon.com": "KubeMicroVM",
|
|
48
|
+
// CNPG and its barman-cloud plugin ship under two groups but are one thing to
|
|
49
|
+
// an author: a Cluster's `plugins[]` names an ObjectStore. The first-segment
|
|
50
|
+
// rule would scatter them into `Postgresql` and `Barmancloud`.
|
|
51
|
+
"postgresql.cnpg.io": "Cnpg",
|
|
52
|
+
"barmancloud.cnpg.io": "Cnpg",
|
|
53
|
+
// Not `Secrets`: `K8s::Secrets::InfisicalSecret` reads like a core Secret,
|
|
54
|
+
// and `K8s::Core::Secret` is right there to be confused with.
|
|
55
|
+
"secrets.infisical.com": "Infisical",
|
|
56
|
+
// k3s's bundled controllers ship under two groups but are one thing to an
|
|
57
|
+
// author — the k3s auto-deploy surface. The first-segment rule would give
|
|
58
|
+
// `K8s::Helm::HelmChart`, which reads like it belongs to the helm lexicon,
|
|
59
|
+
// and would split HelmChart from the Addon that tracks its deployment.
|
|
60
|
+
"helm.cattle.io": "K3s",
|
|
61
|
+
"k3s.cattle.io": "K3s",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Normalize an API group to its PascalCase namespace segment.
|
|
66
|
+
* "" → "Core", overrides first ("argoproj.io" → "Argo"), then the first
|
|
67
|
+
* dot-segment kebab→PascalCased: "cert-manager.io" → "CertManager",
|
|
68
|
+
* "rbac.authorization.k8s.io" → "Rbac", "apps" → "Apps".
|
|
69
|
+
*/
|
|
70
|
+
export function namespaceSegmentForGroup(group: string): string {
|
|
71
|
+
if (!group || group === "") return "Core";
|
|
72
|
+
const override = GROUP_NAMESPACE_OVERRIDES[group];
|
|
73
|
+
if (override) return override;
|
|
74
|
+
const firstSegment = group.split(".")[0];
|
|
75
|
+
return firstSegment
|
|
76
|
+
.split("-")
|
|
77
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
78
|
+
.join("");
|
|
79
|
+
}
|
package/src/import/parser.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { TemplateParser, TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
|
|
10
10
|
import { parseYAML } from "@intentius/chant/yaml";
|
|
11
|
+
import { namespaceSegmentForGroup } from "../group-namespace";
|
|
11
12
|
|
|
12
13
|
// ── GVK to type name mapping ───────────────────────────────────────
|
|
13
14
|
|
|
@@ -91,20 +92,20 @@ function resolveTypeName(apiVersion: string, kind: string): string {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
|
-
* Extract
|
|
95
|
+
* Extract the namespace segment from an apiVersion string.
|
|
95
96
|
* "apps/v1" → "Apps"
|
|
96
97
|
* "v1" → "Core"
|
|
97
|
-
* "networking.k8s.io/v1" → "Networking"
|
|
98
98
|
* "rbac.authorization.k8s.io/v1" → "Rbac"
|
|
99
|
+
* "argoproj.io/v1alpha1" → "Argo" (override)
|
|
100
|
+
*
|
|
101
|
+
* Splits the version off and hands the group to the shared rule, so an
|
|
102
|
+
* imported manifest is typed exactly as a declared one is.
|
|
99
103
|
*/
|
|
100
104
|
function apiVersionToGroup(apiVersion: string): string {
|
|
101
105
|
const slashIdx = apiVersion.indexOf("/");
|
|
102
106
|
if (slashIdx === -1) return "Core"; // core group (v1)
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
const firstSegment = groupStr.split(".")[0];
|
|
106
|
-
if (firstSegment === "rbac") return "Rbac";
|
|
107
|
-
return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
|
|
108
|
+
return namespaceSegmentForGroup(apiVersion.slice(0, slashIdx));
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/**
|
|
@@ -94,7 +94,9 @@ describe("renderKustomizeRoots", () => {
|
|
|
94
94
|
// The generated operation surface's naming, so `lifecycle diff --live`
|
|
95
95
|
// observes a rendered Deployment through the same reader a declared one uses.
|
|
96
96
|
expect(deployment.entityType).toBe("K8s::Apps::Deployment");
|
|
97
|
-
|
|
97
|
+
// Hyphenated groups PascalCase the same way the declarable surface does
|
|
98
|
+
// (#1628), so this Certificate matches the reader a declared one gets.
|
|
99
|
+
expect(entities.get("overlays/prod/certificateProdWebTls")!.entityType).toBe("K8s::CertManager::Certificate");
|
|
98
100
|
|
|
99
101
|
const props = (deployment as unknown as { props: Record<string, unknown> }).props;
|
|
100
102
|
const metadata = props.metadata as { annotations: Record<string, string> };
|
package/src/spec/parse.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
primaryType,
|
|
14
14
|
type JsonSchemaProperty,
|
|
15
15
|
} from "@intentius/chant/codegen/json-schema";
|
|
16
|
+
import { namespaceSegmentForGroup } from "../group-namespace";
|
|
16
17
|
|
|
17
18
|
// ── Types ──────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -343,22 +344,13 @@ export function gvkToApiVersion(gvk: GroupVersionKind): string {
|
|
|
343
344
|
}
|
|
344
345
|
|
|
345
346
|
/**
|
|
346
|
-
* Normalize API group to a PascalCase segment
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* "rbac.authorization.k8s.io" → "Rbac"
|
|
351
|
-
* "networking.k8s.io" → "Networking"
|
|
347
|
+
* Normalize API group to a PascalCase segment — the shared rule, so the
|
|
348
|
+
* swagger-generated surface, kustomize-rendered entities and live discovery
|
|
349
|
+
* (all three reach here through `gvkToTypeName`) spell a kind exactly as CRD
|
|
350
|
+
* codegen and `chant import` do. See `../group-namespace`.
|
|
352
351
|
*/
|
|
353
352
|
function normalizeGroup(group: string): string {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// Take the first segment before any dots
|
|
357
|
-
const firstSegment = group.split(".")[0];
|
|
358
|
-
// Special-case RBAC
|
|
359
|
-
if (firstSegment === "rbac") return "Rbac";
|
|
360
|
-
// PascalCase the first segment
|
|
361
|
-
return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
|
|
353
|
+
return namespaceSegmentForGroup(group);
|
|
362
354
|
}
|
|
363
355
|
|
|
364
356
|
/**
|