@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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `argo-app` — the Argo CD deploy leaf for the component model (#1549
|
|
3
|
+
* piece 2).
|
|
4
|
+
*
|
|
5
|
+
* GitOps inverts the flow kubectl-apply assumes: the controller deploys,
|
|
6
|
+
* chant declares the controller's objects and observes convergence. This
|
|
7
|
+
* capability is that inversion as one step — apply the declared
|
|
8
|
+
* `Application` CR(s) through the SAME server-side apply `kubectl-apply`
|
|
9
|
+
* uses (ownership stamping, marker-scoped prune, stack labels all
|
|
10
|
+
* identical), then block until every applied Application reports
|
|
11
|
+
* `health=Healthy && sync=Synced` via the existing `waitForArgoSync`
|
|
12
|
+
* activity (#957).
|
|
13
|
+
*
|
|
14
|
+
* The deploy unit is the CR itself under its stack label — `stack` doubles
|
|
15
|
+
* as ownership identity and deploy unit exactly as kubectl-apply's, so
|
|
16
|
+
* `chant components status --live` observes the Application through the
|
|
17
|
+
* same label sweep with zero new status walks (core
|
|
18
|
+
* `components/deploy-units.ts`).
|
|
19
|
+
*
|
|
20
|
+
* The #1074 boundary applies: nothing exported from the lexicon entry point
|
|
21
|
+
* may statically import the API-client chain, so both the applier and the
|
|
22
|
+
* wait activity are reached by dynamic import inside `run()` and their
|
|
23
|
+
* argument shapes are structural mirrors, checked against the real ones at
|
|
24
|
+
* the call site.
|
|
25
|
+
*/
|
|
26
|
+
import type { Capability, DeployContext } from "@intentius/chant/components/capability";
|
|
27
|
+
|
|
28
|
+
/** Structural mirrors of the activity modules' shapes (see the module doc). */
|
|
29
|
+
interface AppliedRef {
|
|
30
|
+
apiVersion: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
name: string;
|
|
33
|
+
namespace?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ApplyOutcome {
|
|
37
|
+
fieldManager: string;
|
|
38
|
+
applied: AppliedRef[];
|
|
39
|
+
pruned: AppliedRef[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ApplierArgs {
|
|
43
|
+
manifest: string;
|
|
44
|
+
environment?: string;
|
|
45
|
+
stack?: string;
|
|
46
|
+
context?: string;
|
|
47
|
+
deleteMode?: "never" | "owned-only" | "gated";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Applier = (args: ApplierArgs) => Promise<ApplyOutcome>;
|
|
51
|
+
|
|
52
|
+
interface ArgoWaiterArgs {
|
|
53
|
+
appName: string;
|
|
54
|
+
namespace?: string;
|
|
55
|
+
server?: string;
|
|
56
|
+
authToken?: string;
|
|
57
|
+
insecure?: boolean;
|
|
58
|
+
context?: string;
|
|
59
|
+
intervalMs?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type ArgoWaiter = (args: ArgoWaiterArgs) => Promise<{ health: string; sync: string }>;
|
|
63
|
+
|
|
64
|
+
/** One Application's final status, after the wait. */
|
|
65
|
+
export interface ArgoAppSynced {
|
|
66
|
+
name: string;
|
|
67
|
+
namespace: string;
|
|
68
|
+
health: string;
|
|
69
|
+
sync: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ArgoAppOutcome extends ApplyOutcome {
|
|
73
|
+
/** Every applied Application, Healthy+Synced, in wait order. */
|
|
74
|
+
synced: ArgoAppSynced[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ArgoAppInput {
|
|
78
|
+
/** Path to the Application CR manifest(s) — a file, or a directory of them. */
|
|
79
|
+
manifest: string;
|
|
80
|
+
/**
|
|
81
|
+
* The deploy unit / ownership stack — same double duty as kubectl-apply's.
|
|
82
|
+
* Omitted derives from the project's `ownership.stack`, but then the unit
|
|
83
|
+
* is invisible to `components status --live`. Name it.
|
|
84
|
+
*/
|
|
85
|
+
stack?: string;
|
|
86
|
+
/** kubectl context. Omitted resolves `k8s.profiles.<ctx.env>.context`. */
|
|
87
|
+
context?: string;
|
|
88
|
+
/** Same delete vocabulary as kubectl-apply; default `never`. */
|
|
89
|
+
delete?: "never" | "owned-only";
|
|
90
|
+
/**
|
|
91
|
+
* Argo CD API base URL. When set, sync status is read from the REST API
|
|
92
|
+
* instead of the cluster — pass `authToken` with it (the `waitForArgoSync`
|
|
93
|
+
* surface, #957).
|
|
94
|
+
*/
|
|
95
|
+
server?: string;
|
|
96
|
+
/** Bearer token for the Argo CD REST API (used with `server`). */
|
|
97
|
+
authToken?: string;
|
|
98
|
+
/** Skip TLS verification for the REST API (default false). */
|
|
99
|
+
insecure?: boolean;
|
|
100
|
+
/** Poll interval in ms (default 15000). */
|
|
101
|
+
intervalMs?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True when the ref is an Argo CD Application (any argoproj.io version). */
|
|
105
|
+
function isArgoApplication(ref: AppliedRef): boolean {
|
|
106
|
+
return ref.kind === "Application" && /^argoproj\.io(\/|$)/.test(ref.apiVersion);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Factory with injectable applier + waiter, so tests assert the exact
|
|
110
|
+
* delegation without a cluster — the kubectl-apply seam. Defaults resolve by
|
|
111
|
+
* dynamic import on first run, keeping the API-client chain off the build
|
|
112
|
+
* path (#1074). */
|
|
113
|
+
export function createArgoAppCapability(
|
|
114
|
+
apply?: Applier,
|
|
115
|
+
wait?: ArgoWaiter,
|
|
116
|
+
): Capability<ArgoAppInput, ArgoAppOutcome> {
|
|
117
|
+
return {
|
|
118
|
+
kind: "argo-app",
|
|
119
|
+
// The apply half is a server-side apply with no native undo, and Argo
|
|
120
|
+
// offers no declarative "un-sync" — same posture as kubectl-apply.
|
|
121
|
+
rollbackPolicy: "needs-opt-out",
|
|
122
|
+
async run(ctx: DeployContext, input: ArgoAppInput): Promise<ArgoAppOutcome> {
|
|
123
|
+
const applier: Applier = apply ?? (await import("../op/activities/kubectl")).applyManifest;
|
|
124
|
+
const outcome = await applier({
|
|
125
|
+
manifest: input.manifest,
|
|
126
|
+
environment: ctx.env,
|
|
127
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
128
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
129
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// AppProject / ApplicationSet in the same manifest apply fine; the wait
|
|
133
|
+
// gates only on Applications (an ApplicationSet's generated Applications
|
|
134
|
+
// are the controller's, not declared here).
|
|
135
|
+
const apps = outcome.applied.filter(isArgoApplication);
|
|
136
|
+
if (apps.length === 0) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`argo-app: "${input.manifest}" applied no argoproj.io Application — nothing to sync on. ` +
|
|
139
|
+
`A plain manifest belongs on kubectl-apply.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const waiter: ArgoWaiter = wait ?? (await import("../op/activities/argo")).waitForArgoSync;
|
|
144
|
+
const synced: ArgoAppSynced[] = [];
|
|
145
|
+
for (const app of apps) {
|
|
146
|
+
const namespace = app.namespace ?? "argocd";
|
|
147
|
+
const status = await waiter({
|
|
148
|
+
appName: app.name,
|
|
149
|
+
namespace,
|
|
150
|
+
...(input.server !== undefined ? { server: input.server } : {}),
|
|
151
|
+
...(input.authToken !== undefined ? { authToken: input.authToken } : {}),
|
|
152
|
+
...(input.insecure !== undefined ? { insecure: input.insecure } : {}),
|
|
153
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
154
|
+
...(input.intervalMs !== undefined ? { intervalMs: input.intervalMs } : {}),
|
|
155
|
+
});
|
|
156
|
+
synced.push({ name: app.name, namespace, health: status.health, sync: status.sync });
|
|
157
|
+
}
|
|
158
|
+
return { ...outcome, synced };
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const argoAppCapability = createArgoAppCapability();
|
|
@@ -14,9 +14,17 @@
|
|
|
14
14
|
import { step } from "@intentius/chant/components";
|
|
15
15
|
import type { KubectlApplyInput } from "./kubectl-apply";
|
|
16
16
|
import type { KustomizeApplyInput } from "./kustomize-apply";
|
|
17
|
+
import type { ArgoAppInput } from "./argo-app";
|
|
18
|
+
import type { FluxReconcileInput } from "./flux-reconcile";
|
|
17
19
|
|
|
18
20
|
export const kubectlApply = step<KubectlApplyInput & { noRollback?: string }>("kubectl-apply");
|
|
19
21
|
|
|
20
22
|
// Same needs-opt-out posture, same sugar (#1548): a kustomize render in front
|
|
21
23
|
// of the identical server-side apply.
|
|
22
24
|
export const kustomizeApply = step<KustomizeApplyInput & { noRollback?: string }>("kustomize-apply");
|
|
25
|
+
|
|
26
|
+
// GitOps verbs (#1549 piece 2): the identical server-side apply of the
|
|
27
|
+
// controller's CR, then a convergence wait — Healthy+Synced for Argo, the
|
|
28
|
+
// Flux readiness registry (Ready, terminal on wedge reasons) for Flux.
|
|
29
|
+
export const argoApp = step<ArgoAppInput & { noRollback?: string }>("argo-app");
|
|
30
|
+
export const fluxReconcile = step<FluxReconcileInput & { noRollback?: string }>("flux-reconcile");
|
|
@@ -12,9 +12,14 @@ import type { Capability } from "@intentius/chant/components/capability";
|
|
|
12
12
|
import { ownPackageVersion, type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
13
13
|
import { kubectlApplyCapability } from "./kubectl-apply";
|
|
14
14
|
import { kustomizeApplyCapability } from "./kustomize-apply";
|
|
15
|
+
import { argoAppCapability } from "./argo-app";
|
|
16
|
+
import { fluxReconcileCapability } from "./flux-reconcile";
|
|
15
17
|
|
|
16
18
|
export const K8S_VERB_FAMILIES = {
|
|
17
19
|
apply: ["kubectl-apply", "kustomize-apply"],
|
|
20
|
+
// GitOps verbs (#1549 piece 2): apply the controller's CR through the same
|
|
21
|
+
// stack-labelled pipeline, then wait for the controller to converge.
|
|
22
|
+
gitops: ["argo-app", "flux-reconcile"],
|
|
18
23
|
} as const;
|
|
19
24
|
|
|
20
25
|
export const k8sCapabilityPlugin: CapabilityPlugin = {
|
|
@@ -23,7 +28,12 @@ export const k8sCapabilityPlugin: CapabilityPlugin = {
|
|
|
23
28
|
// a literal here was stale one release after it was written.
|
|
24
29
|
version: ownPackageVersion(import.meta.url),
|
|
25
30
|
capabilities(): Array<Capability<never, unknown>> {
|
|
26
|
-
return [
|
|
31
|
+
return [
|
|
32
|
+
kubectlApplyCapability as Capability<never, unknown>,
|
|
33
|
+
kustomizeApplyCapability as Capability<never, unknown>,
|
|
34
|
+
argoAppCapability as Capability<never, unknown>,
|
|
35
|
+
fluxReconcileCapability as Capability<never, unknown>,
|
|
36
|
+
];
|
|
27
37
|
},
|
|
28
38
|
families(): Record<string, readonly string[]> {
|
|
29
39
|
return K8S_VERB_FAMILIES;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `flux-reconcile` (#1549 piece 2) — the Flux deploy leaf. What must hold:
|
|
3
|
+
* the capability registers under its kind in the gitops family, the apply
|
|
4
|
+
* goes through the SAME applier delegation kubectl-apply uses, the wait runs
|
|
5
|
+
* the generic `waitForReady` once per applied Flux CR — sources before
|
|
6
|
+
* reconcilers, non-Flux objects skipped — with the group/kind the #1554
|
|
7
|
+
* readiness entries key on, a manifest with no Flux CR is an error, and the
|
|
8
|
+
* stack is the deploy unit core's status walk reads.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { createFluxReconcileCapability, fluxReconcileCapability, type FluxReconcileInput } from "./flux-reconcile";
|
|
12
|
+
import { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin";
|
|
13
|
+
import { isCapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
14
|
+
import type { DeployContext } from "@intentius/chant/components/capability";
|
|
15
|
+
|
|
16
|
+
const ctx = { env: "dev" } as DeployContext;
|
|
17
|
+
|
|
18
|
+
const KUSTOMIZATION = {
|
|
19
|
+
apiVersion: "kustomize.toolkit.fluxcd.io/v1",
|
|
20
|
+
kind: "Kustomization",
|
|
21
|
+
name: "apps",
|
|
22
|
+
namespace: "flux-system",
|
|
23
|
+
};
|
|
24
|
+
const GIT_REPO = {
|
|
25
|
+
apiVersion: "source.toolkit.fluxcd.io/v1",
|
|
26
|
+
kind: "GitRepository",
|
|
27
|
+
name: "repo",
|
|
28
|
+
namespace: "flux-system",
|
|
29
|
+
};
|
|
30
|
+
const HELM_RELEASE = {
|
|
31
|
+
apiVersion: "helm.toolkit.fluxcd.io/v2",
|
|
32
|
+
kind: "HelmRelease",
|
|
33
|
+
name: "podinfo",
|
|
34
|
+
namespace: "flux-system",
|
|
35
|
+
};
|
|
36
|
+
const NAMESPACE = { apiVersion: "v1", kind: "Namespace", name: "flux-system" };
|
|
37
|
+
|
|
38
|
+
describe("flux-reconcile capability (#1549 piece 2)", () => {
|
|
39
|
+
test("registers on the k8s plugin, in the gitops family", () => {
|
|
40
|
+
expect(isCapabilityPlugin(k8sCapabilityPlugin)).toBe(true);
|
|
41
|
+
expect(k8sCapabilityPlugin.capabilities().map((c) => c.kind)).toContain("flux-reconcile");
|
|
42
|
+
expect(K8S_VERB_FAMILIES.gitops).toContain("flux-reconcile");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("rollback is needs-opt-out — a server-side apply keeps no previous state", () => {
|
|
46
|
+
expect(fluxReconcileCapability.rollbackPolicy).toBe("needs-opt-out");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("applies through the kubectl-apply delegation, then waits Ready on each Flux CR — sources first, non-Flux skipped", async () => {
|
|
50
|
+
const applierArgs: unknown[] = [];
|
|
51
|
+
const waited: Array<{ kind: string; group?: string }> = [];
|
|
52
|
+
const cap = createFluxReconcileCapability(
|
|
53
|
+
async (args) => {
|
|
54
|
+
applierArgs.push(args);
|
|
55
|
+
// Applied order: reconciler before its source, plus a plain Namespace.
|
|
56
|
+
return { fieldManager: "chant:gitops", applied: [NAMESPACE, KUSTOMIZATION, GIT_REPO, HELM_RELEASE], pruned: [] };
|
|
57
|
+
},
|
|
58
|
+
async (args) => {
|
|
59
|
+
waited.push(args);
|
|
60
|
+
return {};
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const outcome = await cap.run(ctx, {
|
|
65
|
+
manifest: "flux/",
|
|
66
|
+
stack: "gitops",
|
|
67
|
+
delete: "owned-only",
|
|
68
|
+
intervalMs: 500,
|
|
69
|
+
} as FluxReconcileInput as never);
|
|
70
|
+
|
|
71
|
+
// The exact args kubectl-apply hands the shared applier — same path, same stamping.
|
|
72
|
+
expect(applierArgs).toEqual([
|
|
73
|
+
{ manifest: "flux/", environment: "dev", stack: "gitops", deleteMode: "owned-only" },
|
|
74
|
+
]);
|
|
75
|
+
// Source first (a Kustomization can't be Ready before its GitRepository has
|
|
76
|
+
// an artifact), then the reconcilers in applied order; the Namespace is not waited on.
|
|
77
|
+
expect(waited).toEqual([
|
|
78
|
+
{
|
|
79
|
+
kind: "GitRepository",
|
|
80
|
+
name: "repo",
|
|
81
|
+
namespace: "flux-system",
|
|
82
|
+
group: "source.toolkit.fluxcd.io",
|
|
83
|
+
environment: "dev",
|
|
84
|
+
intervalMs: 500,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
kind: "Kustomization",
|
|
88
|
+
name: "apps",
|
|
89
|
+
namespace: "flux-system",
|
|
90
|
+
group: "kustomize.toolkit.fluxcd.io",
|
|
91
|
+
environment: "dev",
|
|
92
|
+
intervalMs: 500,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
kind: "HelmRelease",
|
|
96
|
+
name: "podinfo",
|
|
97
|
+
namespace: "flux-system",
|
|
98
|
+
group: "helm.toolkit.fluxcd.io",
|
|
99
|
+
environment: "dev",
|
|
100
|
+
intervalMs: 500,
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
expect(outcome.ready).toEqual([GIT_REPO, KUSTOMIZATION, HELM_RELEASE]);
|
|
104
|
+
expect(outcome.fieldManager).toBe("chant:gitops");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("the group/kind handed to waitForReady key the #1554 readiness registry entries", async () => {
|
|
108
|
+
// The registry is keyed "<group>/<kind>" — assert the pair the capability
|
|
109
|
+
// passes resolves to the ConditionReasonMatch entry, not the generic default.
|
|
110
|
+
const { readinessFor } = await import("../op/activities/wait-for-ready");
|
|
111
|
+
const spec = readinessFor("kustomize.toolkit.fluxcd.io", "Kustomization");
|
|
112
|
+
expect(spec.terminal).toBeDefined();
|
|
113
|
+
expect(JSON.stringify(spec.terminal)).toContain("BuildFailed");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a manifest that applied no Flux CR is an error, not a silent no-wait", async () => {
|
|
117
|
+
const cap = createFluxReconcileCapability(
|
|
118
|
+
async () => ({ fieldManager: "chant", applied: [NAMESPACE], pruned: [] }),
|
|
119
|
+
async () => ({}),
|
|
120
|
+
);
|
|
121
|
+
await expect(cap.run(ctx, { manifest: "k8s.yaml" } as FluxReconcileInput as never)).rejects.toThrow(
|
|
122
|
+
/no Flux CR/,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a wedged CR propagates — the step does not report success past a terminal Ready=False", async () => {
|
|
127
|
+
const cap = createFluxReconcileCapability(
|
|
128
|
+
async () => ({ fieldManager: "chant", applied: [KUSTOMIZATION], pruned: [] }),
|
|
129
|
+
async () => {
|
|
130
|
+
throw new Error("Kustomization/apps reached a terminal state");
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
await expect(cap.run(ctx, { manifest: "flux/" } as FluxReconcileInput as never)).rejects.toThrow(
|
|
134
|
+
/terminal state/,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("the stack field is the deploy unit core's status walk reads (#1549 piece 2)", async () => {
|
|
139
|
+
const { deployUnits } = await import("@intentius/chant/components/deploy-units");
|
|
140
|
+
const units = deployUnits([
|
|
141
|
+
{ phase: "Apply", steps: [{ kind: "flux-reconcile", manifest: "flux/", stack: "gitops" }] } as never,
|
|
142
|
+
]);
|
|
143
|
+
expect(units).toEqual([{ unit: "gitops", lexicon: "k8s" }]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `flux-reconcile` — the Flux deploy leaf for the component model (#1549
|
|
3
|
+
* piece 2).
|
|
4
|
+
*
|
|
5
|
+
* The sibling of `argo-app` for the Flux toolkit: apply the declared Flux
|
|
6
|
+
* CRs (`GitRepository`, `Kustomization`, `HelmRelease`, `OCIRepository`, …)
|
|
7
|
+
* through the SAME server-side apply `kubectl-apply` uses — ownership
|
|
8
|
+
* stamping, marker-scoped prune, stack labels all identical — then block
|
|
9
|
+
* until every applied Flux CR reports `Ready=True` via the generic
|
|
10
|
+
* `waitForReady`. The readiness registry's Flux entries (#1554,
|
|
11
|
+
* `ConditionReasonMatch`) make that wait honest: a wedge reason like
|
|
12
|
+
* `BuildFailed` or `UpgradeFailed` fails fast instead of polling out the
|
|
13
|
+
* timeout.
|
|
14
|
+
*
|
|
15
|
+
* Sources are waited first (`source.toolkit.fluxcd.io` before the rest,
|
|
16
|
+
* applied order within each half): a Kustomization cannot become Ready
|
|
17
|
+
* before its GitRepository has an artifact, so gating on the source first
|
|
18
|
+
* surfaces a wedged clone as the source's error, not as a reconciler
|
|
19
|
+
* timeout downstream of it.
|
|
20
|
+
*
|
|
21
|
+
* The deploy unit is the CR itself under its stack label — one name, both
|
|
22
|
+
* jobs, exactly as kubectl-apply (core `components/deploy-units.ts`).
|
|
23
|
+
*
|
|
24
|
+
* The #1074 boundary applies: the applier and the wait activity are reached
|
|
25
|
+
* by dynamic import inside `run()`; their argument shapes are structural
|
|
26
|
+
* mirrors, checked against the real ones at the call site.
|
|
27
|
+
*/
|
|
28
|
+
import type { Capability, DeployContext } from "@intentius/chant/components/capability";
|
|
29
|
+
|
|
30
|
+
/** Structural mirrors of the activity modules' shapes (see the module doc). */
|
|
31
|
+
interface AppliedRef {
|
|
32
|
+
apiVersion: string;
|
|
33
|
+
kind: string;
|
|
34
|
+
name: string;
|
|
35
|
+
namespace?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ApplyOutcome {
|
|
39
|
+
fieldManager: string;
|
|
40
|
+
applied: AppliedRef[];
|
|
41
|
+
pruned: AppliedRef[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ApplierArgs {
|
|
45
|
+
manifest: string;
|
|
46
|
+
environment?: string;
|
|
47
|
+
stack?: string;
|
|
48
|
+
context?: string;
|
|
49
|
+
deleteMode?: "never" | "owned-only" | "gated";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Applier = (args: ApplierArgs) => Promise<ApplyOutcome>;
|
|
53
|
+
|
|
54
|
+
interface ReadyWaiterArgs {
|
|
55
|
+
kind: string;
|
|
56
|
+
name: string;
|
|
57
|
+
namespace?: string;
|
|
58
|
+
group?: string;
|
|
59
|
+
environment?: string;
|
|
60
|
+
context?: string;
|
|
61
|
+
intervalMs?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type ReadyWaiter = (args: ReadyWaiterArgs) => Promise<Record<string, unknown>>;
|
|
65
|
+
|
|
66
|
+
export interface FluxReconcileOutcome extends ApplyOutcome {
|
|
67
|
+
/** The Flux CRs that reached Ready, in wait order (sources first). */
|
|
68
|
+
ready: AppliedRef[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FluxReconcileInput {
|
|
72
|
+
/** Path to the Flux CR manifest(s) — a file, or a directory of them. */
|
|
73
|
+
manifest: string;
|
|
74
|
+
/**
|
|
75
|
+
* The deploy unit / ownership stack — same double duty as kubectl-apply's.
|
|
76
|
+
* Omitted derives from the project's `ownership.stack`, but then the unit
|
|
77
|
+
* is invisible to `components status --live`. Name it.
|
|
78
|
+
*/
|
|
79
|
+
stack?: string;
|
|
80
|
+
/** kubectl context. Omitted resolves `k8s.profiles.<ctx.env>.context`. */
|
|
81
|
+
context?: string;
|
|
82
|
+
/** Same delete vocabulary as kubectl-apply; default `never`. */
|
|
83
|
+
delete?: "never" | "owned-only";
|
|
84
|
+
/** Poll interval in ms (default 15000). */
|
|
85
|
+
intervalMs?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The API group of a ref, "" for the core group. */
|
|
89
|
+
function groupOf(ref: AppliedRef): string {
|
|
90
|
+
const slash = ref.apiVersion.indexOf("/");
|
|
91
|
+
return slash === -1 ? "" : ref.apiVersion.slice(0, slash);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* True for any Flux CR: the toolkit groups (`source.` / `kustomize.` /
|
|
96
|
+
* `helm.` / `image.` / `notification.toolkit.fluxcd.io`) plus the flux-operator
|
|
97
|
+
* group (`fluxcd.controlplane.io`, `FluxInstance`). Kinds without a registry
|
|
98
|
+
* entry still wait correctly — the toolkit is kstatus-conformant, so the
|
|
99
|
+
* generic `Ready=True` default covers them; the #1554 entries add fail-fast.
|
|
100
|
+
*/
|
|
101
|
+
function isFluxRef(ref: AppliedRef): boolean {
|
|
102
|
+
const group = groupOf(ref);
|
|
103
|
+
return /(^|\.)toolkit\.fluxcd\.io$/.test(group) || group === "fluxcd.controlplane.io";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Factory with injectable applier + waiter — the argo-app seam. Defaults
|
|
107
|
+
* resolve by dynamic import on first run (#1074). */
|
|
108
|
+
export function createFluxReconcileCapability(
|
|
109
|
+
apply?: Applier,
|
|
110
|
+
wait?: ReadyWaiter,
|
|
111
|
+
): Capability<FluxReconcileInput, FluxReconcileOutcome> {
|
|
112
|
+
return {
|
|
113
|
+
kind: "flux-reconcile",
|
|
114
|
+
// Server-side apply keeps no previous object state, and the controller
|
|
115
|
+
// reconciles forward only — same posture as kubectl-apply.
|
|
116
|
+
rollbackPolicy: "needs-opt-out",
|
|
117
|
+
async run(ctx: DeployContext, input: FluxReconcileInput): Promise<FluxReconcileOutcome> {
|
|
118
|
+
const applier: Applier = apply ?? (await import("../op/activities/kubectl")).applyManifest;
|
|
119
|
+
const outcome = await applier({
|
|
120
|
+
manifest: input.manifest,
|
|
121
|
+
environment: ctx.env,
|
|
122
|
+
...(input.stack !== undefined ? { stack: input.stack } : {}),
|
|
123
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
124
|
+
...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Namespaces/ConfigMaps alongside the CRs apply fine; the wait gates
|
|
128
|
+
// only on the Flux objects. Sources first — see the module doc.
|
|
129
|
+
const flux = outcome.applied.filter(isFluxRef);
|
|
130
|
+
if (flux.length === 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`flux-reconcile: "${input.manifest}" applied no Flux CR (toolkit.fluxcd.io / fluxcd.controlplane.io) — ` +
|
|
133
|
+
`nothing to reconcile on. A plain manifest belongs on kubectl-apply.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const sources = flux.filter((r) => groupOf(r) === "source.toolkit.fluxcd.io");
|
|
137
|
+
const rest = flux.filter((r) => groupOf(r) !== "source.toolkit.fluxcd.io");
|
|
138
|
+
|
|
139
|
+
const waiter: ReadyWaiter =
|
|
140
|
+
wait ?? ((await import("../op/activities/wait-for-ready")).waitForReady as unknown as ReadyWaiter);
|
|
141
|
+
const ready: AppliedRef[] = [];
|
|
142
|
+
for (const ref of [...sources, ...rest]) {
|
|
143
|
+
await waiter({
|
|
144
|
+
kind: ref.kind,
|
|
145
|
+
name: ref.name,
|
|
146
|
+
group: groupOf(ref),
|
|
147
|
+
environment: ctx.env,
|
|
148
|
+
...(ref.namespace !== undefined ? { namespace: ref.namespace } : {}),
|
|
149
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
150
|
+
...(input.intervalMs !== undefined ? { intervalMs: input.intervalMs } : {}),
|
|
151
|
+
});
|
|
152
|
+
ready.push(ref);
|
|
153
|
+
}
|
|
154
|
+
return { ...outcome, ready };
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const fluxReconcileCapability = createFluxReconcileCapability();
|
package/src/components/index.ts
CHANGED
|
@@ -26,3 +26,16 @@ export {
|
|
|
26
26
|
type KustomizeApplyInput,
|
|
27
27
|
type KustomizeApplyOutcome,
|
|
28
28
|
} from "./kustomize-apply";
|
|
29
|
+
export {
|
|
30
|
+
argoAppCapability,
|
|
31
|
+
createArgoAppCapability,
|
|
32
|
+
type ArgoAppInput,
|
|
33
|
+
type ArgoAppOutcome,
|
|
34
|
+
type ArgoAppSynced,
|
|
35
|
+
} from "./argo-app";
|
|
36
|
+
export {
|
|
37
|
+
fluxReconcileCapability,
|
|
38
|
+
createFluxReconcileCapability,
|
|
39
|
+
type FluxReconcileInput,
|
|
40
|
+
type FluxReconcileOutcome,
|
|
41
|
+
} from "./flux-reconcile";
|
|
@@ -14,32 +14,26 @@
|
|
|
14
14
|
* `stack` doubles as the ownership identity and the deploy unit, exactly as
|
|
15
15
|
* `kubectl-apply` — one name, both jobs.
|
|
16
16
|
*
|
|
17
|
-
* Renderer resolution
|
|
17
|
+
* Renderer resolution lives in `../kustomize/render.ts` (shared with the
|
|
18
|
+
* build-root path, #1548 piece 3): `kustomize build`, falling back to
|
|
18
19
|
* `kubectl kustomize` (the same renderer vendored into kubectl) when the
|
|
19
20
|
* standalone binary is absent. Both run through an injectable runner so tests
|
|
20
|
-
* assert the exact command without either binary installed.
|
|
21
|
-
* maxBuffer mirrors helm-upgrade's — a big overlay renders megabytes.
|
|
21
|
+
* assert the exact command without either binary installed.
|
|
22
22
|
*
|
|
23
23
|
* The #1074 boundary applies as it does to kubectl-apply: nothing exported
|
|
24
24
|
* from the lexicon entry point may statically import the API-client chain, so
|
|
25
25
|
* the applier is reached by dynamic import inside `run()` and its argument
|
|
26
26
|
* shape is a structural mirror.
|
|
27
27
|
*/
|
|
28
|
-
import { exec } from "node:child_process";
|
|
29
|
-
import { promisify } from "node:util";
|
|
30
|
-
import { loadAll } from "js-yaml";
|
|
31
28
|
import type { Capability, DeployContext } from "@intentius/chant/components/capability";
|
|
29
|
+
import {
|
|
30
|
+
defaultKustomizeRunner,
|
|
31
|
+
renderCommand,
|
|
32
|
+
renderKustomizeDocuments,
|
|
33
|
+
type KustomizeRunner,
|
|
34
|
+
} from "../kustomize/render";
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/** Injectable shell runner — the helm-upgrade seam, same buffer bound. */
|
|
36
|
-
export type KustomizeRunner = (command: string) => Promise<{ stdout: string }>;
|
|
37
|
-
const defaultRunner: KustomizeRunner = (command) => execAsync(command, { maxBuffer: 64 * 1024 * 1024 });
|
|
38
|
-
|
|
39
|
-
/** Single-quote shell escaping, as helm-upgrade quotes its argv. */
|
|
40
|
-
function q(v: string): string {
|
|
41
|
-
return `'${v.replace(/'/g, "'\\''")}'`;
|
|
42
|
-
}
|
|
36
|
+
export { renderCommand, type KustomizeRunner };
|
|
43
37
|
|
|
44
38
|
/** Structural mirrors of the activity module's shapes (see the module doc). */
|
|
45
39
|
interface AppliedRef {
|
|
@@ -81,33 +75,10 @@ export interface KustomizeApplyInput {
|
|
|
81
75
|
delete?: "never" | "owned-only";
|
|
82
76
|
}
|
|
83
77
|
|
|
84
|
-
/** The exact render command, standalone binary first. Pure; exported for tests. */
|
|
85
|
-
export function renderCommand(dir: string, tool: "kustomize" | "kubectl" = "kustomize"): string {
|
|
86
|
-
return tool === "kustomize" ? `kustomize build ${q(dir)}` : `kubectl kustomize ${q(dir)}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Render the overlay, falling back to kubectl's vendored kustomize when the
|
|
90
|
-
* standalone binary is missing (ENOENT/127), and parse the emitted YAML. */
|
|
91
|
-
async function renderDocuments(dir: string, run: KustomizeRunner): Promise<Array<Record<string, unknown>>> {
|
|
92
|
-
let stdout: string;
|
|
93
|
-
try {
|
|
94
|
-
({ stdout } = await run(renderCommand(dir, "kustomize")));
|
|
95
|
-
} catch (err) {
|
|
96
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
-
if (!/ENOENT|not found|command not found|127/.test(message)) throw err;
|
|
98
|
-
({ stdout } = await run(renderCommand(dir, "kubectl")));
|
|
99
|
-
}
|
|
100
|
-
const documents: Array<Record<string, unknown>> = [];
|
|
101
|
-
for (const doc of loadAll(stdout)) {
|
|
102
|
-
if (doc && typeof doc === "object" && !Array.isArray(doc)) documents.push(doc as Record<string, unknown>);
|
|
103
|
-
}
|
|
104
|
-
return documents;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
78
|
/** Factory with injectable renderer + applier, so tests assert the render
|
|
108
79
|
* command and the exact applier delegation without kustomize or a cluster. */
|
|
109
80
|
export function createKustomizeApplyCapability(
|
|
110
|
-
run: KustomizeRunner =
|
|
81
|
+
run: KustomizeRunner = defaultKustomizeRunner,
|
|
111
82
|
apply?: Applier,
|
|
112
83
|
): Capability<KustomizeApplyInput, KustomizeApplyOutcome> {
|
|
113
84
|
return {
|
|
@@ -116,7 +87,7 @@ export function createKustomizeApplyCapability(
|
|
|
116
87
|
// undo of its own — same posture as kubectl-apply.
|
|
117
88
|
rollbackPolicy: "needs-opt-out",
|
|
118
89
|
async run(ctx: DeployContext, input: KustomizeApplyInput): Promise<KustomizeApplyOutcome> {
|
|
119
|
-
const documents = await
|
|
90
|
+
const documents = await renderKustomizeDocuments(input.dir, run);
|
|
120
91
|
const applier: Applier = apply ?? ((await import("../op/activities/kubectl")).applyManifest as unknown as Applier);
|
|
121
92
|
return applier({
|
|
122
93
|
manifest: `kustomize:${input.dir}`,
|