@intentius/chant-lexicon-k8s 0.41.19 → 0.42.1
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/components/builders.d.ts +4 -0
- package/dist/components/builders.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/kustomize-apply.d.ts +48 -0
- package/dist/components/kustomize-apply.d.ts.map +1 -0
- package/dist/deep-observe-hooks.d.ts +8 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +250 -0
- package/dist/okf/rules/ARGO001.md +16 -0
- package/dist/okf/rules/ARGO002.md +16 -0
- package/dist/okf/rules/ARGO003.md +15 -0
- package/dist/okf/rules/ARGO004.md +17 -0
- package/dist/okf/rules/ARGO005.md +15 -0
- package/dist/okf/rules/WK8001.md +16 -0
- package/dist/okf/rules/WK8002.md +20 -0
- package/dist/okf/rules/WK8003.md +19 -0
- package/dist/okf/rules/WK8005.md +11 -0
- package/dist/okf/rules/WK8006.md +11 -0
- package/dist/okf/rules/WK8041.md +11 -0
- package/dist/okf/rules/WK8042.md +11 -0
- package/dist/okf/rules/WK8101.md +11 -0
- package/dist/okf/rules/WK8102.md +11 -0
- package/dist/okf/rules/WK8103.md +11 -0
- package/dist/okf/rules/WK8104.md +15 -0
- package/dist/okf/rules/WK8105.md +11 -0
- package/dist/okf/rules/WK8201.md +11 -0
- package/dist/okf/rules/WK8202.md +11 -0
- package/dist/okf/rules/WK8203.md +11 -0
- package/dist/okf/rules/WK8204.md +11 -0
- package/dist/okf/rules/WK8205.md +11 -0
- package/dist/okf/rules/WK8207.md +11 -0
- package/dist/okf/rules/WK8208.md +11 -0
- package/dist/okf/rules/WK8209.md +11 -0
- package/dist/okf/rules/WK8301.md +16 -0
- package/dist/okf/rules/WK8302.md +11 -0
- package/dist/okf/rules/WK8303.md +15 -0
- package/dist/okf/rules/WK8304.md +15 -0
- package/dist/okf/rules/WK8305.md +15 -0
- package/dist/okf/rules/WK8306.md +11 -0
- package/dist/okf/rules/WK8401.md +15 -0
- package/dist/okf/rules/WK8402.md +15 -0
- package/dist/okf/rules/WK8403.md +15 -0
- package/dist/okf/types/APIGroup.md +16 -0
- package/dist/okf/types/APIGroupList.md +13 -0
- package/dist/okf/types/APIResourceList.md +14 -0
- package/dist/okf/types/APIService.md +14 -0
- package/dist/okf/types/APIServiceList.md +14 -0
- package/dist/okf/types/APIVersions.md +14 -0
- package/dist/okf/types/Alert.md +14 -0
- package/dist/okf/types/AppProject.md +19 -0
- package/dist/okf/types/Application.md +23 -0
- package/dist/okf/types/ApplicationSet.md +18 -0
- package/dist/okf/types/Backup.md +14 -0
- package/dist/okf/types/BatchJob.md +14 -0
- package/dist/okf/types/Binding.md +14 -0
- package/dist/okf/types/Bucket.md +14 -0
- package/dist/okf/types/CM.md +16 -0
- package/dist/okf/types/CSIDriver.md +14 -0
- package/dist/okf/types/CSIDriverList.md +14 -0
- package/dist/okf/types/CSINode.md +14 -0
- package/dist/okf/types/CSINodeList.md +14 -0
- package/dist/okf/types/CSIStorageCapacity.md +17 -0
- package/dist/okf/types/CSIStorageCapacityList.md +14 -0
- package/dist/okf/types/Certificate.md +18 -0
- package/dist/okf/types/CertificateRequest.md +14 -0
- package/dist/okf/types/CertificateSigningRequest.md +14 -0
- package/dist/okf/types/CertificateSigningRequestList.md +14 -0
- package/dist/okf/types/Challenge.md +14 -0
- package/dist/okf/types/Cluster.md +14 -0
- package/dist/okf/types/ClusterIssuer.md +14 -0
- package/dist/okf/types/ClusterRole.md +15 -0
- package/dist/okf/types/ClusterRoleBinding.md +15 -0
- package/dist/okf/types/ClusterRoleBindingList.md +14 -0
- package/dist/okf/types/ClusterRoleList.md +14 -0
- package/dist/okf/types/ClusterTrustBundle.md +14 -0
- package/dist/okf/types/ClusterTrustBundleList.md +14 -0
- package/dist/okf/types/ComponentStatus.md +14 -0
- package/dist/okf/types/ComponentStatusList.md +14 -0
- package/dist/okf/types/ConfigMapList.md +14 -0
- package/dist/okf/types/ControllerRevision.md +15 -0
- package/dist/okf/types/ControllerRevisionList.md +14 -0
- package/dist/okf/types/CoreEventList.md +14 -0
- package/dist/okf/types/CrdbCluster.md +14 -0
- package/dist/okf/types/CronJob.md +20 -0
- package/dist/okf/types/CronJobList.md +14 -0
- package/dist/okf/types/CustomResourceDefinition.md +14 -0
- package/dist/okf/types/CustomResourceDefinitionList.md +14 -0
- package/dist/okf/types/DaemonSet.md +19 -0
- package/dist/okf/types/DaemonSetList.md +14 -0
- package/dist/okf/types/DeleteOptions.md +18 -0
- package/dist/okf/types/Deploy.md +14 -0
- package/dist/okf/types/DeploymentList.md +14 -0
- package/dist/okf/types/DeviceClass.md +14 -0
- package/dist/okf/types/DeviceClassList.md +14 -0
- package/dist/okf/types/DeviceTaintRule.md +14 -0
- package/dist/okf/types/DeviceTaintRuleList.md +14 -0
- package/dist/okf/types/EndpointSlice.md +16 -0
- package/dist/okf/types/EndpointSliceList.md +14 -0
- package/dist/okf/types/Endpoints.md +14 -0
- package/dist/okf/types/EndpointsList.md +14 -0
- package/dist/okf/types/Event.md +27 -0
- package/dist/okf/types/EventsEvent.md +27 -0
- package/dist/okf/types/EventsEventList.md +14 -0
- package/dist/okf/types/Eviction.md +14 -0
- package/dist/okf/types/FlowSchema.md +14 -0
- package/dist/okf/types/FlowSchemaList.md +14 -0
- package/dist/okf/types/FluxInstance.md +14 -0
- package/dist/okf/types/FluxReport.md +14 -0
- package/dist/okf/types/GRPCRoute.md +14 -0
- package/dist/okf/types/Gateway.md +14 -0
- package/dist/okf/types/GatewayClass.md +14 -0
- package/dist/okf/types/GitRepository.md +14 -0
- package/dist/okf/types/HTTPRoute.md +14 -0
- package/dist/okf/types/HelmChart.md +14 -0
- package/dist/okf/types/HelmRelease.md +14 -0
- package/dist/okf/types/HelmRepository.md +14 -0
- package/dist/okf/types/HorizontalPodAutoscaler.md +14 -0
- package/dist/okf/types/HorizontalPodAutoscalerList.md +14 -0
- package/dist/okf/types/IPAddress.md +14 -0
- package/dist/okf/types/IPAddressList.md +14 -0
- package/dist/okf/types/ImagePolicy.md +14 -0
- package/dist/okf/types/ImageRepository.md +14 -0
- package/dist/okf/types/ImageUpdateAutomation.md +14 -0
- package/dist/okf/types/InfisicalDynamicSecret.md +14 -0
- package/dist/okf/types/InfisicalPushSecret.md +14 -0
- package/dist/okf/types/InfisicalSecret.md +14 -0
- package/dist/okf/types/Ing.md +14 -0
- package/dist/okf/types/IngressClass.md +14 -0
- package/dist/okf/types/IngressClassList.md +14 -0
- package/dist/okf/types/IngressList.md +14 -0
- package/dist/okf/types/IngressRoute.md +14 -0
- package/dist/okf/types/IngressRouteTCP.md +14 -0
- package/dist/okf/types/IngressRouteUDP.md +14 -0
- package/dist/okf/types/Issuer.md +14 -0
- package/dist/okf/types/JobList.md +14 -0
- package/dist/okf/types/Kustomization.md +14 -0
- package/dist/okf/types/Lease.md +14 -0
- package/dist/okf/types/LeaseCandidate.md +14 -0
- package/dist/okf/types/LeaseCandidateList.md +14 -0
- package/dist/okf/types/LeaseList.md +14 -0
- package/dist/okf/types/LimitRange.md +14 -0
- package/dist/okf/types/LimitRangeList.md +14 -0
- package/dist/okf/types/LocalSubjectAccessReview.md +14 -0
- package/dist/okf/types/MicroVM.md +14 -0
- package/dist/okf/types/MicroVMClass.md +14 -0
- package/dist/okf/types/MicroVMImage.md +14 -0
- package/dist/okf/types/MicroVMNetwork.md +14 -0
- package/dist/okf/types/MicroVMReplicaSet.md +14 -0
- package/dist/okf/types/Middleware.md +14 -0
- package/dist/okf/types/MiddlewareTCP.md +14 -0
- package/dist/okf/types/MutatingAdmissionPolicy.md +14 -0
- package/dist/okf/types/MutatingAdmissionPolicyBinding.md +14 -0
- package/dist/okf/types/MutatingAdmissionPolicyBindingList.md +14 -0
- package/dist/okf/types/MutatingAdmissionPolicyList.md +14 -0
- package/dist/okf/types/MutatingWebhookConfiguration.md +14 -0
- package/dist/okf/types/MutatingWebhookConfigurationList.md +14 -0
- package/dist/okf/types/Namespace.md +18 -0
- package/dist/okf/types/NamespaceList.md +14 -0
- package/dist/okf/types/NetPol.md +14 -0
- package/dist/okf/types/NetworkPolicyList.md +14 -0
- package/dist/okf/types/Node.md +21 -0
- package/dist/okf/types/NodeList.md +14 -0
- package/dist/okf/types/OCIRepository.md +14 -0
- package/dist/okf/types/ObjectStore.md +14 -0
- package/dist/okf/types/Order.md +14 -0
- package/dist/okf/types/PDB.md +18 -0
- package/dist/okf/types/PersistentVolume.md +14 -0
- package/dist/okf/types/PersistentVolumeClaim.md +14 -0
- package/dist/okf/types/PersistentVolumeClaimList.md +14 -0
- package/dist/okf/types/PersistentVolumeList.md +14 -0
- package/dist/okf/types/Pod.md +18 -0
- package/dist/okf/types/PodCertificateRequest.md +14 -0
- package/dist/okf/types/PodCertificateRequestList.md +14 -0
- package/dist/okf/types/PodDisruptionBudgetList.md +14 -0
- package/dist/okf/types/PodList.md +14 -0
- package/dist/okf/types/PodTemplate.md +14 -0
- package/dist/okf/types/PodTemplateList.md +14 -0
- package/dist/okf/types/Pooler.md +14 -0
- package/dist/okf/types/PriorityClass.md +17 -0
- package/dist/okf/types/PriorityClassList.md +14 -0
- package/dist/okf/types/PriorityLevelConfiguration.md +14 -0
- package/dist/okf/types/PriorityLevelConfigurationList.md +14 -0
- package/dist/okf/types/PrometheusRule.md +14 -0
- package/dist/okf/types/Provider.md +14 -0
- package/dist/okf/types/RayCluster.md +20 -0
- package/dist/okf/types/RayJob.md +14 -0
- package/dist/okf/types/RayService.md +14 -0
- package/dist/okf/types/Receiver.md +14 -0
- package/dist/okf/types/ReferenceGrant.md +14 -0
- package/dist/okf/types/ReplicaSet.md +19 -0
- package/dist/okf/types/ReplicaSetList.md +14 -0
- package/dist/okf/types/ReplicationController.md +14 -0
- package/dist/okf/types/ReplicationControllerList.md +14 -0
- package/dist/okf/types/ResourceClaim.md +14 -0
- package/dist/okf/types/ResourceClaimList.md +14 -0
- package/dist/okf/types/ResourceClaimTemplate.md +14 -0
- package/dist/okf/types/ResourceClaimTemplateList.md +14 -0
- package/dist/okf/types/ResourceQuota.md +14 -0
- package/dist/okf/types/ResourceQuotaList.md +14 -0
- package/dist/okf/types/ResourceSet.md +14 -0
- package/dist/okf/types/ResourceSetInputProvider.md +14 -0
- package/dist/okf/types/ResourceSlice.md +14 -0
- package/dist/okf/types/ResourceSliceList.md +14 -0
- package/dist/okf/types/Role.md +14 -0
- package/dist/okf/types/RoleBinding.md +15 -0
- package/dist/okf/types/RoleBindingList.md +14 -0
- package/dist/okf/types/RoleList.md +14 -0
- package/dist/okf/types/RuntimeClass.md +16 -0
- package/dist/okf/types/RuntimeClassList.md +14 -0
- package/dist/okf/types/SA.md +16 -0
- package/dist/okf/types/Scale.md +14 -0
- package/dist/okf/types/ScheduledBackup.md +14 -0
- package/dist/okf/types/Sec.md +17 -0
- package/dist/okf/types/SecretList.md +14 -0
- package/dist/okf/types/SelfSubjectAccessReview.md +14 -0
- package/dist/okf/types/SelfSubjectReview.md +13 -0
- package/dist/okf/types/SelfSubjectRulesReview.md +14 -0
- package/dist/okf/types/ServersTransport.md +14 -0
- package/dist/okf/types/ServersTransportTCP.md +14 -0
- package/dist/okf/types/Service.md +20 -0
- package/dist/okf/types/ServiceAccountList.md +14 -0
- package/dist/okf/types/ServiceCIDR.md +14 -0
- package/dist/okf/types/ServiceCIDRList.md +14 -0
- package/dist/okf/types/ServiceList.md +14 -0
- package/dist/okf/types/ServiceMonitor.md +14 -0
- package/dist/okf/types/StatefulSet.md +19 -0
- package/dist/okf/types/StatefulSetList.md +14 -0
- package/dist/okf/types/Status.md +17 -0
- package/dist/okf/types/StorageClass.md +20 -0
- package/dist/okf/types/StorageClassList.md +14 -0
- package/dist/okf/types/StorageVersionMigration.md +14 -0
- package/dist/okf/types/StorageVersionMigrationList.md +14 -0
- package/dist/okf/types/SubjectAccessReview.md +14 -0
- package/dist/okf/types/TLSOption.md +14 -0
- package/dist/okf/types/TLSStore.md +14 -0
- package/dist/okf/types/TokenRequest.md +14 -0
- package/dist/okf/types/TokenReview.md +14 -0
- package/dist/okf/types/TraefikService.md +14 -0
- package/dist/okf/types/ValidatingAdmissionPolicy.md +14 -0
- package/dist/okf/types/ValidatingAdmissionPolicyBinding.md +14 -0
- package/dist/okf/types/ValidatingAdmissionPolicyBindingList.md +14 -0
- package/dist/okf/types/ValidatingAdmissionPolicyList.md +14 -0
- package/dist/okf/types/ValidatingWebhookConfiguration.md +14 -0
- package/dist/okf/types/ValidatingWebhookConfigurationList.md +14 -0
- package/dist/okf/types/VolumeAttachment.md +14 -0
- package/dist/okf/types/VolumeAttachmentList.md +14 -0
- package/dist/okf/types/VolumeAttributesClass.md +15 -0
- package/dist/okf/types/VolumeAttributesClassList.md +14 -0
- package/dist/okf/types/WatchEvent.md +14 -0
- package/dist/op/activities/kubectl.d.ts +15 -1
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/wait-for-ready.d.ts +16 -6
- package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/builders.ts +5 -0
- package/src/components/capability-plugin.ts +3 -2
- package/src/components/index.ts +7 -0
- package/src/components/kustomize-apply.test.ts +98 -0
- package/src/components/kustomize-apply.ts +133 -0
- package/src/deep-observe-hooks.ts +60 -0
- package/src/deep-observe.test.ts +114 -0
- package/src/describe-resources.test.ts +159 -0
- package/src/describe-resources.ts +142 -2
- package/src/index.ts +1 -0
- package/src/op/activities/kubectl.test.ts +22 -0
- package/src/op/activities/kubectl.ts +15 -2
- package/src/op/activities/wait-for-ready.test.ts +27 -0
- package/src/op/activities/wait-for-ready.ts +84 -6
package/src/components/index.ts
CHANGED
|
@@ -19,3 +19,10 @@ export {
|
|
|
19
19
|
type KubectlApplyInput,
|
|
20
20
|
type KubectlApplyOutcome,
|
|
21
21
|
} from "./kubectl-apply";
|
|
22
|
+
export {
|
|
23
|
+
kustomizeApplyCapability,
|
|
24
|
+
createKustomizeApplyCapability,
|
|
25
|
+
renderCommand,
|
|
26
|
+
type KustomizeApplyInput,
|
|
27
|
+
type KustomizeApplyOutcome,
|
|
28
|
+
} from "./kustomize-apply";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kustomize-apply` (#1548) — the render-then-apply leaf. What must hold:
|
|
3
|
+
* the capability registers under its kind with the lexicon's own version
|
|
4
|
+
* (#1505), the render command is exact and falls back to kubectl's vendored
|
|
5
|
+
* kustomize, the rendered documents reach the applier INLINE (no temp file)
|
|
6
|
+
* with the deploy-unit stack, and the rollback posture matches kubectl-apply
|
|
7
|
+
* (server-side apply has no native undo).
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect } from "vitest";
|
|
10
|
+
import { createKustomizeApplyCapability, kustomizeApplyCapability, renderCommand, type KustomizeApplyInput } from "./kustomize-apply";
|
|
11
|
+
import { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin";
|
|
12
|
+
import { isCapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
13
|
+
import type { DeployContext } from "@intentius/chant/components/capability";
|
|
14
|
+
|
|
15
|
+
const ctx = { env: "dev" } as DeployContext;
|
|
16
|
+
|
|
17
|
+
const RENDERED = `apiVersion: v1
|
|
18
|
+
kind: Namespace
|
|
19
|
+
metadata:
|
|
20
|
+
name: web
|
|
21
|
+
---
|
|
22
|
+
apiVersion: apps/v1
|
|
23
|
+
kind: Deployment
|
|
24
|
+
metadata:
|
|
25
|
+
name: app
|
|
26
|
+
namespace: web
|
|
27
|
+
spec:
|
|
28
|
+
replicas: 1
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
describe("kustomize-apply capability (#1548)", () => {
|
|
32
|
+
test("registers on the k8s plugin, in the apply family, at the package's own version", () => {
|
|
33
|
+
expect(isCapabilityPlugin(k8sCapabilityPlugin)).toBe(true);
|
|
34
|
+
expect(k8sCapabilityPlugin.capabilities().map((c) => c.kind)).toContain("kustomize-apply");
|
|
35
|
+
expect(K8S_VERB_FAMILIES.apply).toContain("kustomize-apply");
|
|
36
|
+
// #1505 — never a stale literal.
|
|
37
|
+
const { version } = JSON.parse(
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
39
|
+
require("node:fs").readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
|
|
40
|
+
) as { version: string };
|
|
41
|
+
expect(k8sCapabilityPlugin.version).toBe(version);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("rollback is needs-opt-out — a server-side apply keeps no previous state", () => {
|
|
45
|
+
expect(kustomizeApplyCapability.rollbackPolicy).toBe("needs-opt-out");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("renders with kustomize build and hands the parsed documents to the applier inline", async () => {
|
|
49
|
+
const commands: string[] = [];
|
|
50
|
+
const applierArgs: unknown[] = [];
|
|
51
|
+
const cap = createKustomizeApplyCapability(
|
|
52
|
+
async (command) => {
|
|
53
|
+
commands.push(command);
|
|
54
|
+
return { stdout: RENDERED };
|
|
55
|
+
},
|
|
56
|
+
async (args) => {
|
|
57
|
+
applierArgs.push(args);
|
|
58
|
+
return { fieldManager: "chant:web", applied: [], pruned: [] };
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
await cap.run(ctx, { dir: "overlays/dev", stack: "web", delete: "owned-only" } as KustomizeApplyInput as never);
|
|
63
|
+
|
|
64
|
+
expect(commands).toEqual(["kustomize build 'overlays/dev'"]);
|
|
65
|
+
expect(applierArgs).toHaveLength(1);
|
|
66
|
+
const args = applierArgs[0] as { manifest: string; documents: unknown[]; environment: string; stack: string; deleteMode: string };
|
|
67
|
+
expect(args.manifest).toBe("kustomize:overlays/dev"); // a label, not a path
|
|
68
|
+
expect(args.documents).toHaveLength(2);
|
|
69
|
+
expect((args.documents[1] as { kind: string }).kind).toBe("Deployment");
|
|
70
|
+
expect(args.environment).toBe("dev");
|
|
71
|
+
expect(args.stack).toBe("web");
|
|
72
|
+
expect(args.deleteMode).toBe("owned-only");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("falls back to kubectl's vendored kustomize when the binary is missing", async () => {
|
|
76
|
+
const commands: string[] = [];
|
|
77
|
+
const cap = createKustomizeApplyCapability(
|
|
78
|
+
async (command) => {
|
|
79
|
+
commands.push(command);
|
|
80
|
+
if (command.startsWith("kustomize ")) throw new Error("spawn kustomize ENOENT");
|
|
81
|
+
return { stdout: RENDERED };
|
|
82
|
+
},
|
|
83
|
+
async () => ({ fieldManager: "chant", applied: [], pruned: [] }),
|
|
84
|
+
);
|
|
85
|
+
await cap.run(ctx, { dir: "overlays/dev" } as KustomizeApplyInput as never);
|
|
86
|
+
expect(commands).toEqual([renderCommand("overlays/dev", "kustomize"), renderCommand("overlays/dev", "kubectl")]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a render failure that is NOT a missing binary propagates — a broken overlay is not silently retried", async () => {
|
|
90
|
+
const cap = createKustomizeApplyCapability(
|
|
91
|
+
async () => {
|
|
92
|
+
throw new Error("accumulating resources: missing base ../common");
|
|
93
|
+
},
|
|
94
|
+
async () => ({ fieldManager: "chant", applied: [], pruned: [] }),
|
|
95
|
+
);
|
|
96
|
+
await expect(cap.run(ctx, { dir: "overlays/dev" } as KustomizeApplyInput as never)).rejects.toThrow("missing base");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kustomize-apply` — the kustomize leaf for the component model (#1548).
|
|
3
|
+
*
|
|
4
|
+
* Kustomize is a RENDERER, not a substrate: `kustomize build <dir>` emits
|
|
5
|
+
* exactly the Kubernetes manifests the existing apply pipeline was built for.
|
|
6
|
+
* So this capability is a render step in front of `kubectl-apply`'s machinery
|
|
7
|
+
* — the rendered documents go straight to the same server-side applier
|
|
8
|
+
* (inline, no temp file), which stamps the deploy-unit stack (#1539), prunes
|
|
9
|
+
* marker-scoped, retakes chant's own field conflicts (#1541/#1542), and is
|
|
10
|
+
* observed by the same `describeStackStatus` label sweep. An estate that
|
|
11
|
+
* keeps its overlay tree gets components-status honesty without adopting a
|
|
12
|
+
* single typed manifest.
|
|
13
|
+
*
|
|
14
|
+
* `stack` doubles as the ownership identity and the deploy unit, exactly as
|
|
15
|
+
* `kubectl-apply` — one name, both jobs.
|
|
16
|
+
*
|
|
17
|
+
* Renderer resolution: `kustomize build`, falling back to
|
|
18
|
+
* `kubectl kustomize` (the same renderer vendored into kubectl) when the
|
|
19
|
+
* standalone binary is absent. Both run through an injectable runner so tests
|
|
20
|
+
* assert the exact command without either binary installed. The 64MiB
|
|
21
|
+
* maxBuffer mirrors helm-upgrade's — a big overlay renders megabytes.
|
|
22
|
+
*
|
|
23
|
+
* The #1074 boundary applies as it does to kubectl-apply: nothing exported
|
|
24
|
+
* from the lexicon entry point may statically import the API-client chain, so
|
|
25
|
+
* the applier is reached by dynamic import inside `run()` and its argument
|
|
26
|
+
* shape is a structural mirror.
|
|
27
|
+
*/
|
|
28
|
+
import { exec } from "node:child_process";
|
|
29
|
+
import { promisify } from "node:util";
|
|
30
|
+
import { loadAll } from "js-yaml";
|
|
31
|
+
import type { Capability, DeployContext } from "@intentius/chant/components/capability";
|
|
32
|
+
|
|
33
|
+
const execAsync = promisify(exec);
|
|
34
|
+
|
|
35
|
+
/** Injectable shell runner — the helm-upgrade seam, same buffer bound. */
|
|
36
|
+
export type KustomizeRunner = (command: string) => Promise<{ stdout: string }>;
|
|
37
|
+
const defaultRunner: KustomizeRunner = (command) => execAsync(command, { maxBuffer: 64 * 1024 * 1024 });
|
|
38
|
+
|
|
39
|
+
/** Single-quote shell escaping, as helm-upgrade quotes its argv. */
|
|
40
|
+
function q(v: string): string {
|
|
41
|
+
return `'${v.replace(/'/g, "'\\''")}'`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Structural mirrors of the activity module's shapes (see the module doc). */
|
|
45
|
+
interface AppliedRef {
|
|
46
|
+
apiVersion: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
name: string;
|
|
49
|
+
namespace?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface KustomizeApplyOutcome {
|
|
53
|
+
fieldManager: string;
|
|
54
|
+
applied: AppliedRef[];
|
|
55
|
+
pruned: AppliedRef[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ApplierArgs {
|
|
59
|
+
manifest: string;
|
|
60
|
+
documents?: Array<Record<string, unknown>>;
|
|
61
|
+
environment?: string;
|
|
62
|
+
stack?: string;
|
|
63
|
+
context?: string;
|
|
64
|
+
deleteMode?: "never" | "owned-only" | "gated";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Applier = (args: ApplierArgs) => Promise<KustomizeApplyOutcome>;
|
|
68
|
+
|
|
69
|
+
export interface KustomizeApplyInput {
|
|
70
|
+
/** The kustomization directory (holds `kustomization.yaml`) — an overlay or a base. */
|
|
71
|
+
dir: string;
|
|
72
|
+
/**
|
|
73
|
+
* The deploy unit / ownership stack — same double duty as kubectl-apply's.
|
|
74
|
+
* Omitted derives from the project's `ownership.stack`, but then the unit is
|
|
75
|
+
* invisible to `components status --live`. Name it.
|
|
76
|
+
*/
|
|
77
|
+
stack?: string;
|
|
78
|
+
/** kubectl context. Omitted resolves `k8s.profiles.<ctx.env>.context`. */
|
|
79
|
+
context?: string;
|
|
80
|
+
/** Same delete vocabulary as kubectl-apply; default `never`. */
|
|
81
|
+
delete?: "never" | "owned-only";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The exact render command, standalone binary first. Pure; exported for tests. */
|
|
85
|
+
export function renderCommand(dir: string, tool: "kustomize" | "kubectl" = "kustomize"): string {
|
|
86
|
+
return tool === "kustomize" ? `kustomize build ${q(dir)}` : `kubectl kustomize ${q(dir)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Render the overlay, falling back to kubectl's vendored kustomize when the
|
|
90
|
+
* standalone binary is missing (ENOENT/127), and parse the emitted YAML. */
|
|
91
|
+
async function renderDocuments(dir: string, run: KustomizeRunner): Promise<Array<Record<string, unknown>>> {
|
|
92
|
+
let stdout: string;
|
|
93
|
+
try {
|
|
94
|
+
({ stdout } = await run(renderCommand(dir, "kustomize")));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
if (!/ENOENT|not found|command not found|127/.test(message)) throw err;
|
|
98
|
+
({ stdout } = await run(renderCommand(dir, "kubectl")));
|
|
99
|
+
}
|
|
100
|
+
const documents: Array<Record<string, unknown>> = [];
|
|
101
|
+
for (const doc of loadAll(stdout)) {
|
|
102
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc)) documents.push(doc as Record<string, unknown>);
|
|
103
|
+
}
|
|
104
|
+
return documents;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Factory with injectable renderer + applier, so tests assert the render
|
|
108
|
+
* command and the exact applier delegation without kustomize or a cluster. */
|
|
109
|
+
export function createKustomizeApplyCapability(
|
|
110
|
+
run: KustomizeRunner = defaultRunner,
|
|
111
|
+
apply?: Applier,
|
|
112
|
+
): Capability<KustomizeApplyInput, KustomizeApplyOutcome> {
|
|
113
|
+
return {
|
|
114
|
+
kind: "kustomize-apply",
|
|
115
|
+
// Server-side apply keeps no previous object state, and kustomize adds no
|
|
116
|
+
// undo of its own — same posture as kubectl-apply.
|
|
117
|
+
rollbackPolicy: "needs-opt-out",
|
|
118
|
+
async run(ctx: DeployContext, input: KustomizeApplyInput): Promise<KustomizeApplyOutcome> {
|
|
119
|
+
const documents = await renderDocuments(input.dir, run);
|
|
120
|
+
const applier: Applier = apply ?? ((await import("../op/activities/kubectl")).applyManifest as unknown as Applier);
|
|
121
|
+
return applier({
|
|
122
|
+
manifest: `kustomize:${input.dir}`,
|
|
123
|
+
documents,
|
|
124
|
+
environment: ctx.env,
|
|
125
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
126
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
127
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const kustomizeApplyCapability = createKustomizeApplyCapability();
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
import { createRequire } from "module";
|
|
45
45
|
import type { DeepArrayElement, DeepNode, DeepNormalizationHooks } from "@intentius/chant/lexicon";
|
|
46
46
|
import { K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS, k8sListMapOrderKey } from "@intentius/chant/managed-fields";
|
|
47
|
+
import { LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
|
|
47
48
|
|
|
48
49
|
// This module ships as ESM (tsx strips types from src/ directly), where a bare
|
|
49
50
|
// `require` is not defined — the same reason serializer.ts and the LSP modules
|
|
@@ -86,13 +87,64 @@ export const K8S_SERVICE_DEFAULTS: Record<string, Record<string, unknown>> = {
|
|
|
86
87
|
"spec.template.spec.restartPolicy": "Always",
|
|
87
88
|
"spec.template.spec.terminationGracePeriodSeconds": 30,
|
|
88
89
|
"spec.template.spec.schedulerName": "default-scheduler",
|
|
90
|
+
// Widened on the CC canonical estate (#1214): a container declaring an
|
|
91
|
+
// httpGet probe, a named port, or nothing about termination gets exactly
|
|
92
|
+
// these from the API server. SSA attributes a server-defaulted field to no
|
|
93
|
+
// manager, so the managed-fields prune deliberately leaves them diffable
|
|
94
|
+
// (see ./deep-observe.ts) — this table is where they subtract.
|
|
95
|
+
"spec.template.spec.containers[].livenessProbe.failureThreshold": 3,
|
|
96
|
+
"spec.template.spec.containers[].livenessProbe.periodSeconds": 10,
|
|
97
|
+
"spec.template.spec.containers[].livenessProbe.successThreshold": 1,
|
|
98
|
+
"spec.template.spec.containers[].livenessProbe.timeoutSeconds": 1,
|
|
99
|
+
"spec.template.spec.containers[].livenessProbe.httpGet.scheme": "HTTP",
|
|
100
|
+
"spec.template.spec.containers[].readinessProbe.failureThreshold": 3,
|
|
101
|
+
"spec.template.spec.containers[].readinessProbe.periodSeconds": 10,
|
|
102
|
+
"spec.template.spec.containers[].readinessProbe.successThreshold": 1,
|
|
103
|
+
"spec.template.spec.containers[].readinessProbe.timeoutSeconds": 1,
|
|
104
|
+
"spec.template.spec.containers[].readinessProbe.httpGet.scheme": "HTTP",
|
|
105
|
+
"spec.template.spec.containers[].ports[].protocol": "TCP",
|
|
106
|
+
"spec.template.spec.containers[].terminationMessagePath": "/dev/termination-log",
|
|
107
|
+
"spec.template.spec.containers[].terminationMessagePolicy": "File",
|
|
108
|
+
"spec.template.spec.securityContext": {},
|
|
89
109
|
},
|
|
90
110
|
"K8s::Core::Service": {
|
|
91
111
|
"spec.sessionAffinity": "None",
|
|
92
112
|
"spec.type": "ClusterIP",
|
|
113
|
+
"spec.internalTrafficPolicy": "Cluster",
|
|
114
|
+
"spec.ipFamilyPolicy": "SingleStack",
|
|
115
|
+
"spec.ports[].protocol": "TCP",
|
|
93
116
|
},
|
|
94
117
|
};
|
|
95
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Server-ASSIGNED fields — populated by the API server with a value that
|
|
121
|
+
* varies per object (an allocated ClusterIP, the resolved IP family list), so
|
|
122
|
+
* unlike {@link K8S_SERVICE_DEFAULTS} there is no fixed value to compare
|
|
123
|
+
* against. Counterpart-gated like the defaults table: a declared `clusterIP`
|
|
124
|
+
* is still compared, only the purely server-filled appearance is noise.
|
|
125
|
+
*/
|
|
126
|
+
export const K8S_SERVER_ASSIGNED_PATTERNS: Record<string, ReadonlySet<string>> = {
|
|
127
|
+
"K8s::Core::Service": new Set(["spec.clusterIP", "spec.clusterIPs", "spec.ipFamilies"]),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* chant's own ownership marker, as label paths (#1214 — the k8s edition of
|
|
132
|
+
* azure's #1213 rule). The build stamps `app.kubernetes.io/managed-by` +
|
|
133
|
+
* `chant.intentius.io/{stack,env}` into every manifest it emits, so a managed
|
|
134
|
+
* object always carries them live while the declared *source* the diff
|
|
135
|
+
* compares does not — chant reading its own signature back as drift. The
|
|
136
|
+
* managed-fields prune cannot subtract them: they are in the applied config,
|
|
137
|
+
* so chant's field manager owns them, and chant-owned is always diffable.
|
|
138
|
+
* Counterpart-gated at the call site; managed-by is additionally gated on the
|
|
139
|
+
* value `"chant"`, so `managed-by: helm` appearing out of band still surfaces.
|
|
140
|
+
*/
|
|
141
|
+
const K8S_OWNERSHIP_LABEL_PATTERNS: ReadonlySet<string> = new Set([
|
|
142
|
+
`metadata.labels.${LABEL_OWNERSHIP_KEYS.stack}`,
|
|
143
|
+
`metadata.labels.${LABEL_OWNERSHIP_KEYS.env}`,
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
const K8S_MANAGED_BY_LABEL_PATTERN = `metadata.labels.${LABEL_OWNERSHIP_KEYS.managedBy}`;
|
|
147
|
+
|
|
96
148
|
/** Stable JSON with sorted keys — the fallback ordering key for a set-like array without a natural identity field. */
|
|
97
149
|
function canonicalJson(value: unknown): string {
|
|
98
150
|
return (
|
|
@@ -206,6 +258,14 @@ export const k8sDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
|
206
258
|
if (K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS.has(node.pattern)) return true;
|
|
207
259
|
|
|
208
260
|
if (node.side !== "live" || node.counterpart !== "absent") return false;
|
|
261
|
+
|
|
262
|
+
// chant's own ownership marker is not drift (see the sets' docs).
|
|
263
|
+
if (K8S_OWNERSHIP_LABEL_PATTERNS.has(node.pattern)) return true;
|
|
264
|
+
if (node.pattern === K8S_MANAGED_BY_LABEL_PATTERN && node.value === OWNERSHIP_MANAGED_BY_VALUE) return true;
|
|
265
|
+
|
|
266
|
+
// A server-assigned value nobody declared is allocation, not drift.
|
|
267
|
+
if (K8S_SERVER_ASSIGNED_PATTERNS[node.entityType]?.has(node.pattern)) return true;
|
|
268
|
+
|
|
209
269
|
const defaults = K8S_SERVICE_DEFAULTS[node.entityType];
|
|
210
270
|
if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, node.pattern)) return false;
|
|
211
271
|
return canonicalJson(defaults[node.pattern]) === canonicalJson(node.value);
|
package/src/deep-observe.test.ts
CHANGED
|
@@ -96,6 +96,120 @@ describe("k8sDeepNormalizationHooks — the static rules", () => {
|
|
|
96
96
|
expect(declaredExplicit).toEqual({ spec: { strategy: strategyDefault, replicas: 3 } });
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
test("chant's own stamped labels are not drift; a foreign managed-by and a user label still are", () => {
|
|
100
|
+
const live = {
|
|
101
|
+
metadata: {
|
|
102
|
+
name: "web",
|
|
103
|
+
labels: {
|
|
104
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
105
|
+
"chant.intentius.io/stack": "cc-azure-canonical",
|
|
106
|
+
"chant.intentius.io/env": "local",
|
|
107
|
+
team: "platform",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const out = normalizeDeepProperties(live, {
|
|
112
|
+
entityType: "K8s::Apps::Deployment",
|
|
113
|
+
side: "live",
|
|
114
|
+
hooks: k8sDeepNormalizationHooks,
|
|
115
|
+
counterpartPaths: new Set(["metadata", "metadata.name"]),
|
|
116
|
+
});
|
|
117
|
+
expect(out).toEqual({ metadata: { name: "web", labels: { team: "platform" } } });
|
|
118
|
+
|
|
119
|
+
const foreign = normalizeDeepProperties(
|
|
120
|
+
{ metadata: { name: "web", labels: { "app.kubernetes.io/managed-by": "helm" } } },
|
|
121
|
+
{
|
|
122
|
+
entityType: "K8s::Apps::Deployment",
|
|
123
|
+
side: "live",
|
|
124
|
+
hooks: k8sDeepNormalizationHooks,
|
|
125
|
+
counterpartPaths: new Set(["metadata", "metadata.name"]),
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
expect(foreign).toEqual({ metadata: { name: "web", labels: { "app.kubernetes.io/managed-by": "helm" } } });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a server-assigned ClusterIP nobody declared subtracts; a declared one is compared", () => {
|
|
132
|
+
const undeclared = normalizeDeepProperties(
|
|
133
|
+
{ spec: { clusterIP: "10.43.201.161", clusterIPs: ["10.43.201.161"], ipFamilies: ["IPv4"], selector: { app: "web" } } },
|
|
134
|
+
{
|
|
135
|
+
entityType: "K8s::Core::Service",
|
|
136
|
+
side: "live",
|
|
137
|
+
hooks: k8sDeepNormalizationHooks,
|
|
138
|
+
counterpartPaths: new Set(["spec", "spec.selector", "spec.selector.app"]),
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
expect(undeclared).toEqual({ spec: { selector: { app: "web" } } });
|
|
142
|
+
|
|
143
|
+
const declared = normalizeDeepProperties(
|
|
144
|
+
{ spec: { clusterIP: "10.43.0.10" } },
|
|
145
|
+
{
|
|
146
|
+
entityType: "K8s::Core::Service",
|
|
147
|
+
side: "live",
|
|
148
|
+
hooks: k8sDeepNormalizationHooks,
|
|
149
|
+
counterpartPaths: new Set(["spec", "spec.clusterIP"]),
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
expect(declared).toEqual({ spec: { clusterIP: "10.43.0.10" } });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("probe and termination defaults subtract inside a declared container", () => {
|
|
156
|
+
const out = normalizeDeepProperties(
|
|
157
|
+
{
|
|
158
|
+
spec: {
|
|
159
|
+
template: {
|
|
160
|
+
spec: {
|
|
161
|
+
securityContext: {},
|
|
162
|
+
containers: [
|
|
163
|
+
{
|
|
164
|
+
name: "api",
|
|
165
|
+
image: "example:1",
|
|
166
|
+
terminationMessagePath: "/dev/termination-log",
|
|
167
|
+
terminationMessagePolicy: "File",
|
|
168
|
+
livenessProbe: { httpGet: { path: "/", port: 8080, scheme: "HTTP" }, failureThreshold: 3, periodSeconds: 10, successThreshold: 1, timeoutSeconds: 1 },
|
|
169
|
+
ports: [{ containerPort: 8080, name: "http", protocol: "TCP" }],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
entityType: "K8s::Apps::Deployment",
|
|
178
|
+
side: "live",
|
|
179
|
+
hooks: k8sDeepNormalizationHooks,
|
|
180
|
+
counterpartPaths: new Set([
|
|
181
|
+
"spec", "spec.template", "spec.template.spec",
|
|
182
|
+
"spec.template.spec.containers", "spec.template.spec.containers[]",
|
|
183
|
+
"spec.template.spec.containers[].name", "spec.template.spec.containers[].image",
|
|
184
|
+
"spec.template.spec.containers[].livenessProbe",
|
|
185
|
+
"spec.template.spec.containers[].livenessProbe.httpGet",
|
|
186
|
+
"spec.template.spec.containers[].livenessProbe.httpGet.path",
|
|
187
|
+
"spec.template.spec.containers[].livenessProbe.httpGet.port",
|
|
188
|
+
"spec.template.spec.containers[].ports",
|
|
189
|
+
"spec.template.spec.containers[].ports[]",
|
|
190
|
+
"spec.template.spec.containers[].ports[].containerPort",
|
|
191
|
+
"spec.template.spec.containers[].ports[].name",
|
|
192
|
+
]),
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
expect(out).toEqual({
|
|
196
|
+
spec: {
|
|
197
|
+
template: {
|
|
198
|
+
spec: {
|
|
199
|
+
containers: [
|
|
200
|
+
{
|
|
201
|
+
name: "api",
|
|
202
|
+
image: "example:1",
|
|
203
|
+
livenessProbe: { httpGet: { path: "/", port: 8080 } },
|
|
204
|
+
ports: [{ containerPort: 8080, name: "http" }],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
99
213
|
test("a one-sided pass never subtracts defaults — the reader has no declared tree yet", () => {
|
|
100
214
|
const out = normalizeDeepProperties(
|
|
101
215
|
{ spec: { strategy: { type: "RollingUpdate", rollingUpdate: { maxSurge: "25%", maxUnavailable: "25%" } } } },
|
|
@@ -573,6 +573,135 @@ describe("k8s describeResources", () => {
|
|
|
573
573
|
expect(result.resources["prod/hand-made"]).toBeUndefined();
|
|
574
574
|
});
|
|
575
575
|
|
|
576
|
+
test("a Flux-managed Deployment is attributed to its declared Kustomization by the controller's labels (#1549)", async () => {
|
|
577
|
+
// Flux stamps managed objects with name+namespace labels instead of
|
|
578
|
+
// ownerReferences, so the chain walk reaches nothing — the label
|
|
579
|
+
// channel resolves against the DECLARED CR, exactly. The workload also
|
|
580
|
+
// lives in a namespace the estate never declares an entity in: the
|
|
581
|
+
// Kustomization's own spec.targetNamespace is what puts it in scope.
|
|
582
|
+
const kustomization = {
|
|
583
|
+
apiVersion: "kustomize.toolkit.fluxcd.io/v1",
|
|
584
|
+
kind: "Kustomization",
|
|
585
|
+
metadata: { name: "apps", namespace: "flux-system", uid: "ks-uid" },
|
|
586
|
+
status: { conditions: [{ type: "Ready", status: "True" }] },
|
|
587
|
+
};
|
|
588
|
+
const managed = {
|
|
589
|
+
apiVersion: "apps/v1",
|
|
590
|
+
kind: "Deployment",
|
|
591
|
+
metadata: {
|
|
592
|
+
name: "web",
|
|
593
|
+
namespace: "prod",
|
|
594
|
+
uid: "dep-uid",
|
|
595
|
+
labels: { "kustomize.toolkit.fluxcd.io/name": "apps", "kustomize.toolkit.fluxcd.io/namespace": "flux-system" },
|
|
596
|
+
},
|
|
597
|
+
status: { readyReplicas: 1, replicas: 1 },
|
|
598
|
+
};
|
|
599
|
+
const cluster = fakeCluster({
|
|
600
|
+
objects: {
|
|
601
|
+
[objectKey("kustomize.toolkit.fluxcd.io/v1", "Kustomization", "apps", "flux-system")]: kustomization,
|
|
602
|
+
[objectKey("apps/v1", "Deployment", "web", "prod")]: managed,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const result = await describeResources(
|
|
607
|
+
{
|
|
608
|
+
environment: "prod",
|
|
609
|
+
buildOutput: "",
|
|
610
|
+
entityNames: ["apps"],
|
|
611
|
+
entities: makeEntities([
|
|
612
|
+
{
|
|
613
|
+
name: "apps",
|
|
614
|
+
entityType: "K8s::Flux::Kustomization",
|
|
615
|
+
props: { metadata: { name: "apps", namespace: "flux-system" }, spec: { targetNamespace: "prod", sourceRef: { kind: "GitRepository", name: "infra" } } },
|
|
616
|
+
},
|
|
617
|
+
]),
|
|
618
|
+
},
|
|
619
|
+
cluster.connector,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
expect(result.resources["prod/web"]).toMatchObject({
|
|
623
|
+
type: "K8s::Apps::Deployment",
|
|
624
|
+
ownerChain: { root: "declared", entity: "apps" },
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("Argo's bare instance label claims only a UNIQUE declared Application (#1549)", async () => {
|
|
629
|
+
const app = (name: string) => ({
|
|
630
|
+
apiVersion: "argoproj.io/v1alpha1",
|
|
631
|
+
kind: "Application",
|
|
632
|
+
metadata: { name, namespace: "argocd", uid: `${name}-uid` },
|
|
633
|
+
status: { health: { status: "Healthy" }, sync: { status: "Synced" } },
|
|
634
|
+
});
|
|
635
|
+
const managed = {
|
|
636
|
+
apiVersion: "apps/v1",
|
|
637
|
+
kind: "Deployment",
|
|
638
|
+
metadata: { name: "web", namespace: "prod", uid: "dep-uid", labels: { "app.kubernetes.io/instance": "web" } },
|
|
639
|
+
};
|
|
640
|
+
const cluster = fakeCluster({
|
|
641
|
+
objects: {
|
|
642
|
+
[objectKey("argoproj.io/v1alpha1", "Application", "web", "argocd")]: app("web"),
|
|
643
|
+
[objectKey("apps/v1", "Deployment", "web", "prod")]: managed,
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const result = await describeResources(
|
|
648
|
+
{
|
|
649
|
+
environment: "prod",
|
|
650
|
+
buildOutput: "",
|
|
651
|
+
entityNames: ["webApp"],
|
|
652
|
+
entities: makeEntities([
|
|
653
|
+
{
|
|
654
|
+
name: "webApp",
|
|
655
|
+
entityType: "K8s::Argo::Application",
|
|
656
|
+
props: { metadata: { name: "web", namespace: "argocd" }, spec: { destination: { namespace: "prod" } } },
|
|
657
|
+
},
|
|
658
|
+
]),
|
|
659
|
+
},
|
|
660
|
+
cluster.connector,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
expect(result.resources["prod/web"]).toMatchObject({
|
|
664
|
+
type: "K8s::Apps::Deployment",
|
|
665
|
+
ownerChain: { root: "declared", entity: "webApp" },
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("a generic instance label matching NO declared Application claims nothing — not an inventory (#1549)", async () => {
|
|
670
|
+
// `app.kubernetes.io/instance` is also what plain helm sets; on an
|
|
671
|
+
// estate declaring a Flux CR (so the widened sweep runs), a foreign
|
|
672
|
+
// helm-installed Deployment must not be attributed.
|
|
673
|
+
const kustomization = {
|
|
674
|
+
apiVersion: "kustomize.toolkit.fluxcd.io/v1",
|
|
675
|
+
kind: "Kustomization",
|
|
676
|
+
metadata: { name: "apps", namespace: "flux-system", uid: "ks-uid" },
|
|
677
|
+
};
|
|
678
|
+
const foreign = {
|
|
679
|
+
apiVersion: "apps/v1",
|
|
680
|
+
kind: "Deployment",
|
|
681
|
+
metadata: { name: "cert-manager", namespace: "flux-system", uid: "cm-uid", labels: { "app.kubernetes.io/instance": "cert-manager" } },
|
|
682
|
+
};
|
|
683
|
+
const cluster = fakeCluster({
|
|
684
|
+
objects: {
|
|
685
|
+
[objectKey("kustomize.toolkit.fluxcd.io/v1", "Kustomization", "apps", "flux-system")]: kustomization,
|
|
686
|
+
[objectKey("apps/v1", "Deployment", "cert-manager", "flux-system")]: foreign,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const result = await describeResources(
|
|
691
|
+
{
|
|
692
|
+
environment: "prod",
|
|
693
|
+
buildOutput: "",
|
|
694
|
+
entityNames: ["apps"],
|
|
695
|
+
entities: makeEntities([
|
|
696
|
+
{ name: "apps", entityType: "K8s::Flux::Kustomization", props: { metadata: { name: "apps", namespace: "flux-system" } } },
|
|
697
|
+
]),
|
|
698
|
+
},
|
|
699
|
+
cluster.connector,
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
expect(result.resources["flux-system/cert-manager"]).toBeUndefined();
|
|
703
|
+
});
|
|
704
|
+
|
|
576
705
|
test("a core-only estate sweeps no extra groups — Pods stay the core representative", async () => {
|
|
577
706
|
const svc = { apiVersion: "v1", kind: "Service", metadata: { name: "web", namespace: "prod", uid: "svc-uid" } };
|
|
578
707
|
const cluster = fakeCluster({
|
|
@@ -880,6 +1009,36 @@ describe("statusFromObject — the most specific failing signal (#1397)", () =>
|
|
|
880
1009
|
expect(statusFromObject({ status: { readyReplicas: 1, replicas: 3 } } as never)).toBe("PROGRESSING(1/3)");
|
|
881
1010
|
expect(statusFromObject({} as never)).toBe("PRESENT");
|
|
882
1011
|
});
|
|
1012
|
+
|
|
1013
|
+
test("a conditions-first CR reads its Ready condition, not PRESENT (#1549)", () => {
|
|
1014
|
+
// A wedged Flux Kustomization/HelmRelease has no phase and no replica
|
|
1015
|
+
// counts, so it fell straight to PRESENT — a wedged reconciler read
|
|
1016
|
+
// exactly like a healthy one.
|
|
1017
|
+
expect(
|
|
1018
|
+
statusFromObject({ status: { conditions: [{ type: "Ready", status: "False", reason: "BuildFailed" }] } } as never),
|
|
1019
|
+
).toBe("BuildFailed");
|
|
1020
|
+
expect(
|
|
1021
|
+
statusFromObject({ status: { conditions: [{ type: "Ready", status: "False" }] } } as never),
|
|
1022
|
+
).toBe("NOT-READY");
|
|
1023
|
+
expect(
|
|
1024
|
+
statusFromObject({ status: { conditions: [{ type: "Ready", status: "True", reason: "ReconciliationSucceeded" }] } } as never),
|
|
1025
|
+
).toBe("READY");
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test("the Ready rung never changes a Pod or workload word — phase and replicas rank above it (#1549)", () => {
|
|
1029
|
+
expect(
|
|
1030
|
+
statusFromObject({ status: { phase: "Running", conditions: [{ type: "Ready", status: "False" }] } } as never),
|
|
1031
|
+
).toBe("Running");
|
|
1032
|
+
expect(
|
|
1033
|
+
statusFromObject({ status: { readyReplicas: 1, replicas: 3, conditions: [{ type: "Ready", status: "False" }] } } as never),
|
|
1034
|
+
).toBe("PROGRESSING(1/3)");
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test("an Argo Application reads health/sync (#1549)", () => {
|
|
1038
|
+
expect(statusFromObject({ status: { health: { status: "Degraded" }, sync: { status: "Synced" } } } as never)).toBe("Degraded");
|
|
1039
|
+
expect(statusFromObject({ status: { health: { status: "Healthy" }, sync: { status: "OutOfSync" } } } as never)).toBe("OutOfSync");
|
|
1040
|
+
expect(statusFromObject({ status: { health: { status: "Healthy" }, sync: { status: "Synced" } } } as never)).toBe("READY");
|
|
1041
|
+
});
|
|
883
1042
|
});
|
|
884
1043
|
|
|
885
1044
|
// #1401 — #1397 made the status WORD honest; this carries the part that says
|