@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
|
@@ -9,6 +9,7 @@ function p(member: unknown): Record<string, unknown> {
|
|
|
9
9
|
}
|
|
10
10
|
import { StatefulApp } from "./stateful-app";
|
|
11
11
|
import { ArgoAppFor, ArgoAppSetForRegions, registerArgoCluster } from "./argo-app";
|
|
12
|
+
import { FluxGitSource, FluxAppFor } from "./flux-app";
|
|
12
13
|
import { CronWorkload } from "./cron-workload";
|
|
13
14
|
import { AutoscaledService } from "./autoscaled-service";
|
|
14
15
|
import { WorkerPool } from "./worker-pool";
|
|
@@ -3359,6 +3360,121 @@ describe("registerArgoCluster", () => {
|
|
|
3359
3360
|
});
|
|
3360
3361
|
});
|
|
3361
3362
|
|
|
3363
|
+
describe("FluxGitSource", () => {
|
|
3364
|
+
test("returns a single GitRepository", () => {
|
|
3365
|
+
const result = FluxGitSource("home-chant", { url: "https://github.com/jhgaylor/home-chant" });
|
|
3366
|
+
expect(result.gitRepository).toBeDefined();
|
|
3367
|
+
expect((result.gitRepository as any).entityType).toBe("K8s::Flux::GitRepository");
|
|
3368
|
+
});
|
|
3369
|
+
|
|
3370
|
+
test("defaults: 5m interval, main branch, flux-system namespace", () => {
|
|
3371
|
+
const result = FluxGitSource("home-chant", { url: "https://github.com/jhgaylor/home-chant" });
|
|
3372
|
+
const meta = p(result.gitRepository).metadata as any;
|
|
3373
|
+
const spec = p(result.gitRepository).spec as any;
|
|
3374
|
+
expect(meta.namespace).toBe("flux-system");
|
|
3375
|
+
expect(meta.labels["app.kubernetes.io/managed-by"]).toBe("chant");
|
|
3376
|
+
expect(spec.interval).toBe("5m");
|
|
3377
|
+
expect(spec.url).toBe("https://github.com/jhgaylor/home-chant");
|
|
3378
|
+
expect(spec.ref).toEqual({ branch: "main" });
|
|
3379
|
+
});
|
|
3380
|
+
|
|
3381
|
+
test("a tag pin wins over the branch default", () => {
|
|
3382
|
+
const spec = p(FluxGitSource("app", { url: "u", tag: "v1.2.3" }).gitRepository).spec as any;
|
|
3383
|
+
expect(spec.ref).toEqual({ tag: "v1.2.3" });
|
|
3384
|
+
});
|
|
3385
|
+
|
|
3386
|
+
test("secretRef renders as a name reference", () => {
|
|
3387
|
+
const spec = p(FluxGitSource("app", { url: "u", secretRef: "git-creds" }).gitRepository).spec as any;
|
|
3388
|
+
expect(spec.secretRef).toEqual({ name: "git-creds" });
|
|
3389
|
+
});
|
|
3390
|
+
});
|
|
3391
|
+
|
|
3392
|
+
describe("FluxAppFor", () => {
|
|
3393
|
+
const source = () => FluxGitSource("home-chant", { url: "https://github.com/jhgaylor/home-chant" });
|
|
3394
|
+
|
|
3395
|
+
test("returns a single Kustomization (the source is not duplicated)", () => {
|
|
3396
|
+
const result = FluxAppFor("hello-chant", { source: source(), path: "./apps/hello-chant/k8s" });
|
|
3397
|
+
expect(result.kustomization).toBeDefined();
|
|
3398
|
+
expect((result.kustomization as any).entityType).toBe("K8s::Flux::Kustomization");
|
|
3399
|
+
expect((result as any).gitRepository).toBeUndefined();
|
|
3400
|
+
});
|
|
3401
|
+
|
|
3402
|
+
test("defaults: 10m interval, prune, wait, flux-system namespace", () => {
|
|
3403
|
+
const result = FluxAppFor("hello-chant", { source: source(), path: "./apps/hello-chant/k8s" });
|
|
3404
|
+
const meta = p(result.kustomization).metadata as any;
|
|
3405
|
+
const spec = p(result.kustomization).spec as any;
|
|
3406
|
+
expect(meta.name).toBe("hello-chant");
|
|
3407
|
+
expect(meta.namespace).toBe("flux-system");
|
|
3408
|
+
expect(spec.interval).toBe("10m");
|
|
3409
|
+
expect(spec.prune).toBe(true);
|
|
3410
|
+
expect(spec.wait).toBe(true);
|
|
3411
|
+
expect(spec.path).toBe("./apps/hello-chant/k8s");
|
|
3412
|
+
});
|
|
3413
|
+
|
|
3414
|
+
test("derives sourceRef from a FluxGitSource result", () => {
|
|
3415
|
+
const spec = p(FluxAppFor("hello", { source: source(), path: "./k8s" }).kustomization).spec as any;
|
|
3416
|
+
expect(spec.sourceRef).toEqual({ kind: "GitRepository", name: "home-chant", namespace: "flux-system" });
|
|
3417
|
+
});
|
|
3418
|
+
|
|
3419
|
+
test("many apps share one source", () => {
|
|
3420
|
+
const shared = source();
|
|
3421
|
+
const a = FluxAppFor("mealie", { source: shared, path: "./apps/mealie/k8s" });
|
|
3422
|
+
const b = FluxAppFor("ntfy", { source: shared, path: "./apps/ntfy/k8s" });
|
|
3423
|
+
expect((p(a.kustomization).spec as any).sourceRef.name).toBe("home-chant");
|
|
3424
|
+
expect((p(b.kustomization).spec as any).sourceRef.name).toBe("home-chant");
|
|
3425
|
+
});
|
|
3426
|
+
|
|
3427
|
+
test("a string source names an existing GitRepository (flux-system)", () => {
|
|
3428
|
+
const spec = p(FluxAppFor("mealie", { source: "flux-system", path: "./apps/mealie/generated" }).kustomization)
|
|
3429
|
+
.spec as any;
|
|
3430
|
+
expect(spec.sourceRef).toEqual({ kind: "GitRepository", name: "flux-system" });
|
|
3431
|
+
});
|
|
3432
|
+
|
|
3433
|
+
test("an explicit ref carries a non-git source kind", () => {
|
|
3434
|
+
const spec = p(
|
|
3435
|
+
FluxAppFor("fountain", { source: { kind: "OCIRepository", name: "fountain" }, path: "./k8s" }).kustomization,
|
|
3436
|
+
).spec as any;
|
|
3437
|
+
expect(spec.sourceRef).toEqual({ kind: "OCIRepository", name: "fountain" });
|
|
3438
|
+
});
|
|
3439
|
+
|
|
3440
|
+
test("dependsOn is a plain name list rendered to name objects", () => {
|
|
3441
|
+
const spec = p(
|
|
3442
|
+
FluxAppFor("hello", {
|
|
3443
|
+
source: "flux-system",
|
|
3444
|
+
path: "./k8s",
|
|
3445
|
+
dependsOn: ["cert-manager", "traefik"],
|
|
3446
|
+
}).kustomization,
|
|
3447
|
+
).spec as any;
|
|
3448
|
+
expect(spec.dependsOn).toEqual([{ name: "cert-manager" }, { name: "traefik" }]);
|
|
3449
|
+
});
|
|
3450
|
+
|
|
3451
|
+
test("omits dependsOn when the list is empty", () => {
|
|
3452
|
+
const spec = p(FluxAppFor("hello", { source: "flux-system", path: "./k8s" }).kustomization).spec as any;
|
|
3453
|
+
expect(spec.dependsOn).toBeUndefined();
|
|
3454
|
+
});
|
|
3455
|
+
|
|
3456
|
+
test("maps targetNamespace and opt-outs onto the spec", () => {
|
|
3457
|
+
const spec = p(
|
|
3458
|
+
FluxAppFor("cloudflared", {
|
|
3459
|
+
source: "flux-system",
|
|
3460
|
+
path: "./apps/cloudflared/generated",
|
|
3461
|
+
targetNamespace: "cloudflared",
|
|
3462
|
+
wait: false,
|
|
3463
|
+
prune: false,
|
|
3464
|
+
suspend: true,
|
|
3465
|
+
timeout: "3m",
|
|
3466
|
+
serviceAccountName: "flux-applier",
|
|
3467
|
+
}).kustomization,
|
|
3468
|
+
).spec as any;
|
|
3469
|
+
expect(spec.targetNamespace).toBe("cloudflared");
|
|
3470
|
+
expect(spec.wait).toBe(false);
|
|
3471
|
+
expect(spec.prune).toBe(false);
|
|
3472
|
+
expect(spec.suspend).toBe(true);
|
|
3473
|
+
expect(spec.timeout).toBe("3m");
|
|
3474
|
+
expect(spec.serviceAccountName).toBe("flux-applier");
|
|
3475
|
+
});
|
|
3476
|
+
});
|
|
3477
|
+
|
|
3362
3478
|
describe("package index re-exports (regression guard)", () => {
|
|
3363
3479
|
test("Argo composites are reachable from the package entry", async () => {
|
|
3364
3480
|
const pkg: any = await import("../index");
|
|
@@ -3366,4 +3482,10 @@ describe("package index re-exports (regression guard)", () => {
|
|
|
3366
3482
|
expect(typeof pkg.ArgoAppSetForRegions).toBe("function");
|
|
3367
3483
|
expect(typeof pkg.registerArgoCluster).toBe("function");
|
|
3368
3484
|
});
|
|
3485
|
+
|
|
3486
|
+
test("Flux composites are reachable from the package entry", async () => {
|
|
3487
|
+
const pkg: any = await import("../index");
|
|
3488
|
+
expect(typeof pkg.FluxGitSource).toBe("function");
|
|
3489
|
+
expect(typeof pkg.FluxAppFor).toBe("function");
|
|
3490
|
+
});
|
|
3369
3491
|
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flux CD composites — turn Chant build targets into Flux sources and
|
|
3
|
+
* `Kustomization`s.
|
|
4
|
+
*
|
|
5
|
+
* The k8s lexicon stays runtime-agnostic: it only emits manifests. These
|
|
6
|
+
* composites are the opt-in bridge to Flux's reconciliation layer, the
|
|
7
|
+
* `ArgoAppFor` analogue for the source-controller/kustomize-controller pair.
|
|
8
|
+
* Authoring a `GitRepository` + `Kustomization` by hand is ~40 lines of YAML
|
|
9
|
+
* per app; `FluxAppFor` collapses the pair to one call with defaults taken
|
|
10
|
+
* from real Flux estates.
|
|
11
|
+
*
|
|
12
|
+
* - FluxGitSource(name, opts) → a single K8s::Flux::GitRepository
|
|
13
|
+
* - FluxAppFor(target, opts) → a single K8s::Flux::Kustomization
|
|
14
|
+
*
|
|
15
|
+
* The two are split deliberately: the common estate shape is one source
|
|
16
|
+
* shared by many Kustomizations. A `GitRepository` per app is the mistake
|
|
17
|
+
* this split makes hard — declare the source once, hand its result to every
|
|
18
|
+
* `FluxAppFor` that reconciles a path out of it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
22
|
+
import {
|
|
23
|
+
GitRepository as GitRepositoryResource,
|
|
24
|
+
Kustomization as KustomizationResource,
|
|
25
|
+
} from "../generated";
|
|
26
|
+
|
|
27
|
+
// ── Shared types ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** The namespace the Flux controllers run in and watch by default. */
|
|
30
|
+
const FLUX_NAMESPACE = "flux-system";
|
|
31
|
+
|
|
32
|
+
/** Source kinds a Kustomization can reconcile from. */
|
|
33
|
+
export type FluxSourceKind = "GitRepository" | "OCIRepository" | "Bucket";
|
|
34
|
+
|
|
35
|
+
/** An explicit reference to a source declared elsewhere. */
|
|
36
|
+
export interface FluxSourceRef {
|
|
37
|
+
/** Source kind (default "GitRepository"). */
|
|
38
|
+
kind?: FluxSourceKind;
|
|
39
|
+
/** Name of the source object. */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Namespace of the source, when it differs from the Kustomization's. */
|
|
42
|
+
namespace?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── FluxGitSource ────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface FluxGitSourceOptions {
|
|
48
|
+
/** Git repository URL the source-controller fetches. */
|
|
49
|
+
url: string;
|
|
50
|
+
/** Branch to track (default "main"). Ignored when `tag` is set. */
|
|
51
|
+
branch?: string;
|
|
52
|
+
/** Tag to pin instead of a branch. */
|
|
53
|
+
tag?: string;
|
|
54
|
+
/** Fetch interval (default "5m"). */
|
|
55
|
+
interval?: string;
|
|
56
|
+
/** Name of the Secret holding git credentials, for private repos. */
|
|
57
|
+
secretRef?: string;
|
|
58
|
+
/** Namespace the GitRepository lives in (default "flux-system"). */
|
|
59
|
+
fluxNamespace?: string;
|
|
60
|
+
/** Extra labels applied to the GitRepository. */
|
|
61
|
+
labels?: Record<string, string>;
|
|
62
|
+
defaults?: { gitRepository?: Partial<Record<string, unknown>> };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type FluxGitSourceResult = {
|
|
66
|
+
gitRepository: InstanceType<typeof GitRepositoryResource>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const FluxGitRepository = Composite<{ name: string } & FluxGitSourceOptions, FluxGitSourceResult>(
|
|
70
|
+
(props) => {
|
|
71
|
+
const {
|
|
72
|
+
name,
|
|
73
|
+
url,
|
|
74
|
+
branch = "main",
|
|
75
|
+
tag,
|
|
76
|
+
interval = "5m",
|
|
77
|
+
secretRef,
|
|
78
|
+
fluxNamespace = FLUX_NAMESPACE,
|
|
79
|
+
labels = {},
|
|
80
|
+
defaults,
|
|
81
|
+
} = props;
|
|
82
|
+
|
|
83
|
+
const gitRepository = new GitRepositoryResource(mergeDefaults({
|
|
84
|
+
metadata: {
|
|
85
|
+
name,
|
|
86
|
+
namespace: fluxNamespace,
|
|
87
|
+
labels: {
|
|
88
|
+
"app.kubernetes.io/name": name,
|
|
89
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
90
|
+
...labels,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
spec: {
|
|
94
|
+
interval,
|
|
95
|
+
url,
|
|
96
|
+
// An unset spec.ref falls back to `master` — always pin (FLUX001).
|
|
97
|
+
ref: tag !== undefined ? { tag } : { branch },
|
|
98
|
+
...(secretRef !== undefined && { secretRef: { name: secretRef } }),
|
|
99
|
+
},
|
|
100
|
+
}, defaults?.gitRepository));
|
|
101
|
+
|
|
102
|
+
return { gitRepository };
|
|
103
|
+
},
|
|
104
|
+
"FluxGitRepository",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Declare a Flux `GitRepository` source in one call.
|
|
109
|
+
*
|
|
110
|
+
* Declare it once and share it: every `FluxAppFor` that reconciles a path out
|
|
111
|
+
* of the same repo should take this result as its `source`.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { FluxGitSource } from "@intentius/chant-lexicon-k8s";
|
|
116
|
+
*
|
|
117
|
+
* export const source = FluxGitSource("home-chant", {
|
|
118
|
+
* url: "https://github.com/jhgaylor/home-chant",
|
|
119
|
+
* branch: "main",
|
|
120
|
+
* });
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export function FluxGitSource(name: string, options: FluxGitSourceOptions): FluxGitSourceResult {
|
|
124
|
+
return FluxGitRepository({ name, ...options });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── FluxAppFor ───────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export interface FluxAppForOptions {
|
|
130
|
+
/**
|
|
131
|
+
* The source to reconcile from — a `FluxGitSource` result, the name of an
|
|
132
|
+
* existing `GitRepository` (e.g. the bootstrap-created "flux-system"), or an
|
|
133
|
+
* explicit `{ kind, name }` reference for OCIRepository/Bucket sources.
|
|
134
|
+
*/
|
|
135
|
+
source: FluxGitSourceResult | FluxSourceRef | string;
|
|
136
|
+
/** Path within the source holding the manifests (e.g. "./apps/api/k8s"). */
|
|
137
|
+
path: string;
|
|
138
|
+
/** Namespace the reconciled resources land in (sets spec.targetNamespace). */
|
|
139
|
+
targetNamespace?: string;
|
|
140
|
+
/** Reconcile interval (default "10m"). */
|
|
141
|
+
interval?: string;
|
|
142
|
+
/** Delete resources that disappear from the source (default true). */
|
|
143
|
+
prune?: boolean;
|
|
144
|
+
/** Wait for reconciled resources to become ready (default true). */
|
|
145
|
+
wait?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Names of Kustomizations that must be ready first, as a plain name list
|
|
148
|
+
* (rendered to spec.dependsOn). Validated against the build by FLUX003.
|
|
149
|
+
*/
|
|
150
|
+
dependsOn?: string[];
|
|
151
|
+
/** Timeout for health checks and apply operations. */
|
|
152
|
+
timeout?: string;
|
|
153
|
+
/** Pause reconciliation without deleting anything. */
|
|
154
|
+
suspend?: boolean;
|
|
155
|
+
/** ServiceAccount the kustomize-controller impersonates for this app. */
|
|
156
|
+
serviceAccountName?: string;
|
|
157
|
+
/** Namespace the Kustomization object itself lives in (default "flux-system"). */
|
|
158
|
+
fluxNamespace?: string;
|
|
159
|
+
/** Extra labels applied to the Kustomization. */
|
|
160
|
+
labels?: Record<string, string>;
|
|
161
|
+
defaults?: { kustomization?: Partial<Record<string, unknown>> };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type FluxAppForResult = {
|
|
165
|
+
kustomization: InstanceType<typeof KustomizationResource>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Resolve the `source` option to a rendered sourceRef. */
|
|
169
|
+
function resolveSourceRef(
|
|
170
|
+
target: string,
|
|
171
|
+
source: FluxGitSourceResult | FluxSourceRef | string,
|
|
172
|
+
): Record<string, unknown> {
|
|
173
|
+
if (typeof source === "string") {
|
|
174
|
+
return { kind: "GitRepository", name: source };
|
|
175
|
+
}
|
|
176
|
+
if ("gitRepository" in source && source.gitRepository !== undefined) {
|
|
177
|
+
const metadata = (source.gitRepository as { props?: { metadata?: { name?: unknown; namespace?: unknown } } })
|
|
178
|
+
.props?.metadata;
|
|
179
|
+
const name = metadata?.name;
|
|
180
|
+
if (typeof name !== "string" || name === "") {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`FluxAppFor("${target}"): the supplied FluxGitSource result has no metadata.name to reference.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
kind: "GitRepository",
|
|
187
|
+
name,
|
|
188
|
+
...(typeof metadata?.namespace === "string" && { namespace: metadata.namespace }),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const ref = source as FluxSourceRef;
|
|
192
|
+
return {
|
|
193
|
+
kind: ref.kind ?? "GitRepository",
|
|
194
|
+
name: ref.name,
|
|
195
|
+
...(ref.namespace !== undefined && { namespace: ref.namespace }),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const FluxKustomization = Composite<{ target: string } & FluxAppForOptions, FluxAppForResult>(
|
|
200
|
+
(props) => {
|
|
201
|
+
const {
|
|
202
|
+
target,
|
|
203
|
+
source,
|
|
204
|
+
path,
|
|
205
|
+
targetNamespace,
|
|
206
|
+
interval = "10m",
|
|
207
|
+
prune = true,
|
|
208
|
+
wait = true,
|
|
209
|
+
dependsOn = [],
|
|
210
|
+
timeout,
|
|
211
|
+
suspend,
|
|
212
|
+
serviceAccountName,
|
|
213
|
+
fluxNamespace = FLUX_NAMESPACE,
|
|
214
|
+
labels = {},
|
|
215
|
+
defaults,
|
|
216
|
+
} = props;
|
|
217
|
+
|
|
218
|
+
const sourceRef = resolveSourceRef(target, source);
|
|
219
|
+
|
|
220
|
+
// The Kustomization's namespace also scopes the sourceRef: an in-spec
|
|
221
|
+
// sourceRef without a namespace resolves in the Kustomization's own.
|
|
222
|
+
const kustomization = new KustomizationResource(mergeDefaults({
|
|
223
|
+
metadata: {
|
|
224
|
+
name: target,
|
|
225
|
+
namespace: fluxNamespace,
|
|
226
|
+
labels: {
|
|
227
|
+
"app.kubernetes.io/name": target,
|
|
228
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
229
|
+
...labels,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
spec: {
|
|
233
|
+
interval,
|
|
234
|
+
path,
|
|
235
|
+
prune,
|
|
236
|
+
wait,
|
|
237
|
+
sourceRef,
|
|
238
|
+
...(targetNamespace !== undefined && { targetNamespace }),
|
|
239
|
+
...(dependsOn.length > 0 && { dependsOn: dependsOn.map((name) => ({ name })) }),
|
|
240
|
+
...(timeout !== undefined && { timeout }),
|
|
241
|
+
...(suspend !== undefined && { suspend }),
|
|
242
|
+
...(serviceAccountName !== undefined && { serviceAccountName }),
|
|
243
|
+
},
|
|
244
|
+
}, defaults?.kustomization));
|
|
245
|
+
|
|
246
|
+
return { kustomization };
|
|
247
|
+
},
|
|
248
|
+
"FluxKustomization",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Reconcile a Chant build target with Flux in one call — the `Kustomization`
|
|
253
|
+
* half of the GitRepository + Kustomization pair. The source is passed in,
|
|
254
|
+
* not created here, so many apps share one repo declaration.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```ts
|
|
258
|
+
* import { FluxGitSource, FluxAppFor } from "@intentius/chant-lexicon-k8s";
|
|
259
|
+
*
|
|
260
|
+
* export const source = FluxGitSource("home-chant", {
|
|
261
|
+
* url: "https://github.com/jhgaylor/home-chant",
|
|
262
|
+
* });
|
|
263
|
+
*
|
|
264
|
+
* export const hello = FluxAppFor("hello-chant", {
|
|
265
|
+
* source,
|
|
266
|
+
* path: "./apps/hello-chant/k8s",
|
|
267
|
+
* targetNamespace: "default",
|
|
268
|
+
* dependsOn: ["cert-manager", "traefik"],
|
|
269
|
+
* });
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export function FluxAppFor(target: string, options: FluxAppForOptions): FluxAppForResult {
|
|
273
|
+
return FluxKustomization({ target, ...options });
|
|
274
|
+
}
|
package/src/composites/index.ts
CHANGED
|
@@ -93,3 +93,12 @@ export type {
|
|
|
93
93
|
RegisterArgoClusterOptions,
|
|
94
94
|
RegisterArgoClusterResult,
|
|
95
95
|
} from "./argo-app";
|
|
96
|
+
export { FluxGitSource, FluxAppFor } from "./flux-app";
|
|
97
|
+
export type {
|
|
98
|
+
FluxSourceKind,
|
|
99
|
+
FluxSourceRef,
|
|
100
|
+
FluxGitSourceOptions,
|
|
101
|
+
FluxGitSourceResult,
|
|
102
|
+
FluxAppForOptions,
|
|
103
|
+
FluxAppForResult,
|
|
104
|
+
} from "./flux-app";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `k8s` cluster-binding block typechecks under `satisfies ChantConfig`
|
|
3
|
+
* (#1455), and its schema rejects the typos that land a read on the wrong
|
|
4
|
+
* cluster (#1344).
|
|
5
|
+
*
|
|
6
|
+
* #1455's reproduction is the first test, verbatim: `ChantConfig` did not
|
|
7
|
+
* declare the `k8s` key the lexicon reads, so the one block that stops a live
|
|
8
|
+
* read hitting the wrong cluster was also the one block that could not
|
|
9
|
+
* typecheck — and the path of least resistance was dropping `satisfies
|
|
10
|
+
* ChantConfig`, which silently gives up checking on everything else in the
|
|
11
|
+
* file. The lexicon's module augmentation (declared in `./config.ts` and
|
|
12
|
+
* `./config-schema.ts`) is what makes it compile; this file is the forgejo
|
|
13
|
+
* lexicon's `config-augmentation.test.ts` guard, ported.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, test, expect } from "vitest";
|
|
17
|
+
import type { ChantConfig } from "@intentius/chant";
|
|
18
|
+
import type { K8sChantConfig } from "./config";
|
|
19
|
+
import { k8sConfigSchema } from "./config-schema";
|
|
20
|
+
|
|
21
|
+
describe("the k8s config namespace (#1455, #1344)", () => {
|
|
22
|
+
test("the issue's reproduction typechecks as a ChantConfig", () => {
|
|
23
|
+
// The compile-time assertion is the point; the runtime one keeps vitest happy.
|
|
24
|
+
const config = {
|
|
25
|
+
lexicons: ["k8s"],
|
|
26
|
+
environments: ["local"],
|
|
27
|
+
k8s: {
|
|
28
|
+
profiles: { local: { context: "k3d-fountain-local" } },
|
|
29
|
+
} satisfies K8sChantConfig,
|
|
30
|
+
} satisfies ChantConfig;
|
|
31
|
+
expect(config.k8s.profiles.local.context).toBe("k3d-fountain-local");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("an empty namespace is valid — every key is optional", () => {
|
|
35
|
+
expect(k8sConfigSchema.safeParse({}).success).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the schema accepts the documented shape", () => {
|
|
39
|
+
const parsed = k8sConfigSchema.safeParse({
|
|
40
|
+
profiles: { prod: { context: "prod-eks" }, staging: { context: "staging-eks" } },
|
|
41
|
+
execCredentialPlugins: ["aws", "my-org-oidc-helper"],
|
|
42
|
+
});
|
|
43
|
+
expect(parsed.success).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("an unknown key at the top of the namespace is rejected, not ignored", () => {
|
|
47
|
+
// Core applies `.strict()` when validating a declared namespace
|
|
48
|
+
// (`validateLexiconConfig`), so `profile` instead of `profiles` errors
|
|
49
|
+
// rather than silently leaving every environment unbound.
|
|
50
|
+
expect(k8sConfigSchema.strict().safeParse({ profile: { local: { context: "x" } } }).success).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a typo inside a profile is rejected too — the nesting is strict", () => {
|
|
54
|
+
// `contxt` used to be accepted and ignored, which meant the ambient
|
|
55
|
+
// kubectl context won and the wrong cluster was read (#1488's setup).
|
|
56
|
+
const parsed = k8sConfigSchema.safeParse({ profiles: { prod: { contxt: "prod-eks" } } });
|
|
57
|
+
expect(parsed.success).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a wrong value type is rejected", () => {
|
|
61
|
+
expect(k8sConfigSchema.safeParse({ profiles: { prod: { context: 42 } } }).success).toBe(false);
|
|
62
|
+
expect(k8sConfigSchema.safeParse({ execCredentialPlugins: "aws" }).success).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("the schema and the exported interface describe the same namespace", () => {
|
|
66
|
+
const value: K8sChantConfig = {
|
|
67
|
+
profiles: { local: { context: "k3d-fountain-local" } },
|
|
68
|
+
execCredentialPlugins: ["aws"],
|
|
69
|
+
};
|
|
70
|
+
expect(k8sConfigSchema.safeParse(value).success).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
package/src/config-schema.ts
CHANGED
|
@@ -23,6 +23,11 @@ export const k8sClusterProfileSchema = z.strictObject({
|
|
|
23
23
|
export const k8sConfigSchema = z.strictObject({
|
|
24
24
|
profiles: z.record(z.string(), k8sClusterProfileSchema).optional(),
|
|
25
25
|
execCredentialPlugins: z.array(z.string()).optional(),
|
|
26
|
+
kustomize: z
|
|
27
|
+
.strictObject({
|
|
28
|
+
roots: z.array(z.string()).optional(),
|
|
29
|
+
})
|
|
30
|
+
.optional(),
|
|
26
31
|
});
|
|
27
32
|
|
|
28
33
|
declare module "@intentius/chant/config" {
|
package/src/config.ts
CHANGED
|
@@ -29,13 +29,15 @@
|
|
|
29
29
|
* resolves this via `@intentius/chant/kubectl-context`'s
|
|
30
30
|
* `resolveClusterTarget`:
|
|
31
31
|
*
|
|
32
|
-
* - A declared binding is passed explicitly
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* - A declared binding is passed explicitly to the typed client on every
|
|
33
|
+
* read and write, regardless of what `kubectl` is ambiently pointed at
|
|
34
|
+
* (#1488 — the binding is a selection, not a check). A bound context the
|
|
35
|
+
* kubeconfig does not have fails with an error naming the context and the
|
|
36
|
+
* `k8s.profiles.<env>.context` binding — never by falling back to ambient.
|
|
36
37
|
* - No binding for the environment keeps today's behavior — the ambient
|
|
37
38
|
* context — but logs a visible note that nothing is pinned, so the
|
|
38
|
-
* fallback is never silent.
|
|
39
|
+
* fallback is never silent. Read failures name the context that was read
|
|
40
|
+
* and the missing binding.
|
|
39
41
|
*
|
|
40
42
|
* `ChantConfig` uses `.passthrough()` in its Zod schema so the `k8s` key is
|
|
41
43
|
* accepted at runtime without core changes, exactly like `temporal.profiles`
|
|
@@ -83,6 +85,32 @@ export interface K8sChantConfig {
|
|
|
83
85
|
* ```
|
|
84
86
|
*/
|
|
85
87
|
execCredentialPlugins?: string[];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Kustomize settings (#1548 piece 3).
|
|
91
|
+
*
|
|
92
|
+
* `roots` names kustomization directories that are build roots: each is
|
|
93
|
+
* rendered (`kustomize build`, `kubectl kustomize` fallback) at build time
|
|
94
|
+
* and the rendered documents join the manifest set — serialized into the
|
|
95
|
+
* build output, ownership-stamped, checked by post-synth rules, and
|
|
96
|
+
* observed by `lifecycle diff --live` like any declared resource. This is
|
|
97
|
+
* how an estate that keeps its overlay tree gets a declared side without
|
|
98
|
+
* converting a single manifest to typed source.
|
|
99
|
+
*
|
|
100
|
+
* Paths are relative to the project root (the directory holding
|
|
101
|
+
* `chant.config.*`), NOT `sourceDir` — the overlay tree usually lives
|
|
102
|
+
* beside the typed source, not inside it.
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* k8s: {
|
|
106
|
+
* kustomize: { roots: ["overlays/prod"] },
|
|
107
|
+
* } satisfies K8sChantConfig
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
kustomize?: {
|
|
111
|
+
/** Kustomization directories to render into the build. */
|
|
112
|
+
roots?: string[];
|
|
113
|
+
};
|
|
86
114
|
}
|
|
87
115
|
|
|
88
116
|
declare module "@intentius/chant/config" {
|
package/src/crd/crd-sources.ts
CHANGED
|
@@ -277,6 +277,40 @@ const FLUX_OPERATOR_INSTALL = `https://github.com/controlplaneio-fluxcd/flux-ope
|
|
|
277
277
|
const KUBEMICROVM_CHART = "oci://ghcr.io/codriverlabs/helm/kube-microvm-operator";
|
|
278
278
|
const KUBEMICROVM_VERSION = "1.0.11";
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* k3s bundled-controller CRDs — helm.cattle.io/v1 + k3s.cattle.io/v1
|
|
282
|
+
*
|
|
283
|
+
* k3s's manifest auto-deploy machinery speaks these CRDs: drop a HelmChart
|
|
284
|
+
* into /var/lib/rancher/k3s/server/manifests and the embedded helm-controller
|
|
285
|
+
* installs the chart; every auto-deployed manifest is tracked by an Addon.
|
|
286
|
+
* Typed here for the same reason the Flux toolkit kinds are — they are what a
|
|
287
|
+
* k3s estate's GitOps surface is written in.
|
|
288
|
+
*
|
|
289
|
+
* Produces (both groups map to the `K3s` namespace — see
|
|
290
|
+
* GROUP_NAMESPACE_OVERRIDES in crd/parser.ts):
|
|
291
|
+
* K8s::K3s::HelmChart → apiVersion: helm.cattle.io/v1, kind: HelmChart
|
|
292
|
+
* K8s::K3s::HelmChartConfig → apiVersion: helm.cattle.io/v1, kind: HelmChartConfig
|
|
293
|
+
* K8s::K3s::Addon → apiVersion: k3s.cattle.io/v1, kind: Addon
|
|
294
|
+
*
|
|
295
|
+
* The pin is k3s v1.36.3+k3s1 (the release the k3s lexicon pins), but the k3s
|
|
296
|
+
* repo itself publishes no CRD YAMLs — its manifests/ directory carries only
|
|
297
|
+
* the bundled charts, and the API types live in dependency repos. The URLs
|
|
298
|
+
* below point at the exact versions that release vendors in its go.mod:
|
|
299
|
+
* github.com/k3s-io/helm-controller v0.17.7 (helm.cattle.io CRDs)
|
|
300
|
+
* github.com/k3s-io/api v0.1.4 (k3s.cattle.io CRDs)
|
|
301
|
+
* When bumping, re-read go.mod at the new k3s tag and move both pins together.
|
|
302
|
+
*
|
|
303
|
+
* k3s-io/api also ships an ETCDSnapshotFile CRD — deliberately left out. It is
|
|
304
|
+
* status-only controller bookkeeping (the server writes them to describe
|
|
305
|
+
* snapshots it took); nothing an author would declare.
|
|
306
|
+
*
|
|
307
|
+
* Controller install: none — both controllers are embedded in the k3s binary.
|
|
308
|
+
*/
|
|
309
|
+
const K3S_HELM_CONTROLLER_VERSION = "v0.17.7"; // vendored by k3s v1.36.3+k3s1
|
|
310
|
+
const K3S_API_VERSION = "v0.1.4"; // vendored by k3s v1.36.3+k3s1
|
|
311
|
+
const K3S_HELM_CONTROLLER_CRD_BASE = `https://raw.githubusercontent.com/k3s-io/helm-controller/${K3S_HELM_CONTROLLER_VERSION}/pkg/crds/yaml/generated`;
|
|
312
|
+
const K3S_API_CRD_BASE = `https://raw.githubusercontent.com/k3s-io/api/${K3S_API_VERSION}/pkg/crds/yaml/generated`;
|
|
313
|
+
|
|
280
314
|
export const CRD_SOURCES: CRDSource[] = [
|
|
281
315
|
{ type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayclusters.yaml` },
|
|
282
316
|
{ type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayjobs.yaml` },
|
|
@@ -335,6 +369,9 @@ export const CRD_SOURCES: CRDSource[] = [
|
|
|
335
369
|
url: FLUX_OPERATOR_INSTALL,
|
|
336
370
|
kinds: ["FluxInstance", "FluxReport", "ResourceSet", "ResourceSetInputProvider"],
|
|
337
371
|
},
|
|
372
|
+
{ type: "url", url: `${K3S_HELM_CONTROLLER_CRD_BASE}/helm.cattle.io_helmcharts.yaml` },
|
|
373
|
+
{ type: "url", url: `${K3S_HELM_CONTROLLER_CRD_BASE}/helm.cattle.io_helmchartconfigs.yaml` },
|
|
374
|
+
{ type: "url", url: `${K3S_API_CRD_BASE}/k3s.cattle.io_addons.yaml` },
|
|
338
375
|
{
|
|
339
376
|
type: "helm",
|
|
340
377
|
chart: KUBEMICROVM_CHART,
|
package/src/crd/parser.ts
CHANGED
|
@@ -52,6 +52,12 @@ const GROUP_NAMESPACE_OVERRIDES: Record<string, string> = {
|
|
|
52
52
|
// Not `Secrets`: `K8s::Secrets::InfisicalSecret` reads like a core Secret,
|
|
53
53
|
// and `K8s::Core::Secret` is right there to be confused with.
|
|
54
54
|
"secrets.infisical.com": "Infisical",
|
|
55
|
+
// k3s's bundled controllers ship under two groups but are one thing to an
|
|
56
|
+
// author — the k3s auto-deploy surface. The first-segment rule would give
|
|
57
|
+
// `K8s::Helm::HelmChart`, which reads like it belongs to the helm lexicon,
|
|
58
|
+
// and would split HelmChart from the Addon that tracks its deployment.
|
|
59
|
+
"helm.cattle.io": "K3s",
|
|
60
|
+
"k3s.cattle.io": "K3s",
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
/**
|