@intentius/chant-lexicon-k8s 0.42.1 → 0.44.2
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/classify.d.ts.map +1 -1
- package/dist/api/connect.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/argo-app.d.ts +104 -0
- package/dist/components/argo-app.d.ts.map +1 -0
- package/dist/components/builders.d.ts +8 -0
- package/dist/components/builders.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +1 -0
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/flux-reconcile.d.ts +84 -0
- package/dist/components/flux-reconcile.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/kustomize-apply.d.ts +29 -7
- package/dist/components/kustomize-apply.d.ts.map +1 -1
- package/dist/composites/flux-app.d.ts +132 -0
- package/dist/composites/flux-app.d.ts.map +1 -0
- package/dist/composites/index.d.ts +2 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/config-schema.d.ts +3 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config.d.ts +32 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/crd/crd-sources.d.ts.map +1 -1
- package/dist/crd/parser.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +10 -8
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/generated/index.d.ts +15 -4
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +10 -6
- package/dist/kube/connect.d.ts +5 -4
- package/dist/kube/connect.d.ts.map +1 -1
- package/dist/kube/get.d.ts +4 -4
- package/dist/kustomize/render.d.ts +15 -0
- package/dist/kustomize/render.d.ts.map +1 -0
- package/dist/kustomize/rendered-entity.d.ts +39 -0
- package/dist/kustomize/rendered-entity.d.ts.map +1 -0
- package/dist/kustomize/root.d.ts +19 -0
- package/dist/kustomize/root.d.ts.map +1 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/argo-helpers.d.ts +2 -0
- package/dist/lint/post-synth/argo-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/flux002.d.ts +14 -0
- package/dist/lint/post-synth/flux002.d.ts.map +1 -0
- package/dist/lint/post-synth/flux003.d.ts +17 -0
- package/dist/lint/post-synth/flux003.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/rules/flux-source-ref-pin.d.ts +16 -0
- package/dist/lint/rules/flux-source-ref-pin.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +92 -16
- package/dist/okf/index.md +7 -1
- package/dist/okf/rules/FLUX001.md +17 -0
- package/dist/okf/rules/FLUX002.md +19 -0
- package/dist/okf/rules/FLUX003.md +15 -0
- package/dist/okf/types/Addon.md +14 -0
- package/dist/okf/types/Bucket.md +4 -0
- package/dist/okf/types/{HelmChart.md → FluxHelmChart.md} +2 -2
- package/dist/okf/types/GitRepository.md +5 -0
- package/dist/okf/types/HelmChartConfig.md +14 -0
- package/dist/okf/types/K3sHelmChart.md +14 -0
- package/dist/okf/types/Kustomization.md +6 -0
- package/dist/okf/types/Namespace.md +1 -0
- package/dist/okf/types/Node.md +1 -0
- package/dist/okf/types/OCIRepository.md +4 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/argo-helpers.ts +2 -0
- package/dist/rules/flux-source-ref-pin.ts +52 -0
- package/dist/rules/flux002.ts +77 -0
- package/dist/rules/flux003.ts +75 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-k8s-flux.md +162 -0
- package/dist/types/index.d.ts +41 -3
- package/dist/validate.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/api/classify.test.ts +37 -0
- package/src/api/classify.ts +11 -1
- package/src/api/connect.ts +10 -0
- package/src/codegen/docs.ts +11 -0
- package/src/components/argo-app.test.ts +137 -0
- package/src/components/argo-app.ts +163 -0
- package/src/components/builders.ts +8 -0
- package/src/components/capability-plugin.ts +11 -1
- package/src/components/flux-reconcile.test.ts +145 -0
- package/src/components/flux-reconcile.ts +159 -0
- package/src/components/index.ts +13 -0
- package/src/components/kustomize-apply.ts +12 -41
- package/src/composites/composites.test.ts +122 -0
- package/src/composites/flux-app.ts +274 -0
- package/src/composites/index.ts +9 -0
- package/src/config-augmentation.test.ts +72 -0
- package/src/config-schema.ts +5 -0
- package/src/config.ts +33 -5
- package/src/crd/crd-sources.ts +37 -0
- package/src/crd/parser.ts +6 -0
- package/src/describe-resources.test.ts +290 -6
- package/src/describe-resources.ts +235 -127
- package/src/generated/index.d.ts +41 -3
- package/src/generated/index.ts +15 -4
- package/src/generated/lexicon-k8s.json +92 -16
- package/src/generated/operations.json +48 -0
- package/src/index.ts +4 -0
- package/src/kube/connect.ts +5 -4
- package/src/kube/get.ts +4 -4
- package/src/kustomize/render.ts +84 -0
- package/src/kustomize/rendered-entity.ts +72 -0
- package/src/kustomize/root.test.ts +268 -0
- package/src/kustomize/root.ts +109 -0
- package/src/lint/audit-catalog.ts +2 -0
- package/src/lint/post-synth/argo-helpers.ts +2 -0
- package/src/lint/post-synth/flux002.ts +77 -0
- package/src/lint/post-synth/flux003.ts +75 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/post-synth.test.ts +132 -0
- package/src/lint/rules/flux-source-ref-pin.ts +52 -0
- package/src/lint/rules/rules.test.ts +51 -0
- package/src/plugin.test.ts +2 -0
- package/src/plugin.ts +47 -0
- package/src/serializer.ts +34 -4
- package/src/skills/chant-k8s-flux.md +162 -0
- package/src/testdata/kustomize-root/base/deployment.yaml +17 -0
- package/src/testdata/kustomize-root/base/kustomization.yaml +3 -0
- package/src/testdata/kustomize-root/base/service.yaml +9 -0
- package/src/testdata/kustomize-root/overlays/prod/kustomization.yaml +6 -0
- package/src/validate.ts +4 -0
|
@@ -105,6 +105,81 @@ describe("k8s describeResources", () => {
|
|
|
105
105
|
expect(result.unobserved ?? {}).toEqual({});
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// The behold#192 shape (#1620): a Flux-deployed object declares no
|
|
109
|
+
// metadata.namespace (the controller stamps targetNamespace at apply), so the
|
|
110
|
+
// live read scopes to the client's default namespace, finds nothing, and
|
|
111
|
+
// correctly reports absence. Every layer is spec-correct and the picture
|
|
112
|
+
// still says the opposite of the truth — unless the report carries the
|
|
113
|
+
// address it actually asked, which is what `queried` is.
|
|
114
|
+
test("an absent verdict carries the resolved query address, namespace defaulted and visible (#1620)", async () => {
|
|
115
|
+
const cluster = fakeCluster({
|
|
116
|
+
// The object lives where Flux put it — not where the read will look.
|
|
117
|
+
objects: { [objectKey("apps/v1", "Deployment", "web", "flux-target")]: web },
|
|
118
|
+
});
|
|
119
|
+
const result = await describeResources(
|
|
120
|
+
{
|
|
121
|
+
environment: "prod",
|
|
122
|
+
buildOutput: "",
|
|
123
|
+
entityNames: ["web"],
|
|
124
|
+
entities: makeEntities([
|
|
125
|
+
// No metadata.namespace declared — Flux stamps it at apply time.
|
|
126
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web" } } },
|
|
127
|
+
]),
|
|
128
|
+
},
|
|
129
|
+
cluster.connector,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Still a clean absence: in neither map, the tri-state unshifted.
|
|
133
|
+
expect(result.resources).toEqual({});
|
|
134
|
+
expect(result.unobserved ?? {}).toEqual({});
|
|
135
|
+
// But the report can now explain itself: the read went to `default`.
|
|
136
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/default/deployments/web");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a present entity carries its query address too — the map is total over issued reads (#1620)", async () => {
|
|
140
|
+
const cluster = fakeCluster({
|
|
141
|
+
objects: { [objectKey("apps/v1", "Deployment", "web", "prod")]: web },
|
|
142
|
+
});
|
|
143
|
+
const result = await describeResources(
|
|
144
|
+
{
|
|
145
|
+
environment: "prod",
|
|
146
|
+
buildOutput: "",
|
|
147
|
+
entityNames: ["web"],
|
|
148
|
+
entities: makeEntities([
|
|
149
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
|
|
150
|
+
]),
|
|
151
|
+
},
|
|
152
|
+
cluster.connector,
|
|
153
|
+
);
|
|
154
|
+
expect(result.resources.web).toBeDefined();
|
|
155
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/prod/deployments/web");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a read-failed unobserved entry carries the address the failing read was issued against (#1620)", async () => {
|
|
159
|
+
const cluster = fakeCluster({
|
|
160
|
+
respond: (req) =>
|
|
161
|
+
req.path.endsWith("/deployments/web")
|
|
162
|
+
? { status: 500, body: statusBody(500, "InternalError", "etcdserver: request timed out") }
|
|
163
|
+
: undefined,
|
|
164
|
+
});
|
|
165
|
+
const result = await describeResources(
|
|
166
|
+
{
|
|
167
|
+
environment: "prod",
|
|
168
|
+
buildOutput: "",
|
|
169
|
+
entityNames: ["web"],
|
|
170
|
+
entities: makeEntities([
|
|
171
|
+
{ name: "web", entityType: "K8s::Apps::Deployment", props: { metadata: { name: "web", namespace: "prod" } } },
|
|
172
|
+
]),
|
|
173
|
+
},
|
|
174
|
+
cluster.connector,
|
|
175
|
+
);
|
|
176
|
+
expect(result.unobserved?.web).toMatchObject({
|
|
177
|
+
reason: "read-failed",
|
|
178
|
+
queried: "/apis/apps/v1/namespaces/prod/deployments/web",
|
|
179
|
+
});
|
|
180
|
+
expect(result.queried?.web).toBe("/apis/apps/v1/namespaces/prod/deployments/web");
|
|
181
|
+
});
|
|
182
|
+
|
|
108
183
|
// The headline of chant #1074: the twenty-entry map is gone, so a CRD is an
|
|
109
184
|
// ordinary read rather than a permanent hole.
|
|
110
185
|
test("a CRD is observed like anything else — no KUBECTL_RESOURCE entry required", async () => {
|
|
@@ -465,6 +540,44 @@ describe("k8s describeResources", () => {
|
|
|
465
540
|
warnSpy.mockRestore();
|
|
466
541
|
});
|
|
467
542
|
|
|
543
|
+
test("a failed read names the bound context it read (#1488)", async () => {
|
|
544
|
+
loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "prod-eks" } } } } });
|
|
545
|
+
const cluster = fakeCluster({
|
|
546
|
+
kubeconfig: twoContexts,
|
|
547
|
+
respond: (req) =>
|
|
548
|
+
req.path.endsWith("/deployments/web")
|
|
549
|
+
? { status: 500, body: statusBody(500, "InternalError", "etcdserver: leader changed") }
|
|
550
|
+
: undefined,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const result = await describeResources(
|
|
554
|
+
{ environment: "prod", buildOutput: "", entityNames: ["web"], entities: entities() },
|
|
555
|
+
(o) => defaultK8sConnector({ ...o, client: { kubeconfig: twoContexts, requestLayer: cluster.layer } }),
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
expect(result.unobserved?.web?.reason).toBe("read-failed");
|
|
559
|
+
expect(result.unobserved?.web?.detail).toContain('context "prod-eks" (bound by k8s.profiles.prod.context)');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("an ambient read failure names the context that was read and the missing binding (#1488)", async () => {
|
|
563
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
564
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
565
|
+
const cluster = fakeCluster({
|
|
566
|
+
kubeconfig: twoContexts,
|
|
567
|
+
respond: (req) =>
|
|
568
|
+
req.path.endsWith("/deployments/web") ? { status: 500, body: statusBody(500, "InternalError", "nope") } : undefined,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const result = await describeResources(
|
|
572
|
+
{ environment: "prod", buildOutput: "", entityNames: ["web"], entities: entities() },
|
|
573
|
+
(o) => defaultK8sConnector({ ...o, client: { kubeconfig: twoContexts, requestLayer: cluster.layer } }),
|
|
574
|
+
);
|
|
575
|
+
warnSpy.mockRestore();
|
|
576
|
+
|
|
577
|
+
expect(result.unobserved?.web?.reason).toBe("read-failed");
|
|
578
|
+
expect(result.unobserved?.web?.detail).toContain('context "prod-eks" (ambient; no k8s.profiles.prod binding)');
|
|
579
|
+
});
|
|
580
|
+
|
|
468
581
|
test("a bound context the kubeconfig does not have refuses with the context name", async () => {
|
|
469
582
|
loadChantConfigMock.mockResolvedValue({ config: { k8s: { profiles: { prod: { context: "gone-eks" } } } } });
|
|
470
583
|
const oneContext = fakeKubeconfig({ contexts: [{ name: "prod-eks" }], currentContext: "prod-eks" });
|
|
@@ -573,6 +686,161 @@ describe("k8s describeResources", () => {
|
|
|
573
686
|
expect(result.resources["prod/hand-made"]).toBeUndefined();
|
|
574
687
|
});
|
|
575
688
|
|
|
689
|
+
// The issue's own estate: a declared MicroVMReplicaSet whose operator
|
|
690
|
+
// made MicroVMs — a CRD kind no table anticipated. Discovery names it.
|
|
691
|
+
test("a controller-made child of a CRD kind surfaces as a runtime child of the declared CR", async () => {
|
|
692
|
+
const replicaSet = {
|
|
693
|
+
apiVersion: "lambda.aws.amazon.com/v1alpha1",
|
|
694
|
+
kind: "MicroVMReplicaSet",
|
|
695
|
+
metadata: { name: "kmv-dev-a-vm", namespace: "microvm-demo", uid: "mvrs-uid" },
|
|
696
|
+
};
|
|
697
|
+
const vm = {
|
|
698
|
+
apiVersion: "lambda.aws.amazon.com/v1alpha1",
|
|
699
|
+
kind: "MicroVM",
|
|
700
|
+
metadata: {
|
|
701
|
+
name: "kmv-dev-a-vm-42sgf",
|
|
702
|
+
namespace: "microvm-demo",
|
|
703
|
+
uid: "vm-uid",
|
|
704
|
+
ownerReferences: [
|
|
705
|
+
{ apiVersion: "lambda.aws.amazon.com/v1alpha1", kind: "MicroVMReplicaSet", name: "kmv-dev-a-vm", uid: "mvrs-uid", controller: true },
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
status: { phase: "Running" },
|
|
709
|
+
};
|
|
710
|
+
const cluster = fakeCluster({
|
|
711
|
+
objects: {
|
|
712
|
+
[objectKey("lambda.aws.amazon.com/v1alpha1", "MicroVMReplicaSet", "kmv-dev-a-vm", "microvm-demo")]: replicaSet,
|
|
713
|
+
[objectKey("lambda.aws.amazon.com/v1alpha1", "MicroVM", "kmv-dev-a-vm-42sgf", "microvm-demo")]: vm,
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
const result = await describeResources(
|
|
718
|
+
{
|
|
719
|
+
environment: "dev",
|
|
720
|
+
buildOutput: "",
|
|
721
|
+
entityNames: ["workloadReplicaSet"],
|
|
722
|
+
entities: makeEntities([
|
|
723
|
+
{
|
|
724
|
+
name: "workloadReplicaSet",
|
|
725
|
+
entityType: "K8s::KubeMicroVM::MicroVMReplicaSet",
|
|
726
|
+
props: { metadata: { name: "kmv-dev-a-vm", namespace: "microvm-demo" } },
|
|
727
|
+
},
|
|
728
|
+
]),
|
|
729
|
+
},
|
|
730
|
+
cluster.connector,
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
expect(result.resources["microvm-demo/kmv-dev-a-vm-42sgf"]).toMatchObject({
|
|
734
|
+
type: "K8s::Lambda::MicroVM",
|
|
735
|
+
physicalId: "vm-uid",
|
|
736
|
+
ownerChain: { root: "declared", entity: "workloadReplicaSet" },
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("a child in a group the estate never declares surfaces too — discovery names the kinds, not the declaration", async () => {
|
|
741
|
+
// A declared CR whose operator makes ordinary Deployments: ray.io is
|
|
742
|
+
// declared, apps/v1 is not, and the estate-groups sweep never listed it.
|
|
743
|
+
const rayCluster = { apiVersion: "ray.io/v1", kind: "RayCluster", metadata: { name: "ml", namespace: "ray", uid: "ray-uid" } };
|
|
744
|
+
const head = {
|
|
745
|
+
apiVersion: "apps/v1",
|
|
746
|
+
kind: "Deployment",
|
|
747
|
+
metadata: {
|
|
748
|
+
name: "ml-head",
|
|
749
|
+
namespace: "ray",
|
|
750
|
+
uid: "head-uid",
|
|
751
|
+
ownerReferences: [{ apiVersion: "ray.io/v1", kind: "RayCluster", name: "ml", uid: "ray-uid", controller: true }],
|
|
752
|
+
},
|
|
753
|
+
status: { readyReplicas: 1, replicas: 1 },
|
|
754
|
+
};
|
|
755
|
+
const cluster = fakeCluster({
|
|
756
|
+
objects: {
|
|
757
|
+
[objectKey("ray.io/v1", "RayCluster", "ml", "ray")]: rayCluster,
|
|
758
|
+
[objectKey("apps/v1", "Deployment", "ml-head", "ray")]: head,
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
const result = await describeResources(
|
|
763
|
+
{
|
|
764
|
+
environment: "prod",
|
|
765
|
+
buildOutput: "",
|
|
766
|
+
entityNames: ["ml"],
|
|
767
|
+
entities: makeEntities([
|
|
768
|
+
{ name: "ml", entityType: "K8s::Ray::RayCluster", props: { metadata: { name: "ml", namespace: "ray" } } },
|
|
769
|
+
]),
|
|
770
|
+
},
|
|
771
|
+
cluster.connector,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
expect(result.resources["ray/ml-head"]).toMatchObject({
|
|
775
|
+
type: "K8s::Apps::Deployment",
|
|
776
|
+
physicalId: "head-uid",
|
|
777
|
+
ownerChain: { root: "declared", entity: "ml" },
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test("a discovered kind the sweep cannot list is an unobserved hole, never silently absent", async () => {
|
|
782
|
+
const cluster = fakeCluster({
|
|
783
|
+
objects: { [objectKey("batch/v1", "CronJob", "backup", "prod")]: cronJob },
|
|
784
|
+
respond: (req) =>
|
|
785
|
+
req.path === "/apis/batch/v1/namespaces/prod/jobs"
|
|
786
|
+
? { status: 403, body: statusBody(403, "Forbidden", "jobs is forbidden") }
|
|
787
|
+
: undefined,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const result = await describeResources(
|
|
791
|
+
{ environment: "prod", buildOutput: "", entityNames: ["backup"], entities: declaredCronJob() },
|
|
792
|
+
cluster.connector,
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
// Declared resolution is never failed by the sweep.
|
|
796
|
+
expect(result.resources.backup).toMatchObject({ type: "K8s::Batch::CronJob", physicalId: "cj-uid" });
|
|
797
|
+
// The failed list is a hole the observation admits, with the failure's
|
|
798
|
+
// own classification — a 403 is a credentials problem, and silence here
|
|
799
|
+
// would have read as "no runtime children in prod".
|
|
800
|
+
expect(result.unobserved?.["runtime-sweep:batch/v1/Job"]).toMatchObject({
|
|
801
|
+
type: "K8s::Batch::Job",
|
|
802
|
+
reason: "no-credentials",
|
|
803
|
+
});
|
|
804
|
+
expect(result.unobserved?.["runtime-sweep:batch/v1/Job"]?.detail).toContain("namespace prod");
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("a cluster-scoped child chains to a declared cluster-scoped entity — the one case cluster-scoped kinds are swept", async () => {
|
|
808
|
+
const ns = { apiVersion: "v1", kind: "Namespace", metadata: { name: "team-a", uid: "ns-uid" }, status: { phase: "Active" } };
|
|
809
|
+
const role = {
|
|
810
|
+
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
811
|
+
kind: "ClusterRole",
|
|
812
|
+
metadata: {
|
|
813
|
+
name: "team-a-admin",
|
|
814
|
+
uid: "role-uid",
|
|
815
|
+
ownerReferences: [{ apiVersion: "v1", kind: "Namespace", name: "team-a", uid: "ns-uid", controller: true }],
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
const cluster = fakeCluster({
|
|
819
|
+
objects: {
|
|
820
|
+
[objectKey("v1", "Namespace", "team-a")]: ns,
|
|
821
|
+
[objectKey("rbac.authorization.k8s.io/v1", "ClusterRole", "team-a-admin")]: role,
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
const result = await describeResources(
|
|
826
|
+
{
|
|
827
|
+
environment: "prod",
|
|
828
|
+
buildOutput: "",
|
|
829
|
+
entityNames: ["team"],
|
|
830
|
+
entities: makeEntities([
|
|
831
|
+
{ name: "team", entityType: "K8s::Core::Namespace", props: { metadata: { name: "team-a" } } },
|
|
832
|
+
]),
|
|
833
|
+
},
|
|
834
|
+
cluster.connector,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
expect(result.resources["cluster:team-a-admin"]).toMatchObject({
|
|
838
|
+
type: "K8s::Rbac::ClusterRole",
|
|
839
|
+
physicalId: "role-uid",
|
|
840
|
+
ownerChain: { root: "declared", entity: "team" },
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
576
844
|
test("a Flux-managed Deployment is attributed to its declared Kustomization by the controller's labels (#1549)", async () => {
|
|
577
845
|
// Flux stamps managed objects with name+namespace labels instead of
|
|
578
846
|
// ownerReferences, so the chain walk reaches nothing — the label
|
|
@@ -702,7 +970,7 @@ describe("k8s describeResources", () => {
|
|
|
702
970
|
expect(result.resources["flux-system/cert-manager"]).toBeUndefined();
|
|
703
971
|
});
|
|
704
972
|
|
|
705
|
-
test("
|
|
973
|
+
test("the sweep's bound is the estate's namespaces, not a kind list — and no cluster-scoped list without a cluster-scoped entity", async () => {
|
|
706
974
|
const svc = { apiVersion: "v1", kind: "Service", metadata: { name: "web", namespace: "prod", uid: "svc-uid" } };
|
|
707
975
|
const cluster = fakeCluster({
|
|
708
976
|
objects: { [objectKey("v1", "Service", "web", "prod")]: svc },
|
|
@@ -720,10 +988,20 @@ describe("k8s describeResources", () => {
|
|
|
720
988
|
cluster.connector,
|
|
721
989
|
);
|
|
722
990
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
991
|
+
const paths = cluster.layer.paths();
|
|
992
|
+
const isDiscovery = (p: string) =>
|
|
993
|
+
p === "/api" || p === "/apis" || /^\/api\/[^/]+$/.test(p) || /^\/apis\/[^/]+\/[^/]+$/.test(p);
|
|
994
|
+
const reads = paths.filter((p) => !isDiscovery(p));
|
|
995
|
+
// Discovery names the kinds; every actual read or list stays inside the
|
|
996
|
+
// one namespace the estate touches. Nothing is listed cluster-wide.
|
|
997
|
+
expect(reads.length).toBeGreaterThan(0);
|
|
998
|
+
expect(reads.every((p) => p.includes("/namespaces/prod/"))).toBe(true);
|
|
999
|
+
// Nothing declared is cluster-scoped, so no owner chain could reach a
|
|
1000
|
+
// cluster-scoped object — their kinds are not listed at all.
|
|
1001
|
+
expect(paths.some((p) => p.endsWith("/clusterroles"))).toBe(false);
|
|
1002
|
+
// Events churn by the thousands and can never chain (`involvedObject`,
|
|
1003
|
+
// not `ownerReferences`) — discovery serves them, the sweep skips them.
|
|
1004
|
+
expect(paths.some((p) => p.endsWith("/events"))).toBe(false);
|
|
727
1005
|
});
|
|
728
1006
|
});
|
|
729
1007
|
|
|
@@ -914,7 +1192,7 @@ describe("k8s describeResources", () => {
|
|
|
914
1192
|
expect(Object.keys(result.resources)).toHaveLength(2); // web + standalonePod, no synthetic duplicate
|
|
915
1193
|
});
|
|
916
1194
|
|
|
917
|
-
test("a Pod-list failure
|
|
1195
|
+
test("a Pod-list failure never fails declared resolution — and surfaces as an unobserved hole (#1517)", async () => {
|
|
918
1196
|
const cluster = fakeCluster({
|
|
919
1197
|
objects: { [objectKey("apps/v1", "Deployment", "web", "prod")]: web },
|
|
920
1198
|
respond: (req) => (req.path.endsWith("/pods") ? { status: 403, body: statusBody(403, "Forbidden", "no pods list") } : undefined),
|
|
@@ -927,6 +1205,12 @@ describe("k8s describeResources", () => {
|
|
|
927
1205
|
|
|
928
1206
|
expect(result.resources.web).toMatchObject({ type: "K8s::Apps::Deployment", physicalId: "uid-1" });
|
|
929
1207
|
expect(Object.keys(result.resources)).toEqual(["web"]);
|
|
1208
|
+
// Silence would read as "no runtime Pods in prod", which the failed
|
|
1209
|
+
// list never proved — the sweep admits the hole instead.
|
|
1210
|
+
expect(result.unobserved?.["runtime-sweep:v1/Pod"]).toMatchObject({
|
|
1211
|
+
type: "K8s::Core::Pod",
|
|
1212
|
+
reason: "no-credentials",
|
|
1213
|
+
});
|
|
930
1214
|
});
|
|
931
1215
|
|
|
932
1216
|
test("a lexicon-supported entity type with no namespaced entities resolved does no Pod scan at all", async () => {
|