@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
|
@@ -31,6 +31,8 @@ import { wk8403 } from "./wk8403";
|
|
|
31
31
|
import { argo002 } from "./argo002";
|
|
32
32
|
import { argo003 } from "./argo003";
|
|
33
33
|
import { argo005 } from "./argo005";
|
|
34
|
+
import { flux002 } from "./flux002";
|
|
35
|
+
import { flux003 } from "./flux003";
|
|
34
36
|
|
|
35
37
|
function makeCtx(yaml: string): PostSynthContext {
|
|
36
38
|
return {
|
|
@@ -1659,3 +1661,133 @@ describe("ARGO005: Application source path exists", () => {
|
|
|
1659
1661
|
expect(argo005.check(ctx).length).toBe(0);
|
|
1660
1662
|
});
|
|
1661
1663
|
});
|
|
1664
|
+
|
|
1665
|
+
// ── FLUX002: Kustomization sourceRef references a declared source ─────────────
|
|
1666
|
+
|
|
1667
|
+
function fluxKustomization(name: string, spec: Record<string, unknown>) {
|
|
1668
|
+
return {
|
|
1669
|
+
apiVersion: "kustomize.toolkit.fluxcd.io/v1",
|
|
1670
|
+
kind: "Kustomization",
|
|
1671
|
+
metadata: { name, namespace: "flux-system" },
|
|
1672
|
+
spec: { interval: "10m", path: "./k8s", prune: true, wait: true, ...spec },
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function fluxGitRepository(name: string) {
|
|
1676
|
+
return {
|
|
1677
|
+
apiVersion: "source.toolkit.fluxcd.io/v1",
|
|
1678
|
+
kind: "GitRepository",
|
|
1679
|
+
metadata: { name, namespace: "flux-system" },
|
|
1680
|
+
spec: { interval: "5m", url: `https://example.com/${name}`, ref: { branch: "main" } },
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
const gitSourceRef = (name: string) => ({ sourceRef: { kind: "GitRepository", name } });
|
|
1684
|
+
|
|
1685
|
+
describe("FLUX002: Kustomization sourceRef references a declared source", () => {
|
|
1686
|
+
test("metadata", () => {
|
|
1687
|
+
expect(flux002.id).toBe("FLUX002");
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
test("flags a Kustomization referencing an undeclared GitRepository", () => {
|
|
1691
|
+
const ctx = manifestsCtx(fluxKustomization("hello", gitSourceRef("home-chant")));
|
|
1692
|
+
const diags = flux002.check(ctx);
|
|
1693
|
+
expect(diags.length).toBe(1);
|
|
1694
|
+
expect(diags[0].checkId).toBe("FLUX002");
|
|
1695
|
+
expect(diags[0].severity).toBe("error");
|
|
1696
|
+
expect(diags[0].message).toContain("home-chant");
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
test("passes when the GitRepository is declared in the same build", () => {
|
|
1700
|
+
const ctx = manifestsCtx(
|
|
1701
|
+
fluxGitRepository("home-chant"),
|
|
1702
|
+
fluxKustomization("hello", gitSourceRef("home-chant")),
|
|
1703
|
+
);
|
|
1704
|
+
expect(flux002.check(ctx).length).toBe(0);
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
test("does NOT flag the bootstrap-created flux-system source", () => {
|
|
1708
|
+
const ctx = manifestsCtx(fluxKustomization("mealie", gitSourceRef("flux-system")));
|
|
1709
|
+
expect(flux002.check(ctx).length).toBe(0);
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
test("matches on source kind — a GitRepository does not satisfy an OCIRepository ref", () => {
|
|
1713
|
+
const ctx = manifestsCtx(
|
|
1714
|
+
fluxGitRepository("fountain"),
|
|
1715
|
+
fluxKustomization("fountain", { sourceRef: { kind: "OCIRepository", name: "fountain" } }),
|
|
1716
|
+
);
|
|
1717
|
+
const diags = flux002.check(ctx);
|
|
1718
|
+
expect(diags.length).toBe(1);
|
|
1719
|
+
expect(diags[0].message).toContain("OCIRepository");
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
test("flags a Kustomization with no sourceRef at all", () => {
|
|
1723
|
+
const ctx = manifestsCtx(fluxKustomization("hello", {}));
|
|
1724
|
+
const diags = flux002.check(ctx);
|
|
1725
|
+
expect(diags.length).toBe(1);
|
|
1726
|
+
expect(diags[0].message).toContain("no spec.sourceRef.name");
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
test("skips kustomize.config.k8s.io Kustomizations (not Flux CRs)", () => {
|
|
1730
|
+
const ctx = manifestsCtx({
|
|
1731
|
+
apiVersion: "kustomize.config.k8s.io/v1beta1",
|
|
1732
|
+
kind: "Kustomization",
|
|
1733
|
+
metadata: { name: "overlay" },
|
|
1734
|
+
spec: {},
|
|
1735
|
+
});
|
|
1736
|
+
expect(flux002.check(ctx).length).toBe(0);
|
|
1737
|
+
});
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
// ── FLUX003: Kustomization dependsOn names declared Kustomizations ────────────
|
|
1741
|
+
|
|
1742
|
+
describe("FLUX003: Kustomization dependsOn names declared Kustomizations", () => {
|
|
1743
|
+
test("metadata", () => {
|
|
1744
|
+
expect(flux003.id).toBe("FLUX003");
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
test("flags a dependsOn entry nothing in the build declares", () => {
|
|
1748
|
+
const ctx = manifestsCtx(
|
|
1749
|
+
fluxKustomization("hello", { ...gitSourceRef("flux-system"), dependsOn: [{ name: "cert-manger" }] }),
|
|
1750
|
+
);
|
|
1751
|
+
const diags = flux003.check(ctx);
|
|
1752
|
+
expect(diags.length).toBe(1);
|
|
1753
|
+
expect(diags[0].checkId).toBe("FLUX003");
|
|
1754
|
+
expect(diags[0].severity).toBe("warning");
|
|
1755
|
+
expect(diags[0].message).toContain("cert-manger");
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
test("passes when every dependency is declared", () => {
|
|
1759
|
+
const ctx = manifestsCtx(
|
|
1760
|
+
fluxKustomization("cert-manager", gitSourceRef("flux-system")),
|
|
1761
|
+
fluxKustomization("traefik", gitSourceRef("flux-system")),
|
|
1762
|
+
fluxKustomization("hello", {
|
|
1763
|
+
...gitSourceRef("flux-system"),
|
|
1764
|
+
dependsOn: [{ name: "cert-manager" }, { name: "traefik" }],
|
|
1765
|
+
}),
|
|
1766
|
+
);
|
|
1767
|
+
expect(flux003.check(ctx).length).toBe(0);
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
test("flags a self-referencing dependsOn entry", () => {
|
|
1771
|
+
const ctx = manifestsCtx(
|
|
1772
|
+
fluxKustomization("hello", { ...gitSourceRef("flux-system"), dependsOn: [{ name: "hello" }] }),
|
|
1773
|
+
);
|
|
1774
|
+
const diags = flux003.check(ctx);
|
|
1775
|
+
expect(diags.length).toBe(1);
|
|
1776
|
+
expect(diags[0].message).toContain("itself");
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
test("reports every broken edge, not just the first", () => {
|
|
1780
|
+
const ctx = manifestsCtx(
|
|
1781
|
+
fluxKustomization("hello", {
|
|
1782
|
+
...gitSourceRef("flux-system"),
|
|
1783
|
+
dependsOn: [{ name: "nope-a" }, { name: "nope-b" }],
|
|
1784
|
+
}),
|
|
1785
|
+
);
|
|
1786
|
+
expect(flux003.check(ctx).length).toBe(2);
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
test("skips a Kustomization without dependsOn", () => {
|
|
1790
|
+
const ctx = manifestsCtx(fluxKustomization("hello", gitSourceRef("flux-system")));
|
|
1791
|
+
expect(flux003.check(ctx).length).toBe(0);
|
|
1792
|
+
});
|
|
1793
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
2
|
+
import { findResourceLiterals, getNestedObject, getNestedString, getProp, lineCol } from "./argo-ast";
|
|
3
|
+
import * as ts from "typescript";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FLUX001: GitRepository must pin spec.ref
|
|
7
|
+
*
|
|
8
|
+
* A Flux `GitRepository` with no `spec.ref` falls back to the `master` branch,
|
|
9
|
+
* which on most repos no longer exists — the source stalls with a checkout
|
|
10
|
+
* error, and every Kustomization downstream of it stalls too. Real estates pin
|
|
11
|
+
* a branch (or tag) on every source; `FluxGitSource` always emits one. Flag
|
|
12
|
+
* hand-written `GitRepository` literals whose spec carries a `url` but no
|
|
13
|
+
* `ref`.
|
|
14
|
+
*
|
|
15
|
+
* Bad: new GitRepository({ spec: { url: "https://github.com/acme/infra" } })
|
|
16
|
+
* Good: new GitRepository({ spec: { url: "https://github.com/acme/infra", ref: { branch: "main" } } })
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const fluxSourceRefPinRule: LintRule = {
|
|
20
|
+
id: "FLUX001",
|
|
21
|
+
severity: "warning",
|
|
22
|
+
category: "correctness",
|
|
23
|
+
description:
|
|
24
|
+
"Flux GitRepository must pin spec.ref (branch, tag, semver, or commit) — the unset default is the master branch",
|
|
25
|
+
|
|
26
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
27
|
+
const { sourceFile } = context;
|
|
28
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
29
|
+
|
|
30
|
+
for (const { literal } of findResourceLiterals(sourceFile, new Set(["GitRepository"]))) {
|
|
31
|
+
const spec = getNestedObject(literal, ["spec"]);
|
|
32
|
+
// No spec object literal to inspect — leave it to other tooling.
|
|
33
|
+
if (!spec) continue;
|
|
34
|
+
if (getProp(spec, "ref") !== undefined) continue;
|
|
35
|
+
|
|
36
|
+
const anchor: ts.Node = spec;
|
|
37
|
+
const { line, column } = lineCol(sourceFile, anchor);
|
|
38
|
+
const name = getNestedString(literal, ["metadata", "name"]) ?? "(unnamed)";
|
|
39
|
+
|
|
40
|
+
diagnostics.push({
|
|
41
|
+
file: sourceFile.fileName,
|
|
42
|
+
line,
|
|
43
|
+
column,
|
|
44
|
+
ruleId: "FLUX001",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message: `GitRepository "${name}" has no spec.ref — Flux falls back to the master branch. Pin a branch, tag, semver range, or commit.`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return diagnostics;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -4,6 +4,7 @@ import { latestImageTagRule } from "./latest-image-tag";
|
|
|
4
4
|
import { missingResourceLimitsRule } from "./missing-resource-limits";
|
|
5
5
|
import { argoAutomatedPruneRule } from "./argo-automated-prune";
|
|
6
6
|
import { argoAppSetSingleProjectRule } from "./argo-appset-single-project";
|
|
7
|
+
import { fluxSourceRefPinRule } from "./flux-source-ref-pin";
|
|
7
8
|
import * as ts from "typescript";
|
|
8
9
|
|
|
9
10
|
function createContext(code: string) {
|
|
@@ -368,3 +369,53 @@ describe("ARGO004: ApplicationSet single AppProject", () => {
|
|
|
368
369
|
expect(diags[0].message).toContain("placeholder");
|
|
369
370
|
});
|
|
370
371
|
});
|
|
372
|
+
|
|
373
|
+
describe("FLUX001: GitRepository must pin spec.ref", () => {
|
|
374
|
+
test("rule metadata", () => {
|
|
375
|
+
expect(fluxSourceRefPinRule.id).toBe("FLUX001");
|
|
376
|
+
expect(fluxSourceRefPinRule.severity).toBe("warning");
|
|
377
|
+
expect(fluxSourceRefPinRule.category).toBe("correctness");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("flags a GitRepository with a url but no ref", () => {
|
|
381
|
+
const ctx = createContext(`
|
|
382
|
+
new GitRepository({
|
|
383
|
+
metadata: { name: "home-chant" },
|
|
384
|
+
spec: { interval: "5m", url: "https://github.com/jhgaylor/home-chant" },
|
|
385
|
+
});
|
|
386
|
+
`);
|
|
387
|
+
const diags = fluxSourceRefPinRule.check(ctx);
|
|
388
|
+
expect(diags.length).toBe(1);
|
|
389
|
+
expect(diags[0].ruleId).toBe("FLUX001");
|
|
390
|
+
expect(diags[0].message).toContain("home-chant");
|
|
391
|
+
expect(diags[0].message).toContain("master");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("does NOT flag a branch pin", () => {
|
|
395
|
+
const ctx = createContext(`
|
|
396
|
+
new GitRepository({
|
|
397
|
+
metadata: { name: "home-chant" },
|
|
398
|
+
spec: { url: "https://github.com/jhgaylor/home-chant", ref: { branch: "main" } },
|
|
399
|
+
});
|
|
400
|
+
`);
|
|
401
|
+
expect(fluxSourceRefPinRule.check(ctx).length).toBe(0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("does NOT flag a tag pin", () => {
|
|
405
|
+
const ctx = createContext(`
|
|
406
|
+
new GitRepository({
|
|
407
|
+
metadata: { name: "infra" },
|
|
408
|
+
spec: { url: "https://example.com/infra", ref: { tag: "v1.2.3" } },
|
|
409
|
+
});
|
|
410
|
+
`);
|
|
411
|
+
expect(fluxSourceRefPinRule.check(ctx).length).toBe(0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("skips a GitRepository with no inspectable spec literal", () => {
|
|
415
|
+
const ctx = createContext(`
|
|
416
|
+
const spec = buildSpec();
|
|
417
|
+
new GitRepository({ metadata: { name: "dynamic" }, spec });
|
|
418
|
+
`);
|
|
419
|
+
expect(fluxSourceRefPinRule.check(ctx).length).toBe(0);
|
|
420
|
+
});
|
|
421
|
+
});
|
package/src/plugin.test.ts
CHANGED
|
@@ -39,6 +39,8 @@ describe("k8sPlugin", () => {
|
|
|
39
39
|
const ids = checks.map((c) => c.id);
|
|
40
40
|
expect(ids).toContain("WK8101");
|
|
41
41
|
expect(ids).toContain("ARGO002");
|
|
42
|
+
expect(ids).toContain("FLUX002");
|
|
43
|
+
expect(ids).toContain("FLUX003");
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
test("intrinsics() returns empty array", () => {
|
package/src/plugin.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { latestImageTagRule } from "./lint/rules/latest-image-tag";
|
|
|
21
21
|
import { missingResourceLimitsRule } from "./lint/rules/missing-resource-limits";
|
|
22
22
|
import { argoAutomatedPruneRule } from "./lint/rules/argo-automated-prune";
|
|
23
23
|
import { argoAppSetSingleProjectRule } from "./lint/rules/argo-appset-single-project";
|
|
24
|
+
import { fluxSourceRefPinRule } from "./lint/rules/flux-source-ref-pin";
|
|
24
25
|
import { k8sCompletions } from "./lsp/completions";
|
|
25
26
|
import { k8sHover } from "./lsp/hover";
|
|
26
27
|
import { K8sParser } from "./import/parser";
|
|
@@ -28,6 +29,8 @@ import { K8sGenerator } from "./import/generator";
|
|
|
28
29
|
import { k8sDeepNormalizationHooks } from "./deep-observe-hooks";
|
|
29
30
|
import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
30
31
|
import { k8sConfigSchema } from "./config-schema";
|
|
32
|
+
import type { K8sChantConfig } from "./config";
|
|
33
|
+
import { renderKustomizeRoots } from "./kustomize/root";
|
|
31
34
|
|
|
32
35
|
export const k8sPlugin: LexiconPlugin = {
|
|
33
36
|
name: "k8s",
|
|
@@ -62,6 +65,7 @@ export const k8sPlugin: LexiconPlugin = {
|
|
|
62
65
|
missingResourceLimitsRule,
|
|
63
66
|
argoAutomatedPruneRule,
|
|
64
67
|
argoAppSetSingleProjectRule,
|
|
68
|
+
fluxSourceRefPinRule,
|
|
65
69
|
];
|
|
66
70
|
},
|
|
67
71
|
|
|
@@ -69,6 +73,18 @@ export const k8sPlugin: LexiconPlugin = {
|
|
|
69
73
|
return postSynthCheckList;
|
|
70
74
|
},
|
|
71
75
|
|
|
76
|
+
// #1548 piece 3 — kustomization dirs declared as build roots. Each entry in
|
|
77
|
+
// `k8s.kustomize.roots` renders at build time (same injectable runner as
|
|
78
|
+
// the kustomize-apply capability: `kustomize build`, `kubectl kustomize`
|
|
79
|
+
// fallback) and the documents join the build as verbatim manifest entities:
|
|
80
|
+
// serialized with ownership stamping, checked post-synth, observed by
|
|
81
|
+
// `lifecycle diff --live`. See ./kustomize/root.ts.
|
|
82
|
+
async buildRoots(ctx) {
|
|
83
|
+
const roots = (ctx.config as { k8s?: K8sChantConfig }).k8s?.kustomize?.roots ?? [];
|
|
84
|
+
if (roots.length === 0) return { entities: new Map() };
|
|
85
|
+
return renderKustomizeRoots({ projectRoot: ctx.projectRoot, roots });
|
|
86
|
+
},
|
|
87
|
+
|
|
72
88
|
// K8s YAML has no template interpolation functions like CloudFormation's
|
|
73
89
|
// Fn::Sub or GitLab's !reference. Cross-resource references are handled by
|
|
74
90
|
// the AttrRef system in the serializer (resolves to metadata.name).
|
|
@@ -624,6 +640,37 @@ const { deployment, service, serviceMonitor, prometheusRule } = MonitoredService
|
|
|
624
640
|
},
|
|
625
641
|
],
|
|
626
642
|
},
|
|
643
|
+
{
|
|
644
|
+
file: "chant-k8s-flux.md",
|
|
645
|
+
name: "chant-k8s-flux",
|
|
646
|
+
description: "Flux CD composites — FluxGitSource + FluxAppFor, the one-source-many-apps shape, dependsOn ordering, the FLUX rules, and the flux-reconcile deploy step",
|
|
647
|
+
triggers: [
|
|
648
|
+
{ type: "context", value: "flux" },
|
|
649
|
+
{ type: "context", value: "fluxcd" },
|
|
650
|
+
{ type: "context", value: "flux cd" },
|
|
651
|
+
{ type: "context", value: "gitops" },
|
|
652
|
+
{ type: "context", value: "gitrepository" },
|
|
653
|
+
{ type: "context", value: "kustomization" },
|
|
654
|
+
{ type: "context", value: "source-controller" },
|
|
655
|
+
{ type: "context", value: "kustomize-controller" },
|
|
656
|
+
{ type: "context", value: "reconcile" },
|
|
657
|
+
],
|
|
658
|
+
parameters: [],
|
|
659
|
+
examples: [
|
|
660
|
+
{
|
|
661
|
+
title: "Source + Kustomization from a build target",
|
|
662
|
+
description: "Reconcile a Chant build target with Flux",
|
|
663
|
+
input: "Deploy my app through Flux",
|
|
664
|
+
output: "import { FluxGitSource, FluxAppFor } from \"@intentius/chant-lexicon-k8s\";\n\nexport const source = FluxGitSource(\"infra\", {\n url: \"https://github.com/acme/infra\",\n branch: \"main\",\n});\n\nexport const app = FluxAppFor(\"app\", {\n source,\n path: \"./dist/apps/app\",\n dependsOn: [\"platform\"],\n});",
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
title: "One source, many apps",
|
|
668
|
+
description: "The multi-app repo shape — one GitRepository shared by every Kustomization",
|
|
669
|
+
input: "Reconcile platform, api, and web from one repo with ordering",
|
|
670
|
+
output: "import { FluxGitSource, FluxAppFor } from \"@intentius/chant-lexicon-k8s\";\n\nconst source = FluxGitSource(\"infra\", { url: \"https://github.com/acme/infra\" });\n\nexport const platform = FluxAppFor(\"platform\", { source, path: \"./dist/platform\" });\nexport const api = FluxAppFor(\"api\", { source, path: \"./dist/apps/api\", dependsOn: [\"platform\"] });\nexport const web = FluxAppFor(\"web\", { source, path: \"./dist/apps/web\", dependsOn: [\"platform\", \"api\"] });",
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
},
|
|
627
674
|
]),
|
|
628
675
|
|
|
629
676
|
async describeResources(options) {
|
package/src/serializer.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
|
14
14
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
15
15
|
import { emitYAML } from "@intentius/chant/yaml";
|
|
16
16
|
import { isDefaultLabels, isDefaultAnnotations, type DefaultLabels, type DefaultAnnotations } from "./default-labels";
|
|
17
|
+
import { isRenderedManifestEntity } from "./kustomize/rendered-entity";
|
|
17
18
|
|
|
18
19
|
const require = createRequire(import.meta.url);
|
|
19
20
|
|
|
@@ -219,10 +220,10 @@ function toYAMLValue(value: unknown, entityNames: Map<Declarable, string>): unkn
|
|
|
219
220
|
export const k8sSerializer: Serializer = {
|
|
220
221
|
name: "k8s",
|
|
221
222
|
rulePrefix: "WK8",
|
|
222
|
-
// The Argo CD checks are
|
|
223
|
-
// their ids are published — renaming them would break every
|
|
224
|
-
// ARGO001` already written (#1349).
|
|
225
|
-
extraRulePrefixes: ["ARGO"],
|
|
223
|
+
// The Argo CD and Flux checks are distinct product surfaces this lexicon
|
|
224
|
+
// covers, and their ids are published — renaming them would break every
|
|
225
|
+
// `chant-disable ARGO001` already written (#1349).
|
|
226
|
+
extraRulePrefixes: ["ARGO", "FLUX"],
|
|
226
227
|
|
|
227
228
|
serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[], context?: SerializeContext): string {
|
|
228
229
|
// Build reverse map: entity → name
|
|
@@ -255,6 +256,35 @@ export const k8sSerializer: Serializer = {
|
|
|
255
256
|
if (isPropertyDeclarable(entity)) continue;
|
|
256
257
|
if (isDefaultLabels(entity) || isDefaultAnnotations(entity)) continue;
|
|
257
258
|
|
|
259
|
+
// A kustomize build root's document (#1548 piece 3) is render-final:
|
|
260
|
+
// the overlay decided every field, so the props ARE the manifest and
|
|
261
|
+
// the spec-inference heuristics below (built for typed declarables)
|
|
262
|
+
// must not reshape it — a doc whose top-level fields aren't `spec`
|
|
263
|
+
// (webhooks, rules on a CRD instance) would otherwise be re-nested.
|
|
264
|
+
// It still gets the exact default-label/annotation merge every
|
|
265
|
+
// discovered resource gets, which is what stamps ownership on it.
|
|
266
|
+
if (isRenderedManifestEntity(entity)) {
|
|
267
|
+
const manifest = { ...entity.props };
|
|
268
|
+
const metadata = { ...((manifest.metadata as Record<string, unknown> | undefined) ?? {}) };
|
|
269
|
+
if (Object.keys(defaultLabelEntries).length > 0) {
|
|
270
|
+
metadata.labels = { ...defaultLabelEntries, ...((metadata.labels ?? {}) as Record<string, unknown>) };
|
|
271
|
+
}
|
|
272
|
+
if (Object.keys(defaultAnnotationEntries).length > 0) {
|
|
273
|
+
metadata.annotations = {
|
|
274
|
+
...defaultAnnotationEntries,
|
|
275
|
+
...((metadata.annotations ?? {}) as Record<string, unknown>),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
manifest.metadata = metadata;
|
|
279
|
+
const yamlDoc = emitK8sManifest(manifest);
|
|
280
|
+
if (manifest.kind === "Namespace") {
|
|
281
|
+
namespaceDocs.push(yamlDoc);
|
|
282
|
+
} else {
|
|
283
|
+
otherDocs.push(yamlDoc);
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
258
288
|
const entityType = (entity as unknown as Record<string, unknown>).entityType as string;
|
|
259
289
|
const gvk = resolveGVK(entityType);
|
|
260
290
|
if (!gvk) continue;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-k8s-flux
|
|
3
|
+
description: Flux CD composites for GitOps reconciliation — FluxGitSource + FluxAppFor, the one-source-many-apps shape, dependsOn ordering, the FLUX rules, and the flux-reconcile deploy step
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Flux CD Composites
|
|
8
|
+
|
|
9
|
+
Chant authors typed infrastructure into manifests. Flux continuously reconciles those manifests into a cluster from git. These composites are the opt-in bridge — the k8s lexicon itself stays runtime-agnostic and only emits YAML; nothing here is implied unless you reach for it.
|
|
10
|
+
|
|
11
|
+
## The split
|
|
12
|
+
|
|
13
|
+
| Layer | Owns | In Chant |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| **Chant** | Authoring typed infra → manifests, committed to git | the lexicons |
|
|
16
|
+
| **Flux** | Continuously reconciling those manifests (source-controller fetches, kustomize-controller applies) | `FluxGitSource` / `FluxAppFor` |
|
|
17
|
+
| **Chant again** | Reading convergence back | `flux-reconcile` deploy step, `chant components status --live` |
|
|
18
|
+
|
|
19
|
+
Flux never learns Chant exists — it fetches a git path and applies what it finds there. Chant's job ends at emitting the manifests and the Flux CRs that point at them.
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
The Flux controllers must be installed in the target cluster before applying any Flux CRs:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
kubectl apply -f https://github.com/fluxcd/flux2/releases/download/v2.9.1/install.yaml
|
|
27
|
+
kubectl -n flux-system wait deploy --all --for=condition=Available --timeout=180s
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
(Or `flux install` / `flux bootstrap` with the Flux CLI. `flux bootstrap` also creates a `flux-system` `GitRepository` — FLUX002 knows about it, see below.)
|
|
31
|
+
|
|
32
|
+
## When to use which
|
|
33
|
+
|
|
34
|
+
| Composite | Use case |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `FluxGitSource` | Declare the git repo the source-controller fetches — **once per repo** |
|
|
37
|
+
| `FluxAppFor` | One `Kustomization` reconciling one path out of a declared source — once per app |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## FluxGitSource — declare the repo once
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { FluxGitSource } from "@intentius/chant-lexicon-k8s";
|
|
45
|
+
|
|
46
|
+
export const source = FluxGitSource("home-chant", {
|
|
47
|
+
url: "https://github.com/jhgaylor/home-chant",
|
|
48
|
+
branch: "main",
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`FluxGitSource(name, options)` returns `{ gitRepository }` — a single `K8s::Flux::GitRepository` in `flux-system`. Options:
|
|
53
|
+
|
|
54
|
+
- **`url`** (required) — the repo the source-controller fetches.
|
|
55
|
+
- **`branch`** — defaults to `"main"`. **`tag`** pins a tag instead and wins over `branch`.
|
|
56
|
+
- **`interval`** — fetch interval, default `"5m"`.
|
|
57
|
+
- **`secretRef`** — name of the Secret holding git credentials, for private repos.
|
|
58
|
+
- **`fluxNamespace`** — where the CR lives, default `"flux-system"`.
|
|
59
|
+
|
|
60
|
+
> **FLUX001** — a hand-written `GitRepository` whose spec has a `url` but no `ref` is flagged: an unset `spec.ref` falls back to the `master` branch, which on most repos no longer exists, so the source stalls with a checkout error and every Kustomization downstream stalls too. `FluxGitSource` always emits a `ref` (branch or tag), so composite output is FLUX001-clean by construction.
|
|
61
|
+
|
|
62
|
+
## FluxAppFor — one Kustomization per app
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { FluxAppFor } from "@intentius/chant-lexicon-k8s";
|
|
66
|
+
|
|
67
|
+
export const hello = FluxAppFor("hello-chant", {
|
|
68
|
+
source, // the FluxGitSource result above
|
|
69
|
+
path: "./apps/hello-chant/k8s",
|
|
70
|
+
targetNamespace: "default",
|
|
71
|
+
dependsOn: ["cert-manager", "traefik"],
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`FluxAppFor(target, options)` returns `{ kustomization }` — a single `K8s::Flux::Kustomization`. Defaults are taken from real Flux estates, not the docs:
|
|
76
|
+
|
|
77
|
+
- **`interval`** — `"10m"` (the source polls at 5m; reconcile less often than you fetch).
|
|
78
|
+
- **`prune: true`** — resources that disappear from the source are deleted.
|
|
79
|
+
- **`wait: true`** — the Kustomization reports Ready only when the reconciled resources are ready.
|
|
80
|
+
- **`targetNamespace`** — optional; sets `spec.targetNamespace` (kustomize-controller stamps it on every reconciled resource).
|
|
81
|
+
- **`timeout`**, **`suspend`**, **`serviceAccountName`** — pass-throughs for the matching spec fields.
|
|
82
|
+
|
|
83
|
+
### The source option
|
|
84
|
+
|
|
85
|
+
`source` accepts three shapes:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
FluxAppFor("app", { source, path: "./apps/app" }); // FluxGitSource result
|
|
89
|
+
FluxAppFor("app", { source: "flux-system", path: "./apps/app" }); // name of an existing GitRepository
|
|
90
|
+
FluxAppFor("app", { source: { kind: "OCIRepository", name: "images" }, path: "./apps/app" }); // explicit ref
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The first is the normal case. The second targets a source declared elsewhere — typically the bootstrap-created `flux-system` repo. The third covers `OCIRepository` / `Bucket` sources.
|
|
94
|
+
|
|
95
|
+
> **FLUX002** — every `Kustomization.spec.sourceRef` must name a source the build declares (`GitRepository`, `OCIRepository`, or `Bucket`), or the kustomize-controller waits forever on an artifact that never arrives. The bootstrap-created `flux-system` `GitRepository` is exempt — it always exists on a bootstrapped cluster, the Flux analogue of ARGO002's built-in `default` project.
|
|
96
|
+
|
|
97
|
+
### One source, many apps
|
|
98
|
+
|
|
99
|
+
The common estate shape is **one `GitRepository` shared by many `Kustomization`s** — a multi-app repo with one Kustomization per path. A `GitRepository` per app is the mistake the composite split makes hard: declare the source once, hand its result to every `FluxAppFor`.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const source = FluxGitSource("infra", { url: "https://github.com/acme/infra" });
|
|
103
|
+
|
|
104
|
+
export const platform = FluxAppFor("platform", { source, path: "./dist/platform" });
|
|
105
|
+
export const api = FluxAppFor("api", { source, path: "./dist/apps/api", dependsOn: ["platform"] });
|
|
106
|
+
export const web = FluxAppFor("web", { source, path: "./dist/apps/web", dependsOn: ["platform", "api"] });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### dependsOn — ordering as a validated name list
|
|
110
|
+
|
|
111
|
+
`dependsOn` is a plain string list rendered to `spec.dependsOn`; the Kustomization stays pending until every named Kustomization is Ready. In raw YAML those names have no referential integrity — a typo, or an entry left behind after a rename, stalls the app silently.
|
|
112
|
+
|
|
113
|
+
> **FLUX003** — every `dependsOn` entry is joined against the Kustomizations the build actually declares. An entry naming nothing is a warning (not an error — estates legitimately split infra and apps across repos, so `cert-manager` may be declared by a build this one never sees). A self-referencing entry is always flagged: Flux can never satisfy it.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Deploying — the flux-reconcile step
|
|
118
|
+
|
|
119
|
+
Applying `dist/flux.yaml` with `kubectl` works. For the component model, `fluxReconcile` is the typed deploy leaf (the sibling of `argo-app`):
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { phase, type Component } from "@intentius/chant/components/component";
|
|
123
|
+
import { fluxReconcile } from "@intentius/chant-lexicon-k8s/components";
|
|
124
|
+
|
|
125
|
+
export const bootstrap: Component = {
|
|
126
|
+
name: "flux-bootstrap",
|
|
127
|
+
archetype: "service",
|
|
128
|
+
dependsOn: [],
|
|
129
|
+
deploy: [
|
|
130
|
+
phase("Reconcile", [
|
|
131
|
+
fluxReconcile({
|
|
132
|
+
manifest: "dist/flux.yaml",
|
|
133
|
+
stack: "my-estate",
|
|
134
|
+
noRollback: "server-side apply keeps no previous object state; the declared source is the restore path",
|
|
135
|
+
}),
|
|
136
|
+
]),
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
What it does:
|
|
142
|
+
|
|
143
|
+
1. **Applies** the Flux CRs through the same server-side apply `kubectl-apply` uses — ownership stamping, marker-scoped prune, stack labels all identical.
|
|
144
|
+
2. **Waits for Ready, sources first.** A Kustomization cannot become Ready before its GitRepository has an artifact, so gating on `source.toolkit.fluxcd.io` kinds first surfaces a wedged clone as the source's error rather than a reconciler timeout downstream. The generic `waitForReady` handles the rest of the toolkit (kstatus-conformant), with fail-fast on wedge reasons like `BuildFailed` or `UpgradeFailed` from the readiness registry.
|
|
145
|
+
3. **Refuses non-Flux manifests.** If the manifest applies no Flux CR at all, the step errors — a plain manifest belongs on `kubectl-apply`, and two things applying the same resources is the failure mode worth engineering against.
|
|
146
|
+
|
|
147
|
+
It is a `needs-opt-out` capability (COMP003): every step needs `noRollback: "<reason>"`, a component `rollback` phase, or a sibling safety step — same posture as `kubectl-apply`, and for the same reason.
|
|
148
|
+
|
|
149
|
+
## Reading convergence back
|
|
150
|
+
|
|
151
|
+
The build stamps `app.kubernetes.io/managed-by: chant` and (with `ownership` configured) `chant.intentius.io/stack: <stack>` labels on every resource. Flux applies the manifests, but the labels travel with them — so `chant components status --live` attributes the running workloads to the stack through the k8s lexicon's label-selector read, regardless of who did the applying. Flux prunes by its own labels; chant observes by its own labels; neither needs the other's.
|
|
152
|
+
|
|
153
|
+
## Troubleshooting
|
|
154
|
+
|
|
155
|
+
| Symptom | Likely cause | Fix |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| Kustomization stuck, no artifact | Source stalled — unset `spec.ref` fell back to `master` | Pin a branch/tag (FLUX001); `FluxGitSource` does this by default |
|
|
158
|
+
| Kustomization waits forever | `sourceRef` names an undeclared source | Declare it with `FluxGitSource`, or point at the bootstrap `flux-system` repo (FLUX002) |
|
|
159
|
+
| App pending, dependencies "not ready" | `dependsOn` names a Kustomization that doesn't exist (typo/rename) | Fix the name (FLUX003), or confirm the other repo declares it |
|
|
160
|
+
| App pending forever, no error | `dependsOn` self-reference | Remove the self-edge (FLUX003 flags it) |
|
|
161
|
+
| Resources deleted unexpectedly | `prune: true` (the default) and the path stopped emitting them | Intended GitOps behavior; pass `prune: false` to opt out |
|
|
162
|
+
| `flux-reconcile` step errors immediately | Manifest contains no Flux CR | Use `kubectl-apply` for plain manifests |
|
package/src/validate.ts
CHANGED
|
@@ -42,5 +42,9 @@ export async function validate(opts?: { basePath?: string }): Promise<ValidateRe
|
|
|
42
42
|
lexiconJsonFilename: "lexicon-k8s.json",
|
|
43
43
|
requiredNames: REQUIRED_NAMES,
|
|
44
44
|
basePath,
|
|
45
|
+
// chant #1475 — the k8s baseline was re-baselined together with the k3s
|
|
46
|
+
// CRD work (#1605), so a release must now verify the API it ships is the
|
|
47
|
+
// reviewed one. Runs only under CHANT_RELEASE_GATE=1.
|
|
48
|
+
checkSurfaceSnapshot: true,
|
|
45
49
|
});
|
|
46
50
|
}
|