@tahminator/pipeline 1.0.90-beta.gd62102f7 → 1.0.91
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/README.md +7 -9
- package/dist/flux/client.d.ts +7 -16
- package/dist/flux/client.js +105 -53
- package/dist/flux/format.js +24 -3
- package/dist/flux/index.d.ts +1 -3
- package/dist/flux/index.js +0 -3
- package/dist/flux/render.d.ts +20 -3
- package/dist/flux/render.js +50 -9
- package/dist/flux/types.d.ts +27 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -312,24 +312,22 @@ const flux = new FluxClient(gh, {
|
|
|
312
312
|
isLocalSource: (ref) => ref.kind === "GitRepository" && ref.name === "flux-system",
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
-
// renders every Flux Kustomization at baseRef and in the working tree
|
|
316
|
-
|
|
317
|
-
// result.changed: [{ name, namespace, path, status, diff, additions, deletions, notes }]
|
|
318
|
-
// result.skipped: Kustomizations whose source is not this repository
|
|
319
|
-
|
|
320
|
-
// posts a new PR comment. If it would exceed GitHub's comment size limit, the full
|
|
315
|
+
// renders every Flux Kustomization at baseRef and in the working tree, then posts the
|
|
316
|
+
// changed ones as a new PR comment. If comment would exceed GitHub's comment size limit, the full
|
|
321
317
|
// comment is uploaded as a workflow run artifact and the PR comment links to it instead.
|
|
322
|
-
await flux.
|
|
318
|
+
await flux.diff({
|
|
319
|
+
baseRef: process.env.BASE_SHA!,
|
|
323
320
|
owner: "tahminator",
|
|
324
321
|
repository: "infra",
|
|
325
322
|
prId: 123,
|
|
326
|
-
result,
|
|
327
323
|
title: "Rendered manifest diff", // optional
|
|
328
324
|
description: "Secrets are shown SOPS-encrypted.", // optional
|
|
329
325
|
});
|
|
330
326
|
```
|
|
331
327
|
|
|
332
|
-
Each Kustomization is rendered as `kustomize build` of `spec.path` with `targetNamespace`, `namePrefix`, `nameSuffix`, `components`, `images`, `patches` and `commonMetadata` applied on top, followed by `postBuild.substitute`. Paths without a `kustomization.yaml` get one generated, as Flux does. Values from `postBuild.substituteFrom` live in the cluster, so those variables are left as-is and listed in the diff's notes. A build failure is shown in the
|
|
328
|
+
Each Kustomization is rendered as `kustomize build` of `spec.path` with `targetNamespace`, `namePrefix`, `nameSuffix`, `components`, `images`, `patches` and `commonMetadata` applied on top, followed by `postBuild.substitute`. Paths without a `kustomization.yaml` get one generated, as Flux does. Values from `postBuild.substituteFrom` live in the cluster, so those variables are left as-is and listed in the diff's notes. A build failure is shown in the comment instead of being skipped. `HelmRelease` objects are diffed as manifests; charts are not templated.
|
|
329
|
+
|
|
330
|
+
The diff is split per Kubernetes object (matched by API group, kind, namespace and name), and each object is labelled with the file that defines it, taken from kustomize's `originAnnotations`. Generated ConfigMaps and Secrets point at the `kustomization.yaml` whose generator creates them. Files that only patch an object are not recorded, so a change made by a patch is attributed to the object's original file. A renamed object (e.g. a new generator hash suffix) shows as one removal and one addition.
|
|
333
331
|
|
|
334
332
|
### `DockerClient`
|
|
335
333
|
|
package/dist/flux/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GitHubClient } from "../gh";
|
|
2
2
|
import { type FormatFluxDiffOpts } from "./format";
|
|
3
|
-
import { type FluxClientOpts, type
|
|
3
|
+
import { type FluxClientOpts, type FluxKustomization } from "./types";
|
|
4
4
|
/**
|
|
5
5
|
* Works with a repository of Flux manifests: finds its Flux `Kustomization`s, renders them the
|
|
6
6
|
* way kustomize-controller would, and reports how a change affects the rendered output.
|
|
@@ -16,27 +16,18 @@ export declare class FluxClient {
|
|
|
16
16
|
findKustomizations(root?: string): Promise<FluxKustomization[]>;
|
|
17
17
|
/**
|
|
18
18
|
* Renders every Flux `Kustomization` at `baseRef` and in the working tree at `root`,
|
|
19
|
-
*
|
|
19
|
+
* and posts the ones whose output differs as a new PR comment.
|
|
20
20
|
*
|
|
21
21
|
* `baseRef` is fetched from `origin` if it is not available locally (e.g. shallow CI clones).
|
|
22
|
-
*/
|
|
23
|
-
diff({ baseRef, root, }: {
|
|
24
|
-
baseRef: string;
|
|
25
|
-
root?: string;
|
|
26
|
-
}): Promise<FluxDiffResult>;
|
|
27
|
-
/**
|
|
28
|
-
* Posts a {@link diff} result as a new PR comment.
|
|
29
|
-
*
|
|
30
22
|
* If the comment would exceed GitHub's size limit, the full comment is uploaded as an
|
|
31
23
|
* artifact of the current workflow run and the PR comment links to it instead.
|
|
32
24
|
*/
|
|
33
|
-
|
|
25
|
+
diff({ baseRef, root, owner, repository, prId, ...formatOpts }: {
|
|
26
|
+
baseRef: string;
|
|
27
|
+
root?: string;
|
|
34
28
|
owner: string;
|
|
35
29
|
repository: string;
|
|
36
30
|
prId: number;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
message: string;
|
|
40
|
-
artifactUrl?: string;
|
|
41
|
-
}>;
|
|
31
|
+
} & Omit<FormatFluxDiffOpts, "artifactUrl">): Promise<void>;
|
|
32
|
+
private collectDiff;
|
|
42
33
|
}
|
package/dist/flux/client.js
CHANGED
|
@@ -31,11 +31,45 @@ export class FluxClient {
|
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Renders every Flux `Kustomization` at `baseRef` and in the working tree at `root`,
|
|
34
|
-
*
|
|
34
|
+
* and posts the ones whose output differs as a new PR comment.
|
|
35
35
|
*
|
|
36
36
|
* `baseRef` is fetched from `origin` if it is not available locally (e.g. shallow CI clones).
|
|
37
|
+
* If the comment would exceed GitHub's size limit, the full comment is uploaded as an
|
|
38
|
+
* artifact of the current workflow run and the PR comment links to it instead.
|
|
37
39
|
*/
|
|
38
|
-
async diff({ baseRef, root = ".", }) {
|
|
40
|
+
async diff({ baseRef, root = ".", owner, repository, prId, ...formatOpts }) {
|
|
41
|
+
const result = await this.collectDiff(baseRef, root);
|
|
42
|
+
const full = formatFluxDiff(result, formatOpts);
|
|
43
|
+
if (full.length <= MAX_COMMENT_LENGTH) {
|
|
44
|
+
await this.ghClient.sendPrMessage({
|
|
45
|
+
owner,
|
|
46
|
+
repository,
|
|
47
|
+
prId,
|
|
48
|
+
message: full,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const dir = await mkdtemp(path.join(tmpdir(), "flux-diff-artifact-"));
|
|
53
|
+
try {
|
|
54
|
+
const file = path.join(dir, "flux-diff.md");
|
|
55
|
+
await Bun.write(file, full);
|
|
56
|
+
const attempt = process.env.GITHUB_RUN_ATTEMPT ?? "1";
|
|
57
|
+
const { url } = await this.ghClient.uploadArtifact({
|
|
58
|
+
name: `flux-diff-pr-${prId}-attempt-${attempt}`,
|
|
59
|
+
files: [file],
|
|
60
|
+
rootDirectory: dir,
|
|
61
|
+
});
|
|
62
|
+
const message = formatFluxDiff(result, {
|
|
63
|
+
...formatOpts,
|
|
64
|
+
artifactUrl: url,
|
|
65
|
+
});
|
|
66
|
+
await this.ghClient.sendPrMessage({ owner, repository, prId, message });
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rm(dir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async collectDiff(baseRef, root) {
|
|
39
73
|
const workdir = await mkdtemp(path.join(tmpdir(), "flux-diff-"));
|
|
40
74
|
const baseRoot = path.join(workdir, "base");
|
|
41
75
|
await $ `git -C ${root} fetch --depth=1 origin ${baseRef}`.quiet().nothrow();
|
|
@@ -55,6 +89,8 @@ export class FluxClient {
|
|
|
55
89
|
}
|
|
56
90
|
const changed = [];
|
|
57
91
|
const skipped = [];
|
|
92
|
+
let nextDiffId = 0;
|
|
93
|
+
const nextDiffDir = () => path.join(workdir, "diffs", String(nextDiffId++));
|
|
58
94
|
await Promise.all([...pairs.values()].map(async ({ base, head }) => {
|
|
59
95
|
const current = (head ?? base);
|
|
60
96
|
const { name, namespace } = current;
|
|
@@ -73,13 +109,23 @@ export class FluxClient {
|
|
|
73
109
|
base ? renderKustomization(baseRoot, base) : undefined,
|
|
74
110
|
head ? renderKustomization(root, head) : undefined,
|
|
75
111
|
]);
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
112
|
+
const buildError = headRender?.error;
|
|
113
|
+
const objects = buildError ?
|
|
114
|
+
[]
|
|
115
|
+
: await diffObjects(baseRender?.objects ?? [], headRender?.objects ?? [], nextDiffDir);
|
|
116
|
+
const unchanged = buildError ?
|
|
117
|
+
buildError === baseRender?.error
|
|
118
|
+
: objects.length === 0;
|
|
119
|
+
if (unchanged) {
|
|
79
120
|
return;
|
|
80
121
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
122
|
+
const notes = new Set([
|
|
123
|
+
...(baseRender?.notes ?? []),
|
|
124
|
+
...(headRender?.notes ?? []),
|
|
125
|
+
]);
|
|
126
|
+
if (baseRender?.error && !buildError) {
|
|
127
|
+
notes.add("`kustomize build` fails at the base commit, so every object is shown as added.");
|
|
128
|
+
}
|
|
83
129
|
changed.push({
|
|
84
130
|
name,
|
|
85
131
|
namespace,
|
|
@@ -87,15 +133,11 @@ export class FluxClient {
|
|
|
87
133
|
status: !base ? "added"
|
|
88
134
|
: !head ? "removed"
|
|
89
135
|
: "changed",
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
...(baseRender?.notes ?? []),
|
|
96
|
-
...(headRender?.notes ?? []),
|
|
97
|
-
]),
|
|
98
|
-
],
|
|
136
|
+
objects,
|
|
137
|
+
...(buildError ? { buildError } : {}),
|
|
138
|
+
additions: objects.reduce((sum, o) => sum + o.additions, 0),
|
|
139
|
+
deletions: objects.reduce((sum, o) => sum + o.deletions, 0),
|
|
140
|
+
notes: [...notes],
|
|
99
141
|
});
|
|
100
142
|
}));
|
|
101
143
|
const byLabel = (a, b) => label(a).localeCompare(label(b));
|
|
@@ -112,44 +154,54 @@ export class FluxClient {
|
|
|
112
154
|
await rm(workdir, { recursive: true, force: true });
|
|
113
155
|
}
|
|
114
156
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
message: full,
|
|
129
|
-
});
|
|
130
|
-
return { message: full };
|
|
131
|
-
}
|
|
132
|
-
const dir = await mkdtemp(path.join(tmpdir(), "flux-diff-artifact-"));
|
|
133
|
-
try {
|
|
134
|
-
const file = path.join(dir, "flux-diff.md");
|
|
135
|
-
await Bun.write(file, full);
|
|
136
|
-
const attempt = process.env.GITHUB_RUN_ATTEMPT ?? "1";
|
|
137
|
-
const { url } = await this.ghClient.uploadArtifact({
|
|
138
|
-
name: `flux-diff-pr-${prId}-attempt-${attempt}`,
|
|
139
|
-
files: [file],
|
|
140
|
-
rootDirectory: dir,
|
|
141
|
-
});
|
|
142
|
-
const message = formatFluxDiff(result, {
|
|
143
|
-
...formatOpts,
|
|
144
|
-
artifactUrl: url,
|
|
145
|
-
});
|
|
146
|
-
await this.ghClient.sendPrMessage({ owner, repository, prId, message });
|
|
147
|
-
return { message, artifactUrl: url };
|
|
148
|
-
}
|
|
149
|
-
finally {
|
|
150
|
-
await rm(dir, { recursive: true, force: true });
|
|
151
|
-
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Pairs base and head objects by API group, kind, namespace and name, and diffs the ones whose
|
|
160
|
+
* rendered YAML differs. An apiVersion bump within a group is a change, not a remove + add.
|
|
161
|
+
*/
|
|
162
|
+
async function diffObjects(baseObjects, headObjects, nextDir) {
|
|
163
|
+
const key = (object) => {
|
|
164
|
+
const group = object.apiVersion.includes("/") ? object.apiVersion.split("/")[0] : "";
|
|
165
|
+
return [group, object.kind, object.namespace, object.name].join("\0");
|
|
166
|
+
};
|
|
167
|
+
const pairs = new Map();
|
|
168
|
+
for (const object of baseObjects) {
|
|
169
|
+
pairs.set(key(object), { base: object });
|
|
152
170
|
}
|
|
171
|
+
for (const object of headObjects) {
|
|
172
|
+
const k = key(object);
|
|
173
|
+
pairs.set(k, { ...pairs.get(k), head: object });
|
|
174
|
+
}
|
|
175
|
+
const diffs = await Promise.all([...pairs.values()].map(async ({ base, head }) => {
|
|
176
|
+
if (base?.yaml === head?.yaml) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const current = (head ?? base);
|
|
180
|
+
const { kind, namespace, name, file } = current;
|
|
181
|
+
const diff = await unifiedDiff(nextDir(), [kind, namespace, name].filter(Boolean).join("/"), base?.yaml ?? "", head?.yaml ?? "");
|
|
182
|
+
const lines = diff.split("\n");
|
|
183
|
+
const objectDiff = {
|
|
184
|
+
kind,
|
|
185
|
+
namespace,
|
|
186
|
+
name,
|
|
187
|
+
...(file ? { file } : {}),
|
|
188
|
+
status: !base ? "added"
|
|
189
|
+
: !head ? "removed"
|
|
190
|
+
: "changed",
|
|
191
|
+
diff,
|
|
192
|
+
additions: lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length,
|
|
193
|
+
deletions: lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length,
|
|
194
|
+
};
|
|
195
|
+
return objectDiff;
|
|
196
|
+
}));
|
|
197
|
+
return diffs
|
|
198
|
+
.filter((diff) => diff !== null)
|
|
199
|
+
.sort((a, b) =>
|
|
200
|
+
// objects without a recorded file sort last
|
|
201
|
+
(a.file ?? "\uffff").localeCompare(b.file ?? "\uffff") ||
|
|
202
|
+
a.kind.localeCompare(b.kind) ||
|
|
203
|
+
a.namespace.localeCompare(b.namespace) ||
|
|
204
|
+
a.name.localeCompare(b.name));
|
|
153
205
|
}
|
|
154
206
|
async function unifiedDiff(dir, targetPath, baseOutput, headOutput) {
|
|
155
207
|
await mkdir(dir, { recursive: true });
|
package/dist/flux/format.js
CHANGED
|
@@ -9,11 +9,26 @@ export function formatFluxDiff(result, { title = "Flux diff", description, artif
|
|
|
9
9
|
`No Flux Kustomization renders differently against ${base}.`
|
|
10
10
|
: `${result.changed.length} Flux Kustomization(s) render differently against ${base}.`, "");
|
|
11
11
|
if (artifactUrl) {
|
|
12
|
-
lines.push(`> [!WARNING]`, `> The rendered diff is too long for a GitHub comment. [Download the full diff from the workflow run](${artifactUrl}).`, ""
|
|
12
|
+
lines.push(`> [!WARNING]`, `> The rendered diff is too long for a GitHub comment. [Download the full diff from the workflow run](${artifactUrl}).`, "");
|
|
13
|
+
for (const change of result.changed) {
|
|
14
|
+
lines.push(`- ${summary(change)}`);
|
|
15
|
+
if (change.buildError) {
|
|
16
|
+
lines.push(" - `kustomize build` failed");
|
|
17
|
+
}
|
|
18
|
+
lines.push(...change.objects.map((object) => ` - ${objectSummary(object)}`));
|
|
19
|
+
}
|
|
20
|
+
lines.push("");
|
|
13
21
|
}
|
|
14
22
|
else {
|
|
15
23
|
for (const change of result.changed) {
|
|
16
|
-
lines.push(`<details><summary>${summary(change)}</summary>`, "", ...change.notes.map((note) => `> ${note}\n`)
|
|
24
|
+
lines.push(`<details><summary>${summary(change)}</summary>`, "", ...change.notes.map((note) => `> ${note}\n`));
|
|
25
|
+
if (change.buildError) {
|
|
26
|
+
lines.push("`kustomize build` failed:", "", "````text", change.buildError, "````", "");
|
|
27
|
+
}
|
|
28
|
+
for (const object of change.objects) {
|
|
29
|
+
lines.push(objectSummary(object), "", "````diff", object.diff.trimEnd(), "````", "");
|
|
30
|
+
}
|
|
31
|
+
lines.push("</details>", "");
|
|
17
32
|
}
|
|
18
33
|
}
|
|
19
34
|
if (result.skipped.length > 0) {
|
|
@@ -25,5 +40,11 @@ export function label({ name, namespace, }) {
|
|
|
25
40
|
return namespace ? `${namespace}/${name}` : name;
|
|
26
41
|
}
|
|
27
42
|
function summary(change) {
|
|
28
|
-
|
|
43
|
+
const counts = change.buildError ? "build failed" : (`${change.objects.length} object(s), +${change.additions} −${change.deletions}`);
|
|
44
|
+
return `<code>${label(change)}</code> — <code>${change.path}</code> (${change.status}, ${counts})`;
|
|
45
|
+
}
|
|
46
|
+
function objectSummary(object) {
|
|
47
|
+
const name = [object.namespace, object.name].filter(Boolean).join("/");
|
|
48
|
+
const file = object.file ? ` — \`${object.file}\`` : "";
|
|
49
|
+
return `**\`${object.kind}\` \`${name}\`**${file} (${object.status}, +${object.additions} −${object.deletions})`;
|
|
29
50
|
}
|
package/dist/flux/index.d.ts
CHANGED
package/dist/flux/index.js
CHANGED
package/dist/flux/render.d.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import type { FluxKustomization } from "./types";
|
|
2
|
-
export type
|
|
3
|
-
|
|
2
|
+
export type RenderedObject = {
|
|
3
|
+
apiVersion: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
namespace: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/**
|
|
8
|
+
* repository-relative file that defines the object (for generated objects, the
|
|
9
|
+
* kustomization.yaml whose generator creates it); `undefined` if kustomize did not record one
|
|
10
|
+
*/
|
|
11
|
+
file?: string;
|
|
12
|
+
/** the object's rendered YAML, without the origin annotation */
|
|
13
|
+
yaml: string;
|
|
14
|
+
};
|
|
15
|
+
type RenderedKustomization = {
|
|
16
|
+
/** rendered objects, in kustomize's output order; empty when `error` is set */
|
|
17
|
+
objects: RenderedObject[];
|
|
18
|
+
/** `kustomize build` error output, when the build failed */
|
|
19
|
+
error?: string;
|
|
4
20
|
notes: string[];
|
|
5
21
|
};
|
|
6
22
|
/**
|
|
@@ -9,7 +25,7 @@ export type RenderedKustomization = {
|
|
|
9
25
|
* `nameSuffix`, `components`, `images`, `patches`, `commonMetadata`) applied on top,
|
|
10
26
|
* followed by `postBuild` variable substitution.
|
|
11
27
|
*
|
|
12
|
-
*
|
|
28
|
+
* Each rendered object records the file that defines it.
|
|
13
29
|
*/
|
|
14
30
|
export declare function renderKustomization(root: string, kustomization: FluxKustomization): Promise<RenderedKustomization>;
|
|
15
31
|
/**
|
|
@@ -25,3 +41,4 @@ export declare function substituteVariables(input: string, vars: Record<string,
|
|
|
25
41
|
output: string;
|
|
26
42
|
unresolved: string[];
|
|
27
43
|
};
|
|
44
|
+
export {};
|
package/dist/flux/render.js
CHANGED
|
@@ -10,13 +10,15 @@ const KUSTOMIZATION_FILES = [
|
|
|
10
10
|
const SUBSTITUTE_DISABLED = /kustomize\.toolkit\.fluxcd\.io\/substitute:\s*["']?disabled["']?/;
|
|
11
11
|
/** `${VAR}`, `${VAR:-default}`, `${VAR-default}`, `${VAR:=default}`, `${VAR=default}`; `$${VAR}` escapes. */
|
|
12
12
|
const VARIABLE = /\$(\$)?\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?[-=])([^}]*))?\}/g;
|
|
13
|
+
/** kustomize's `originAnnotations` build metadata; records the file that defines each object. */
|
|
14
|
+
const ORIGIN_ANNOTATION = "config.kubernetes.io/origin";
|
|
13
15
|
/**
|
|
14
16
|
* Renders a Flux `Kustomization` the way kustomize-controller does: `kustomize build` of
|
|
15
17
|
* `spec.path` with the overlay fields from the spec (`targetNamespace`, `namePrefix`,
|
|
16
18
|
* `nameSuffix`, `components`, `images`, `patches`, `commonMetadata`) applied on top,
|
|
17
19
|
* followed by `postBuild` variable substitution.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
21
|
+
* Each rendered object records the file that defines it.
|
|
20
22
|
*/
|
|
21
23
|
export async function renderKustomization(root, kustomization) {
|
|
22
24
|
const notes = [];
|
|
@@ -45,19 +47,14 @@ export async function renderKustomization(root, kustomization) {
|
|
|
45
47
|
[{ pairs: spec.commonMetadata.labels, includeSelectors: false }]
|
|
46
48
|
: undefined,
|
|
47
49
|
commonAnnotations: spec.commonMetadata?.annotations,
|
|
50
|
+
buildMetadata: ["originAnnotations"],
|
|
48
51
|
};
|
|
49
52
|
await Bun.write(path.join(wrapper, "kustomization.yaml"), yaml.stringify(wrapperKustomization));
|
|
50
53
|
const result = await $ `kustomize build --load-restrictor LoadRestrictionsNone ${wrapper}`
|
|
51
54
|
.quiet()
|
|
52
55
|
.nothrow();
|
|
53
56
|
if (result.exitCode !== 0) {
|
|
54
|
-
|
|
55
|
-
.toString()
|
|
56
|
-
.trim()
|
|
57
|
-
.split("\n")
|
|
58
|
-
.map((line) => `# ${line}`)
|
|
59
|
-
.join("\n");
|
|
60
|
-
return { output: `# kustomize build failed\n${stderr}\n`, notes };
|
|
57
|
+
return { objects: [], error: result.stderr.toString().trim(), notes };
|
|
61
58
|
}
|
|
62
59
|
let output = result.stdout.toString();
|
|
63
60
|
if (spec.postBuild) {
|
|
@@ -70,12 +67,56 @@ export async function renderKustomization(root, kustomization) {
|
|
|
70
67
|
.join(", ")}.`);
|
|
71
68
|
}
|
|
72
69
|
}
|
|
73
|
-
|
|
70
|
+
const docs = yaml.parseAllDocuments(output);
|
|
71
|
+
const invalid = docs.find((doc) => doc.errors.length > 0);
|
|
72
|
+
if (invalid) {
|
|
73
|
+
return {
|
|
74
|
+
objects: [],
|
|
75
|
+
error: `rendered output is not valid YAML: ${invalid.errors[0]?.message}`,
|
|
76
|
+
notes,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const objects = docs
|
|
80
|
+
.filter((doc) => doc.contents !== null)
|
|
81
|
+
.map((doc) => toRenderedObject(doc, root, wrapper));
|
|
82
|
+
return { objects, notes };
|
|
74
83
|
}
|
|
75
84
|
finally {
|
|
76
85
|
await rm(wrapper, { recursive: true, force: true });
|
|
77
86
|
}
|
|
78
87
|
}
|
|
88
|
+
function toRenderedObject(doc, root, wrapper) {
|
|
89
|
+
const origin = doc.getIn(["metadata", "annotations", ORIGIN_ANNOTATION]);
|
|
90
|
+
doc.deleteIn(["metadata", "annotations", ORIGIN_ANNOTATION]);
|
|
91
|
+
const annotations = doc.getIn(["metadata", "annotations"]);
|
|
92
|
+
if (yaml.isMap(annotations) && annotations.items.length === 0) {
|
|
93
|
+
doc.deleteIn(["metadata", "annotations"]);
|
|
94
|
+
}
|
|
95
|
+
const value = (doc.toJS() ?? {});
|
|
96
|
+
return {
|
|
97
|
+
apiVersion: String(value.apiVersion ?? ""),
|
|
98
|
+
kind: String(value.kind ?? ""),
|
|
99
|
+
namespace: String(value.metadata?.namespace ?? ""),
|
|
100
|
+
name: String(value.metadata?.name ?? ""),
|
|
101
|
+
file: typeof origin === "string" ?
|
|
102
|
+
originFile(origin, root, wrapper)
|
|
103
|
+
: undefined,
|
|
104
|
+
yaml: doc.toString({ lineWidth: 0 }).replace(/^---\n/, ""),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Converts an origin annotation (`path`, or `configuredIn` for generated objects, relative to
|
|
109
|
+
* the wrapper kustomization) to a repository-relative path. Remote origins are returned as
|
|
110
|
+
* `<repo>//<path>?ref=<ref>`.
|
|
111
|
+
*/
|
|
112
|
+
function originFile(origin, root, wrapper) {
|
|
113
|
+
const parsed = yaml.parse(origin);
|
|
114
|
+
const file = parsed?.path ?? parsed?.configuredIn;
|
|
115
|
+
if (parsed?.repo) {
|
|
116
|
+
return `${parsed.repo}//${file ?? ""}${parsed.ref ? `?ref=${parsed.ref}` : ""}`;
|
|
117
|
+
}
|
|
118
|
+
return file ? path.relative(root, path.resolve(wrapper, file)) : undefined;
|
|
119
|
+
}
|
|
79
120
|
/**
|
|
80
121
|
* Applies Flux `postBuild` substitution to rendered multi-document YAML. Documents annotated
|
|
81
122
|
* with `kustomize.toolkit.fluxcd.io/substitute: disabled` are left untouched.
|
package/dist/flux/types.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export declare const fluxKustomizationSchema: z.ZodObject<{
|
|
|
40
40
|
}, z.core.$strip>>;
|
|
41
41
|
}, z.core.$strip>;
|
|
42
42
|
}, z.core.$strip>;
|
|
43
|
-
|
|
43
|
+
type FluxKustomizationSpec = z.infer<typeof fluxKustomizationSchema>["spec"];
|
|
44
44
|
/** A Flux `Kustomization` found in the repository. */
|
|
45
45
|
export type FluxKustomization = {
|
|
46
46
|
/** repository-relative file the Kustomization is declared in */
|
|
@@ -53,13 +53,35 @@ export type FluxKustomization = {
|
|
|
53
53
|
spec: FluxKustomizationSpec;
|
|
54
54
|
};
|
|
55
55
|
export type FluxSourceRef = FluxKustomizationSpec["sourceRef"];
|
|
56
|
+
type FluxDiffStatus = "added" | "changed" | "removed";
|
|
57
|
+
/** A rendered Kubernetes object whose output differs between base and head. */
|
|
58
|
+
export type FluxObjectDiff = {
|
|
59
|
+
kind: string;
|
|
60
|
+
/** `""` for cluster-scoped objects */
|
|
61
|
+
namespace: string;
|
|
62
|
+
name: string;
|
|
63
|
+
/**
|
|
64
|
+
* repository-relative file that defines the object, from kustomize's origin annotations.
|
|
65
|
+
* Generated objects (ConfigMap/Secret generators) point at the kustomization.yaml that
|
|
66
|
+
* generates them. Files that only patch the object are not recorded.
|
|
67
|
+
*/
|
|
68
|
+
file?: string;
|
|
69
|
+
status: FluxDiffStatus;
|
|
70
|
+
/** unified diff of the object's rendered YAML */
|
|
71
|
+
diff: string;
|
|
72
|
+
additions: number;
|
|
73
|
+
deletions: number;
|
|
74
|
+
};
|
|
56
75
|
export type FluxKustomizationDiff = {
|
|
57
76
|
name: string;
|
|
58
77
|
namespace: string;
|
|
59
78
|
path: string;
|
|
60
|
-
status:
|
|
61
|
-
/**
|
|
62
|
-
|
|
79
|
+
status: FluxDiffStatus;
|
|
80
|
+
/** changed objects, sorted by file then kind, namespace and name */
|
|
81
|
+
objects: FluxObjectDiff[];
|
|
82
|
+
/** `kustomize build` error at head; `objects` is empty when set */
|
|
83
|
+
buildError?: string;
|
|
84
|
+
/** totals across `objects` */
|
|
63
85
|
additions: number;
|
|
64
86
|
deletions: number;
|
|
65
87
|
/** caveats about how faithful the render is, e.g. unresolvable `substituteFrom` variables */
|
|
@@ -92,3 +114,4 @@ export type FluxClientOpts = {
|
|
|
92
114
|
*/
|
|
93
115
|
isLocalSource?: (sourceRef: FluxSourceRef) => boolean;
|
|
94
116
|
};
|
|
117
|
+
export {};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"type": "module",
|
|
4
4
|
"author": "Tahmid Ahmed",
|
|
5
5
|
"description": "A collection of Bun shell scripts that can be re-used in various CICD pipelines.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.91",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "git+https://github.com/tahminator/pipeline.git"
|
|
9
9
|
},
|