@intentius/chant-lexicon-k8s 0.124.31 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/apps.js +18 -0
- package/dist/actions/batch.js +6 -0
- package/dist/actions/core.js +48 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/networking.js +11 -0
- package/dist/actions/rbac.js +8 -0
- package/dist/api/classify.js +123 -0
- package/dist/api/connect.js +62 -0
- package/dist/api/fake-cluster.js +175 -0
- package/dist/api/operation-surface.d.ts.map +1 -1
- package/dist/api/operation-surface.js +109 -0
- package/dist/api/owner-chain.js +100 -0
- package/dist/api/sweep-types.js +35 -0
- package/dist/behaviour-kinds.js +82 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +146 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +74 -0
- package/dist/codegen/generate-list-map-keys.js +88 -0
- package/dist/codegen/generate-operations.js +53 -0
- package/dist/codegen/generate-typescript.js +124 -0
- package/dist/codegen/generate.js +173 -0
- package/dist/codegen/naming.js +174 -0
- package/dist/codegen/package.js +36 -0
- package/dist/codegen/patches.js +88 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/codegen/versions.js +41 -0
- package/dist/components/argo-app.js +51 -0
- package/dist/components/builders.js +22 -0
- package/dist/components/capability-plugin.js +31 -0
- package/dist/components/flux-reconcile.js +61 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +18 -0
- package/dist/components/kubectl-apply.d.ts +83 -1
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/components/kubectl-apply.js +274 -0
- package/dist/components/kustomize-apply.js +25 -0
- package/dist/composites/adot-collector.js +184 -0
- package/dist/composites/agic-ingress.js +85 -0
- package/dist/composites/aks-external-dns-agent.js +146 -0
- package/dist/composites/alb-ingress.js +86 -0
- package/dist/composites/argo-app.js +199 -0
- package/dist/composites/autoscaled-service.js +174 -0
- package/dist/composites/azure-disk-storage-class.js +47 -0
- package/dist/composites/azure-file-storage-class.js +48 -0
- package/dist/composites/azure-monitor-collector.js +176 -0
- package/dist/composites/batch-job.js +139 -0
- package/dist/composites/catalog.js +4110 -0
- package/dist/composites/cockroachdb-cluster.js +371 -0
- package/dist/composites/cockroachdb-region-stack.js +369 -0
- package/dist/composites/config-connector-context.js +40 -0
- package/dist/composites/configured-app.js +135 -0
- package/dist/composites/cron-schedule.js +38 -0
- package/dist/composites/cron-workload.js +109 -0
- package/dist/composites/ebs-storage-class.js +54 -0
- package/dist/composites/efs-storage-class.js +46 -0
- package/dist/composites/external-dns-agent.js +122 -0
- package/dist/composites/filestore-storage-class.js +49 -0
- package/dist/composites/fluent-bit-agent.js +157 -0
- package/dist/composites/flux-app.js +156 -0
- package/dist/composites/gce-ingress.js +83 -0
- package/dist/composites/gce-pd-storage-class.js +49 -0
- package/dist/composites/gke-external-dns-agent.js +129 -0
- package/dist/composites/gke-fluent-bit-agent.js +165 -0
- package/dist/composites/gke-gateway.js +91 -0
- package/dist/composites/gke-otel-collector.js +94 -0
- package/dist/composites/index.js +51 -0
- package/dist/composites/inference-service.js +76 -0
- package/dist/composites/irsa-service-account.js +77 -0
- package/dist/composites/member-defaults.js +14 -0
- package/dist/composites/metrics-server.js +181 -0
- package/dist/composites/model.js +92 -0
- package/dist/composites/monitored-service.js +152 -0
- package/dist/composites/namespace-env.js +114 -0
- package/dist/composites/network-isolated-app.js +124 -0
- package/dist/composites/node-agent.js +149 -0
- package/dist/composites/operator-stack.js +268 -0
- package/dist/composites/otel-collector-agent.js +142 -0
- package/dist/composites/otel-collector-gateway.js +216 -0
- package/dist/composites/otel-collector-node.js +176 -0
- package/dist/composites/otel-collector-rbac.js +115 -0
- package/dist/composites/otel-collector-shape.js +128 -0
- package/dist/composites/otel-collector.js +115 -0
- package/dist/composites/otel-operator-collector.js +123 -0
- package/dist/composites/ray-cluster.js +378 -0
- package/dist/composites/ray-job.js +136 -0
- package/dist/composites/ray-service.js +178 -0
- package/dist/composites/secure-ingress.js +100 -0
- package/dist/composites/security-context.js +1 -0
- package/dist/composites/sidecar-app.js +105 -0
- package/dist/composites/stateful-app.js +111 -0
- package/dist/composites/vllm-serving-runtime.js +114 -0
- package/dist/composites/web-app.js +157 -0
- package/dist/composites/worker-pool.js +167 -0
- package/dist/composites/workload-identity-sa.js +81 -0
- package/dist/composites/workload-identity-service-account.js +77 -0
- package/dist/config-schema.js +72 -0
- package/dist/config.js +59 -0
- package/dist/coverage.js +27 -0
- package/dist/crd/crd-sources.js +615 -0
- package/dist/crd/loader.js +181 -0
- package/dist/crd/oci-chart.js +144 -0
- package/dist/crd/parser.js +382 -0
- package/dist/crd/project-codegen.js +273 -0
- package/dist/crd/types.js +7 -0
- package/dist/deep-observe-hooks.js +306 -0
- package/dist/deep-observe.js +234 -0
- package/dist/default-labels.js +87 -0
- package/dist/describe-resources.js +699 -0
- package/dist/describe-stack-status.js +99 -0
- package/dist/detect.js +11 -0
- package/dist/effect-receipt-row.js +205 -0
- package/dist/export-resources.js +39 -0
- package/dist/generated/index.js +1092 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/group-namespace.js +96 -0
- package/dist/import/embedded.js +197 -0
- package/dist/import/generator.js +288 -0
- package/dist/import/live-export.js +85 -0
- package/dist/import/parser.js +204 -0
- package/dist/import/testdata/embedded/fixtures.js +60 -0
- package/dist/include/entity.js +78 -0
- package/dist/include/resolve.js +170 -0
- package/dist/index.js +59 -0
- package/dist/integrity.json +3 -3
- package/dist/kube/apply.js +90 -0
- package/dist/kube/connect.js +46 -0
- package/dist/kube/delete.js +138 -0
- package/dist/kube/describe.js +169 -0
- package/dist/kube/events.js +132 -0
- package/dist/kube/flags.js +91 -0
- package/dist/kube/get.js +159 -0
- package/dist/kube/group.js +123 -0
- package/dist/kube/logs.js +82 -0
- package/dist/kube/project.js +94 -0
- package/dist/kube/render.js +227 -0
- package/dist/kube/source.js +122 -0
- package/dist/kube/target.js +63 -0
- package/dist/kube/testing.js +28 -0
- package/dist/kube/top.js +99 -0
- package/dist/kube/verdict.js +72 -0
- package/dist/kube/wait.js +129 -0
- package/dist/kustomize/render.js +73 -0
- package/dist/kustomize/rendered-entity.js +67 -0
- package/dist/kustomize/root.js +68 -0
- package/dist/lint/audit-catalog.js +61 -0
- package/dist/lint/audit-lineage.js +118 -0
- package/dist/lint/post-synth/argo-helpers.js +36 -0
- package/dist/lint/post-synth/argo002.js +31 -0
- package/dist/lint/post-synth/argo003.js +53 -0
- package/dist/lint/post-synth/argo005.js +54 -0
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
- package/dist/lint/post-synth/flux002.js +56 -0
- package/dist/lint/post-synth/flux003.js +50 -0
- package/dist/lint/post-synth/index.js +104 -0
- package/dist/lint/post-synth/k8s-helpers.js +186 -0
- package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
- package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
- package/dist/lint/post-synth/sops-helpers.js +43 -0
- package/dist/lint/post-synth/wk8005.js +44 -0
- package/dist/lint/post-synth/wk8006.js +56 -0
- package/dist/lint/post-synth/wk8041.js +60 -0
- package/dist/lint/post-synth/wk8042.js +36 -0
- package/dist/lint/post-synth/wk8101.js +52 -0
- package/dist/lint/post-synth/wk8102.js +30 -0
- package/dist/lint/post-synth/wk8103.js +33 -0
- package/dist/lint/post-synth/wk8104.js +58 -0
- package/dist/lint/post-synth/wk8105.js +33 -0
- package/dist/lint/post-synth/wk8201.js +44 -0
- package/dist/lint/post-synth/wk8202.js +34 -0
- package/dist/lint/post-synth/wk8203.js +34 -0
- package/dist/lint/post-synth/wk8204.js +71 -0
- package/dist/lint/post-synth/wk8205.js +38 -0
- package/dist/lint/post-synth/wk8207.js +33 -0
- package/dist/lint/post-synth/wk8208.js +33 -0
- package/dist/lint/post-synth/wk8209.js +33 -0
- package/dist/lint/post-synth/wk8301.js +49 -0
- package/dist/lint/post-synth/wk8302.js +34 -0
- package/dist/lint/post-synth/wk8303.js +68 -0
- package/dist/lint/post-synth/wk8304.js +58 -0
- package/dist/lint/post-synth/wk8305.js +96 -0
- package/dist/lint/post-synth/wk8306.js +38 -0
- package/dist/lint/post-synth/wk8401.js +86 -0
- package/dist/lint/post-synth/wk8402.js +31 -0
- package/dist/lint/post-synth/wk8403.js +49 -0
- package/dist/lint/post-synth/wk8404.js +36 -0
- package/dist/lint/post-synth/wk8405.js +65 -0
- package/dist/lint/post-synth/wk8406.js +50 -0
- package/dist/lint/post-synth/wk8407.js +58 -0
- package/dist/lint/post-synth/wk8501.js +36 -0
- package/dist/lint/post-synth/wk8502.js +35 -0
- package/dist/lint/post-synth/wk8503.js +164 -0
- package/dist/lint/post-synth/wk8504.js +20 -0
- package/dist/lint/post-synth/wk8505.js +31 -0
- package/dist/lint/post-synth/wk8601.js +40 -0
- package/dist/lint/post-synth/wk8602.js +120 -0
- package/dist/lint/post-synth/wk8603.js +63 -0
- package/dist/lint/post-synth/wk8604.js +31 -0
- package/dist/lint/post-synth/wk8605.js +124 -0
- package/dist/lint/post-synth/wk8606.js +22 -0
- package/dist/lint/post-synth/wk8701.js +31 -0
- package/dist/lint/post-synth/wk8702.js +37 -0
- package/dist/lint/post-synth/wk8703.js +31 -0
- package/dist/lint/post-synth/wk8704.js +135 -0
- package/dist/lint/post-synth/wk8705.js +38 -0
- package/dist/lint/rules/argo-appset-single-project.js +56 -0
- package/dist/lint/rules/argo-ast.js +94 -0
- package/dist/lint/rules/argo-automated-prune.js +59 -0
- package/dist/lint/rules/flux-source-ref-pin.js +44 -0
- package/dist/lint/rules/hardcoded-namespace.js +57 -0
- package/dist/lint/rules/latest-image-tag.js +103 -0
- package/dist/lint/rules/missing-resource-limits.js +85 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +57 -0
- package/dist/manifest-entity.js +71 -0
- package/dist/manifest.json +1 -1
- package/dist/micro-time.js +49 -0
- package/dist/op/activities/apply-diff.d.ts +31 -0
- package/dist/op/activities/apply-diff.d.ts.map +1 -0
- package/dist/op/activities/apply-diff.js +79 -0
- package/dist/op/activities/argo.js +98 -0
- package/dist/op/activities/ensure-secret.js +38 -0
- package/dist/op/activities/index.js +51 -0
- package/dist/op/activities/kubectl.d.ts +64 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/kubectl.js +517 -0
- package/dist/op/activities/wait-for-ready.js +229 -0
- package/dist/op/builders.js +68 -0
- package/dist/package-cli.js +21 -0
- package/dist/patch/json-patch.js +187 -0
- package/dist/patch/rendered-patch.js +94 -0
- package/dist/plugin.js +741 -0
- package/dist/project-kinds.js +56 -0
- package/dist/receipt-store.js +216 -0
- package/dist/rules/crd-schema-helpers.ts +2 -1
- package/dist/secret-labels.js +33 -0
- package/dist/secret-store.js +116 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +578 -0
- package/dist/sops/encrypted-secret-file.js +194 -0
- package/dist/sops/entity.js +32 -0
- package/dist/spec/fetch.js +58 -0
- package/dist/spec/parse.js +622 -0
- package/dist/subscribe-changes.js +168 -0
- package/dist/teardown.js +197 -0
- package/dist/telemetry-release.js +86 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +47 -0
- package/dist/variables.js +34 -0
- package/dist/whoami.js +109 -0
- package/package.json +12 -12
- package/src/api/operation-surface.ts +2 -1
- package/src/codegen/package.ts +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
- package/src/components/kubectl-apply.test.ts +262 -0
- package/src/components/kubectl-apply.ts +341 -12
- package/src/deep-observe-hooks.ts +2 -2
- package/src/import/roundtrip-build.test.ts +46 -0
- package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/op/activities/apply-diff.ts +87 -0
- package/src/op/activities/kubectl-plan.test.ts +224 -0
- package/src/op/activities/kubectl.ts +204 -16
- package/src/serializer.ts +2 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { findResourceLiterals, getNestedObject, getNestedString, getProp, lineCol } from "./argo-ast.js";
|
|
2
|
+
/**
|
|
3
|
+
* FLUX001: GitRepository must pin spec.ref
|
|
4
|
+
*
|
|
5
|
+
* A Flux `GitRepository` with no `spec.ref` falls back to the `master` branch,
|
|
6
|
+
* which on most repos no longer exists — the source stalls with a checkout
|
|
7
|
+
* error, and every Kustomization downstream of it stalls too. Real estates pin
|
|
8
|
+
* a branch (or tag) on every source; `FluxGitSource` always emits one. Flag
|
|
9
|
+
* hand-written `GitRepository` literals whose spec carries a `url` but no
|
|
10
|
+
* `ref`.
|
|
11
|
+
*
|
|
12
|
+
* Bad: new GitRepository({ spec: { url: "https://github.com/acme/infra" } })
|
|
13
|
+
* Good: new GitRepository({ spec: { url: "https://github.com/acme/infra", ref: { branch: "main" } } })
|
|
14
|
+
*/
|
|
15
|
+
export const fluxSourceRefPinRule = {
|
|
16
|
+
id: "FLUX001",
|
|
17
|
+
severity: "warning",
|
|
18
|
+
category: "correctness",
|
|
19
|
+
description: "Flux GitRepository must pin spec.ref (branch, tag, semver, or commit) — the unset default is the master branch",
|
|
20
|
+
check(context) {
|
|
21
|
+
const { sourceFile } = context;
|
|
22
|
+
const diagnostics = [];
|
|
23
|
+
for (const { literal } of findResourceLiterals(sourceFile, new Set(["GitRepository"]))) {
|
|
24
|
+
const spec = getNestedObject(literal, ["spec"]);
|
|
25
|
+
// No spec object literal to inspect — leave it to other tooling.
|
|
26
|
+
if (!spec)
|
|
27
|
+
continue;
|
|
28
|
+
if (getProp(spec, "ref") !== undefined)
|
|
29
|
+
continue;
|
|
30
|
+
const anchor = spec;
|
|
31
|
+
const { line, column } = lineCol(sourceFile, anchor);
|
|
32
|
+
const name = getNestedString(literal, ["metadata", "name"]) ?? "(unnamed)";
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
file: sourceFile.fileName,
|
|
35
|
+
line,
|
|
36
|
+
column,
|
|
37
|
+
ruleId: "FLUX001",
|
|
38
|
+
severity: "warning",
|
|
39
|
+
message: `GitRepository "${name}" has no spec.ref — Flux falls back to the master branch. Pin a branch, tag, semver range, or commit.`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return diagnostics;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/**
|
|
3
|
+
* WK8001: Hardcoded Namespace
|
|
4
|
+
*
|
|
5
|
+
* Detects hardcoded namespace strings in a Kubernetes resource's `metadata`.
|
|
6
|
+
* Namespaces should be parameterized or derived from configuration, not
|
|
7
|
+
* hardcoded as string literals.
|
|
8
|
+
*
|
|
9
|
+
* Bad: new Deployment({ metadata: { namespace: "production" } })
|
|
10
|
+
* Good: new Deployment({ metadata: { namespace: config.namespace } })
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Only `metadata: { namespace }` is a Kubernetes namespace. Other `namespace`
|
|
14
|
+
* keys are another tool's setting (an OpenTelemetry connector's metric prefix,
|
|
15
|
+
* for one) and a k8s rule has no business flagging them.
|
|
16
|
+
*/
|
|
17
|
+
function isMetadataEntry(node) {
|
|
18
|
+
const owner = node.parent.parent;
|
|
19
|
+
return (ts.isPropertyAssignment(owner) &&
|
|
20
|
+
ts.isIdentifier(owner.name) &&
|
|
21
|
+
owner.name.text === "metadata");
|
|
22
|
+
}
|
|
23
|
+
export const hardcodedNamespaceRule = {
|
|
24
|
+
id: "WK8001",
|
|
25
|
+
severity: "warning",
|
|
26
|
+
category: "correctness",
|
|
27
|
+
description: "Detects hardcoded namespace strings — namespaces should be parameterized",
|
|
28
|
+
check(context) {
|
|
29
|
+
const { sourceFile } = context;
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
function visit(node) {
|
|
32
|
+
// Look for property assignments like `namespace: "production"`
|
|
33
|
+
if (ts.isPropertyAssignment(node) &&
|
|
34
|
+
ts.isIdentifier(node.name) &&
|
|
35
|
+
node.name.text === "namespace" &&
|
|
36
|
+
ts.isStringLiteral(node.initializer) &&
|
|
37
|
+
isMetadataEntry(node)) {
|
|
38
|
+
const value = node.initializer.text;
|
|
39
|
+
// Skip empty strings — those are likely intentional placeholders
|
|
40
|
+
if (value !== "") {
|
|
41
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.initializer.getStart());
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
file: sourceFile.fileName,
|
|
44
|
+
line: line + 1,
|
|
45
|
+
column: character + 1,
|
|
46
|
+
ruleId: "WK8001",
|
|
47
|
+
severity: "warning",
|
|
48
|
+
message: `Hardcoded namespace "${value}" detected. Use a variable or configuration parameter instead.`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
ts.forEachChild(node, visit);
|
|
53
|
+
}
|
|
54
|
+
visit(sourceFile);
|
|
55
|
+
return diagnostics;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/**
|
|
3
|
+
* WK8002: Latest Image Tag
|
|
4
|
+
*
|
|
5
|
+
* Detects when a K8s workload resource uses the `:latest` image tag or no tag
|
|
6
|
+
* at all in a container image string literal. Untagged or `:latest` images are
|
|
7
|
+
* non-deterministic and can cause unexpected rollouts.
|
|
8
|
+
*
|
|
9
|
+
* Bad: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx:latest" }] } } } })
|
|
10
|
+
* Bad: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx" }] } } } })
|
|
11
|
+
* Good: new Deployment({ spec: { template: { spec: { containers: [{ image: "nginx:1.25" }] } } } })
|
|
12
|
+
*/
|
|
13
|
+
const WORKLOAD_KINDS = new Set([
|
|
14
|
+
"Deployment",
|
|
15
|
+
"StatefulSet",
|
|
16
|
+
"DaemonSet",
|
|
17
|
+
"CronJob",
|
|
18
|
+
"Job",
|
|
19
|
+
"ReplicaSet",
|
|
20
|
+
"Pod",
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the string looks like a container image reference that is
|
|
24
|
+
* either untagged or using `:latest`.
|
|
25
|
+
*
|
|
26
|
+
* A string is considered a container image if it:
|
|
27
|
+
* - Contains at least one alphabetic character
|
|
28
|
+
* - Does not contain spaces
|
|
29
|
+
* - Is not a simple keyword like "true", "false", etc.
|
|
30
|
+
*/
|
|
31
|
+
function isProblematicImage(value) {
|
|
32
|
+
if (!value || value.includes(" ") || value.length === 0)
|
|
33
|
+
return false;
|
|
34
|
+
// Skip values that are clearly not images
|
|
35
|
+
const nonImagePatterns = [
|
|
36
|
+
/^(true|false|null|undefined|yes|no)$/i,
|
|
37
|
+
/^\d+$/, // pure numbers
|
|
38
|
+
/^[.\/]/, // relative/absolute paths without image-like structure
|
|
39
|
+
];
|
|
40
|
+
for (const pat of nonImagePatterns) {
|
|
41
|
+
if (pat.test(value))
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Check for :latest explicitly
|
|
45
|
+
if (value.endsWith(":latest"))
|
|
46
|
+
return true;
|
|
47
|
+
// Check for untagged image: no colon at all, but looks like an image name
|
|
48
|
+
// Images contain alphanumeric chars and may have / for registry prefix
|
|
49
|
+
// Must have at least one alpha char and match image naming conventions
|
|
50
|
+
if (!value.includes(":") && !value.includes("@") && /^[a-zA-Z0-9._\-\/]+$/.test(value) && /[a-zA-Z]/.test(value)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
export const latestImageTagRule = {
|
|
56
|
+
id: "WK8002",
|
|
57
|
+
severity: "warning",
|
|
58
|
+
category: "security",
|
|
59
|
+
description: "Detects :latest or untagged container images — use explicit version tags for reproducibility",
|
|
60
|
+
check(context) {
|
|
61
|
+
const { sourceFile } = context;
|
|
62
|
+
const diagnostics = [];
|
|
63
|
+
function isInsideWorkloadConstructor(node) {
|
|
64
|
+
let current = node.parent;
|
|
65
|
+
while (current) {
|
|
66
|
+
if (ts.isNewExpression(current) &&
|
|
67
|
+
ts.isIdentifier(current.expression) &&
|
|
68
|
+
WORKLOAD_KINDS.has(current.expression.text)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
current = current.parent;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
function visit(node) {
|
|
76
|
+
// Look for property assignments like `image: "nginx:latest"` or `image: "nginx"`
|
|
77
|
+
if (ts.isPropertyAssignment(node) &&
|
|
78
|
+
ts.isIdentifier(node.name) &&
|
|
79
|
+
node.name.text === "image" &&
|
|
80
|
+
ts.isStringLiteral(node.initializer) &&
|
|
81
|
+
isInsideWorkloadConstructor(node)) {
|
|
82
|
+
const value = node.initializer.text;
|
|
83
|
+
if (isProblematicImage(value)) {
|
|
84
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.initializer.getStart());
|
|
85
|
+
const isLatest = value.endsWith(":latest");
|
|
86
|
+
diagnostics.push({
|
|
87
|
+
file: sourceFile.fileName,
|
|
88
|
+
line: line + 1,
|
|
89
|
+
column: character + 1,
|
|
90
|
+
ruleId: "WK8002",
|
|
91
|
+
severity: "warning",
|
|
92
|
+
message: isLatest
|
|
93
|
+
? `Container image "${value}" uses the :latest tag. Pin to a specific version for reproducibility.`
|
|
94
|
+
: `Container image "${value}" has no tag. Pin to a specific version for reproducibility.`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
ts.forEachChild(node, visit);
|
|
99
|
+
}
|
|
100
|
+
visit(sourceFile);
|
|
101
|
+
return diagnostics;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/**
|
|
3
|
+
* WK8003: Missing Resource Limits
|
|
4
|
+
*
|
|
5
|
+
* Detects when a container in a Deployment/StatefulSet spec doesn't have
|
|
6
|
+
* resource limits or requests. Without resource limits, a container can
|
|
7
|
+
* consume unbounded cluster resources and cause noisy-neighbour issues.
|
|
8
|
+
*
|
|
9
|
+
* Bad: new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0" }] } } } })
|
|
10
|
+
* Good: new Deployment({ spec: { template: { spec: { containers: [{ name: "app", image: "app:1.0", resources: { limits: { cpu: "500m", memory: "256Mi" } } }] } } } })
|
|
11
|
+
*/
|
|
12
|
+
const WORKLOAD_KINDS = new Set([
|
|
13
|
+
"Deployment",
|
|
14
|
+
"StatefulSet",
|
|
15
|
+
"DaemonSet",
|
|
16
|
+
"CronJob",
|
|
17
|
+
"Job",
|
|
18
|
+
"ReplicaSet",
|
|
19
|
+
]);
|
|
20
|
+
export const missingResourceLimitsRule = {
|
|
21
|
+
id: "WK8003",
|
|
22
|
+
severity: "warning",
|
|
23
|
+
category: "correctness",
|
|
24
|
+
description: "Detects containers without resource limits/requests — always set resource constraints",
|
|
25
|
+
check(context) {
|
|
26
|
+
const { sourceFile } = context;
|
|
27
|
+
const diagnostics = [];
|
|
28
|
+
function isInsideWorkloadConstructor(node) {
|
|
29
|
+
let current = node.parent;
|
|
30
|
+
while (current) {
|
|
31
|
+
if (ts.isNewExpression(current) &&
|
|
32
|
+
ts.isIdentifier(current.expression) &&
|
|
33
|
+
WORKLOAD_KINDS.has(current.expression.text)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
current = current.parent;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function objectLiteralHasProperty(obj, name) {
|
|
41
|
+
return obj.properties.some((p) => ts.isPropertyAssignment(p) &&
|
|
42
|
+
ts.isIdentifier(p.name) &&
|
|
43
|
+
p.name.text === name);
|
|
44
|
+
}
|
|
45
|
+
function visit(node) {
|
|
46
|
+
// Look for object literals inside arrays that represent container specs.
|
|
47
|
+
// A container object literal typically has "name" and "image" properties.
|
|
48
|
+
// We flag it if it lacks a "resources" property.
|
|
49
|
+
if (ts.isObjectLiteralExpression(node) &&
|
|
50
|
+
isInsideWorkloadConstructor(node)) {
|
|
51
|
+
const hasName = objectLiteralHasProperty(node, "name");
|
|
52
|
+
const hasImage = objectLiteralHasProperty(node, "image");
|
|
53
|
+
const hasResources = objectLiteralHasProperty(node, "resources");
|
|
54
|
+
if (hasName && hasImage && !hasResources) {
|
|
55
|
+
// Confirm we're inside an array literal (containers array)
|
|
56
|
+
if (node.parent && ts.isArrayLiteralExpression(node.parent)) {
|
|
57
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
58
|
+
// Try to extract the container name for a better message
|
|
59
|
+
let containerName = "unknown";
|
|
60
|
+
for (const prop of node.properties) {
|
|
61
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
62
|
+
ts.isIdentifier(prop.name) &&
|
|
63
|
+
prop.name.text === "name" &&
|
|
64
|
+
ts.isStringLiteral(prop.initializer)) {
|
|
65
|
+
containerName = prop.initializer.text;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
diagnostics.push({
|
|
70
|
+
file: sourceFile.fileName,
|
|
71
|
+
line: line + 1,
|
|
72
|
+
column: character + 1,
|
|
73
|
+
ruleId: "WK8003",
|
|
74
|
+
severity: "warning",
|
|
75
|
+
message: `Container "${containerName}" is missing resource limits/requests. Set resources.limits and resources.requests to prevent unbounded resource consumption.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
ts.forEachChild(node, visit);
|
|
81
|
+
}
|
|
82
|
+
visit(sourceFile);
|
|
83
|
+
return diagnostics;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAapF;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEvE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
3
|
+
import { LexiconIndex, lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
|
+
let cachedIndex = null;
|
|
6
|
+
function getIndex() {
|
|
7
|
+
if (cachedIndex)
|
|
8
|
+
return cachedIndex;
|
|
9
|
+
const data = require("../generated/lexicon-k8s.json");
|
|
10
|
+
cachedIndex = new LexiconIndex(data);
|
|
11
|
+
return cachedIndex;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provide Kubernetes resource completions based on context.
|
|
15
|
+
*/
|
|
16
|
+
export function k8sCompletions(ctx) {
|
|
17
|
+
return lexiconCompletions(ctx, getIndex(), "Kubernetes resource");
|
|
18
|
+
}
|
package/dist/lsp/hover.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAa1E;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAEjE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
3
|
+
import { LexiconIndex, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
|
+
let cachedIndex = null;
|
|
6
|
+
function getIndex() {
|
|
7
|
+
if (cachedIndex)
|
|
8
|
+
return cachedIndex;
|
|
9
|
+
const data = require("../generated/lexicon-k8s.json");
|
|
10
|
+
cachedIndex = new LexiconIndex(data);
|
|
11
|
+
return cachedIndex;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provide hover information for Kubernetes resource types.
|
|
15
|
+
*/
|
|
16
|
+
export function k8sHover(ctx) {
|
|
17
|
+
return lexiconHover(ctx, getIndex(), resourceHover);
|
|
18
|
+
}
|
|
19
|
+
function resourceHover(className, entry) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
lines.push(`**${className}**`);
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push(`K8s resource type: \`${entry.resourceType}\``);
|
|
24
|
+
// Show apiVersion if stored in attrs
|
|
25
|
+
const apiVersion = entry.attrs?.apiVersion;
|
|
26
|
+
if (apiVersion) {
|
|
27
|
+
lines.push(`apiVersion: \`${apiVersion}\``);
|
|
28
|
+
}
|
|
29
|
+
// Show GVK kind if stored in attrs
|
|
30
|
+
const gvkKind = entry.attrs?.gvkKind;
|
|
31
|
+
if (gvkKind) {
|
|
32
|
+
lines.push(`kind: \`${gvkKind}\``);
|
|
33
|
+
}
|
|
34
|
+
if (entry.kind === "resource") {
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push("*Resource — serialized as a top-level Kubernetes object*");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push("*Property — used as a nested value in resource specs*");
|
|
41
|
+
}
|
|
42
|
+
if (entry.attrs && Object.keys(entry.attrs).length > 0) {
|
|
43
|
+
const displayAttrs = Object.entries(entry.attrs).filter(([k]) => k !== "apiVersion" && k !== "gvkKind");
|
|
44
|
+
if (displayAttrs.length > 0) {
|
|
45
|
+
lines.push("");
|
|
46
|
+
lines.push("**Attributes:**");
|
|
47
|
+
for (const [key, value] of displayAttrs) {
|
|
48
|
+
lines.push(`- \`${key}\` \u2192 \`${value}\``);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (entry.primaryIdentifier && entry.primaryIdentifier.length > 0) {
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(`**Primary identifier:** ${entry.primaryIdentifier.map((p) => `\`${p}\``).join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
return { contents: lines.join("\n") };
|
|
57
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A complete Kubernetes manifest carried into a build as a chant entity.
|
|
3
|
+
*
|
|
4
|
+
* Two callers produce one: a kustomize build root, whose output is a set of
|
|
5
|
+
* finished documents rather than typed chant source (#1548), and `chant carve
|
|
6
|
+
* emit` adopting a Terraform `kubernetes_manifest` (#999), whose whole content
|
|
7
|
+
* is a manifest body read out of state. In both cases the document already
|
|
8
|
+
* decided every field, so `props` IS the manifest: the serializer emits it
|
|
9
|
+
* as-is — plus the default-label / ownership merge every discovered resource
|
|
10
|
+
* gets — instead of running the spec-inference heuristics built for typed
|
|
11
|
+
* declarables, which would re-nest a top-level field that is not `spec`
|
|
12
|
+
* (`rules` on a ClusterRole, `webhooks` on a webhook config).
|
|
13
|
+
*
|
|
14
|
+
* The marker is what tells the serializer to take that path — `Symbol.for`, so
|
|
15
|
+
* a lexicon built against a separate copy of core still matches (the #1137
|
|
16
|
+
* lesson).
|
|
17
|
+
*
|
|
18
|
+
* `entityType` comes from `gvkToTypeName`, the one group→namespace rule
|
|
19
|
+
* (`./group-namespace`), so a manifest declared this way is typed exactly as a
|
|
20
|
+
* generated class would be and `lifecycle diff --live` observes it through the
|
|
21
|
+
* reader a declared resource uses. A kind the generated operation surface does
|
|
22
|
+
* not know reports NOT-OBSERVED with the existing "no generated operation
|
|
23
|
+
* surface" reason — the honest verdict, not a silent hole.
|
|
24
|
+
*/
|
|
25
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
26
|
+
import { gvkToTypeName } from "./spec/parse.js";
|
|
27
|
+
/** Marks an entity whose `props` are a complete, render-final manifest. */
|
|
28
|
+
export const RENDERED_MANIFEST_MARKER = Symbol.for("chant.k8s.renderedManifest");
|
|
29
|
+
export function isRenderedManifestEntity(entity) {
|
|
30
|
+
return entity[RENDERED_MANIFEST_MARKER] === true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Wrap one manifest. Returns null when the document has no string
|
|
34
|
+
* `apiVersion`/`kind` — not a Kubernetes object, nothing to declare.
|
|
35
|
+
*/
|
|
36
|
+
export function manifestEntity(doc) {
|
|
37
|
+
const apiVersion = doc.apiVersion;
|
|
38
|
+
const kind = doc.kind;
|
|
39
|
+
if (typeof apiVersion !== "string" || typeof kind !== "string" || !apiVersion || !kind)
|
|
40
|
+
return null;
|
|
41
|
+
const slash = apiVersion.indexOf("/");
|
|
42
|
+
const group = slash === -1 ? "" : apiVersion.slice(0, slash);
|
|
43
|
+
const version = slash === -1 ? apiVersion : apiVersion.slice(slash + 1);
|
|
44
|
+
return {
|
|
45
|
+
lexicon: "k8s",
|
|
46
|
+
entityType: gvkToTypeName({ group, version, kind }),
|
|
47
|
+
kind: "resource",
|
|
48
|
+
props: { ...doc },
|
|
49
|
+
[DECLARABLE_MARKER]: true,
|
|
50
|
+
[RENDERED_MANIFEST_MARKER]: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Declare an arbitrary Kubernetes object from its manifest — the escape hatch
|
|
55
|
+
* for a kind the lexicon ships no generated class for, and what `chant carve
|
|
56
|
+
* emit` writes when it adopts a `kubernetes_manifest` (#999). The typed
|
|
57
|
+
* constructors cannot serve either case: their props are the kind's own schema,
|
|
58
|
+
* so an unknown CRD has no class at all and a known one rejects an
|
|
59
|
+
* `apiVersion`/`kind` pair that differs from the generated default.
|
|
60
|
+
*
|
|
61
|
+
* Throws on a document without a string `apiVersion` and `kind`: those two
|
|
62
|
+
* fields are what makes it addressable, and a silently dropped resource is the
|
|
63
|
+
* failure this is guarding against.
|
|
64
|
+
*/
|
|
65
|
+
export function k8sManifest(doc) {
|
|
66
|
+
const entity = manifestEntity(doc);
|
|
67
|
+
if (!entity) {
|
|
68
|
+
throw new Error("k8sManifest requires a document with string `apiVersion` and `kind` fields.");
|
|
69
|
+
}
|
|
70
|
+
return entity;
|
|
71
|
+
}
|
package/dist/manifest.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for K8s `MicroTime` fields.
|
|
3
|
+
*
|
|
4
|
+
* The K8s API server validates fields like `coordination.k8s.io/v1.Lease.spec.renewTime`
|
|
5
|
+
* (kind: MicroTime) with the exact format `2006-01-02T15:04:05.000000Z07:00`
|
|
6
|
+
* — microsecond precision, always 6 fractional digits, UTC offset.
|
|
7
|
+
*
|
|
8
|
+
* The naive JS `Date.toISOString()` (millisecond precision, 3 fractional
|
|
9
|
+
* digits) is accepted by the API server too — the schema is more permissive
|
|
10
|
+
* than the documented format string — but `time.RFC3339Nano`-style strings
|
|
11
|
+
* with nanosecond precision (9 fractional digits) are rejected as
|
|
12
|
+
* `422 Unprocessable Entity` with:
|
|
13
|
+
*
|
|
14
|
+
* parsing time "...754Z" as "2006-01-02T15:04:05.000000Z07:00":
|
|
15
|
+
* cannot parse "754Z" as "Z07:00"
|
|
16
|
+
*
|
|
17
|
+
* Pass a JS Date through `microTime()` to get a string the API server
|
|
18
|
+
* always accepts. This is the canonical way to populate MicroTime fields
|
|
19
|
+
* when authoring chant resources by hand.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Format a Date as a K8s MicroTime string (UTC, exactly 6 fractional digits).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* import { Lease } from "@intentius/chant-lexicon-k8s";
|
|
26
|
+
* import { microTime } from "@intentius/chant-lexicon-k8s/micro-time";
|
|
27
|
+
*
|
|
28
|
+
* new Lease({
|
|
29
|
+
* metadata: { name: "my-lease", namespace: "default" },
|
|
30
|
+
* spec: {
|
|
31
|
+
* holderIdentity: "node-1",
|
|
32
|
+
* leaseDurationSeconds: 15,
|
|
33
|
+
* renewTime: microTime(new Date()),
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
export function microTime(date = new Date()) {
|
|
38
|
+
const iso = date.toISOString(); // "2026-05-11T21:25:36.495Z" (3 digits)
|
|
39
|
+
// Replace the millisecond fragment with a 6-digit microsecond fragment.
|
|
40
|
+
// toISOString() always produces ms precision; pad to 6 by appending 3 zeros.
|
|
41
|
+
return iso.replace(/\.(\d{3})Z$/, ".$1000Z");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns true if `value` is a string in the K8s MicroTime canonical format
|
|
45
|
+
* (UTC, microsecond precision). Useful in lint/validation contexts.
|
|
46
|
+
*/
|
|
47
|
+
export function isMicroTimeFormatted(value) {
|
|
48
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/.test(value);
|
|
49
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a server-side apply would change in one object (#3998): the live
|
|
3
|
+
* object against the result of a dry-run apply of the declared one.
|
|
4
|
+
*
|
|
5
|
+
* Both sides come from the API server, so a field the server defaults or
|
|
6
|
+
* fills in appears on both with the same value and drops out of the diff.
|
|
7
|
+
* What is left out before comparing is the bookkeeping the server writes on
|
|
8
|
+
* every request and that says nothing about the declared state: managed
|
|
9
|
+
* fields, the resource version, the uid, the creation timestamp, the
|
|
10
|
+
* generation (a dry run of a spec change bumps it) and `status`.
|
|
11
|
+
*
|
|
12
|
+
* Pure functions only, with no client import, so the capability module can
|
|
13
|
+
* share them without reaching the API-client chain (#1074).
|
|
14
|
+
*/
|
|
15
|
+
/** One field whose value the apply would change. `before` is absent on an added field, `after` on a removed one. */
|
|
16
|
+
export interface FieldChange {
|
|
17
|
+
path: string;
|
|
18
|
+
before?: unknown;
|
|
19
|
+
after?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** A copy of `object` without the fields the server writes on every request. */
|
|
22
|
+
export declare function normalizeServerObject(object: Record<string, unknown>): Record<string, unknown>;
|
|
23
|
+
/** Deep equality over JSON values. */
|
|
24
|
+
export declare function jsonEqual(a: unknown, b: unknown): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The fields that differ between `live` and `next`, both normalized. Objects
|
|
27
|
+
* are walked to their leaves; a list is compared whole and reported at its
|
|
28
|
+
* own path, since a list's items have no stable address in general.
|
|
29
|
+
*/
|
|
30
|
+
export declare function diffFields(live: Record<string, unknown>, next: Record<string, unknown>, prefix?: string): FieldChange[];
|
|
31
|
+
//# sourceMappingURL=apply-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-diff.d.ts","sourceRoot":"","sources":["../../../src/op/activities/apply-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,oHAAoH;AACpH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAKD,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAS9F;AAKD,sCAAsC;AACtC,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CASzD;AAQD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,SAAK,GAAG,WAAW,EAAE,CAuBnH"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a server-side apply would change in one object (#3998): the live
|
|
3
|
+
* object against the result of a dry-run apply of the declared one.
|
|
4
|
+
*
|
|
5
|
+
* Both sides come from the API server, so a field the server defaults or
|
|
6
|
+
* fills in appears on both with the same value and drops out of the diff.
|
|
7
|
+
* What is left out before comparing is the bookkeeping the server writes on
|
|
8
|
+
* every request and that says nothing about the declared state: managed
|
|
9
|
+
* fields, the resource version, the uid, the creation timestamp, the
|
|
10
|
+
* generation (a dry run of a spec change bumps it) and `status`.
|
|
11
|
+
*
|
|
12
|
+
* Pure functions only, with no client import, so the capability module can
|
|
13
|
+
* share them without reaching the API-client chain (#1074).
|
|
14
|
+
*/
|
|
15
|
+
/** The `metadata` keys the server writes on every request. */
|
|
16
|
+
const SERVER_METADATA = ["managedFields", "resourceVersion", "uid", "creationTimestamp", "generation", "selfLink"];
|
|
17
|
+
/** A copy of `object` without the fields the server writes on every request. */
|
|
18
|
+
export function normalizeServerObject(object) {
|
|
19
|
+
const { status: _status, ...rest } = object;
|
|
20
|
+
const metadata = rest.metadata;
|
|
21
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
22
|
+
const copy = { ...metadata };
|
|
23
|
+
for (const key of SERVER_METADATA)
|
|
24
|
+
delete copy[key];
|
|
25
|
+
rest.metadata = copy;
|
|
26
|
+
}
|
|
27
|
+
return rest;
|
|
28
|
+
}
|
|
29
|
+
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
30
|
+
/** Deep equality over JSON values. */
|
|
31
|
+
export function jsonEqual(a, b) {
|
|
32
|
+
if (a === b)
|
|
33
|
+
return true;
|
|
34
|
+
if (Array.isArray(a) && Array.isArray(b))
|
|
35
|
+
return a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
|
|
36
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
37
|
+
const ka = Object.keys(a);
|
|
38
|
+
if (ka.length !== Object.keys(b).length)
|
|
39
|
+
return false;
|
|
40
|
+
return ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && jsonEqual(a[k], b[k]));
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
/** `spec.replicas`, or `metadata.labels["app.kubernetes.io/name"]` for a key that is not a plain identifier. */
|
|
45
|
+
function joinPath(prefix, key) {
|
|
46
|
+
if (/^[A-Za-z_$][\w$-]*$/.test(key))
|
|
47
|
+
return prefix ? `${prefix}.${key}` : key;
|
|
48
|
+
return `${prefix}[${JSON.stringify(key)}]`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The fields that differ between `live` and `next`, both normalized. Objects
|
|
52
|
+
* are walked to their leaves; a list is compared whole and reported at its
|
|
53
|
+
* own path, since a list's items have no stable address in general.
|
|
54
|
+
*/
|
|
55
|
+
export function diffFields(live, next, prefix = "") {
|
|
56
|
+
const changes = [];
|
|
57
|
+
const keys = [...new Set([...Object.keys(live), ...Object.keys(next)])].sort();
|
|
58
|
+
for (const key of keys) {
|
|
59
|
+
const path = joinPath(prefix, key);
|
|
60
|
+
const hasBefore = Object.prototype.hasOwnProperty.call(live, key);
|
|
61
|
+
const hasAfter = Object.prototype.hasOwnProperty.call(next, key);
|
|
62
|
+
const before = live[key];
|
|
63
|
+
const after = next[key];
|
|
64
|
+
// An object on both sides, or added or removed whole, is walked to its
|
|
65
|
+
// leaves. An empty object added or removed has none, so it stands as itself.
|
|
66
|
+
const walkable = (has, v) => !has || isPlainObject(v);
|
|
67
|
+
if ((isPlainObject(before) || isPlainObject(after)) && walkable(hasBefore, before) && walkable(hasAfter, after)) {
|
|
68
|
+
const inner = diffFields(hasBefore ? before : {}, hasAfter ? after : {}, path);
|
|
69
|
+
if (inner.length > 0 || hasBefore === hasAfter) {
|
|
70
|
+
changes.push(...inner);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (hasBefore && hasAfter && jsonEqual(before, after))
|
|
75
|
+
continue;
|
|
76
|
+
changes.push({ path, ...(hasBefore ? { before } : {}), ...(hasAfter ? { after } : {}) });
|
|
77
|
+
}
|
|
78
|
+
return changes;
|
|
79
|
+
}
|