Files
auricom-home-cluster/.github/scripts/helmReleaseDiff.mjs
2022-12-21 03:02:39 +01:00

160 lines
5.3 KiB
JavaScript
Executable File

#!/usr/bin/env zx
$.verbose = false;
/**
* * helmReleaseDiff.mjs
* * Runs `helm template` with your Helm values and then runs `dyff` across Flux HelmRelease manifests
* @param --current-release The source Flux HelmRelease to compare against the target
* @param --incoming-release The target Flux HelmRelease to compare against the source
* @param --kubernetes-dir The directory containing your Flux manifests including the HelmRepository manifests
* * Limitations:
* * Does not work with multiple HelmRelease maninfests in the same YAML document
*/
const CurrentRelease = argv["current-release"];
const IncomingRelease = argv["incoming-release"];
const KubernetesDir = argv["kubernetes-dir"];
const dyff = await which("dyff");
const helm = await which("helm");
const kustomize = await which("kustomize");
async function helmRelease(releaseFile) {
const helmRelease = await fs.readFile(releaseFile, "utf8");
const doc = YAML.parseAllDocuments(helmRelease).map((item) => item.toJS());
const release = doc.filter(
(item) =>
item.apiVersion === "helm.toolkit.fluxcd.io/v2beta1" &&
item.kind === "HelmRelease"
);
return release[0];
}
async function helmRepositoryUrl(kubernetesDir, releaseName) {
const files = await globby([`${kubernetesDir}/**/*.yaml`]);
for await (const file of files) {
const contents = await fs.readFile(file, "utf8");
const doc = YAML.parseAllDocuments(contents).map((item) => item.toJS());
if (
"apiVersion" in doc[0] &&
doc[0].apiVersion === "source.toolkit.fluxcd.io/v1beta2" &&
"kind" in doc[0] &&
doc[0].kind === "HelmRepository" &&
"metadata" in doc[0] &&
"name" in doc[0].metadata &&
doc[0].metadata.name === releaseName
) {
return doc[0].spec.url;
}
}
}
async function kustomizeBuild(releaseBaseDir, releaseName) {
const build =
await $`${kustomize} build --load-restrictor=LoadRestrictionsNone ${releaseBaseDir}`;
const docs = YAML.parseAllDocuments(build.stdout).map((item) => item.toJS());
const release = docs.filter(
(item) =>
item.apiVersion === "helm.toolkit.fluxcd.io/v2beta1" &&
item.kind === "HelmRelease" &&
item.metadata.name === releaseName
);
return release[0];
}
async function helmRepoAdd(registryName, registryUrl) {
await $`${helm} repo add ${registryName} ${registryUrl}`;
}
async function helmTemplate(
releaseName,
registryName,
chartName,
chartVersion,
chartValues
) {
const values = new YAML.Document();
values.contents = chartValues;
const valuesFile = await $`mktemp`;
await fs.writeFile(valuesFile.stdout.trim(), values.toString());
const manifestsFile = await $`mktemp`;
const manifests =
await $`${helm} template --kube-version 1.26.0 --release-name ${releaseName} --include-crds=false ${registryName}/${chartName} --version ${chartVersion} --values ${valuesFile.stdout.trim()}`;
// Remove docs that are CustomResourceDefinition and keys which contain generated fields
let documents = YAML.parseAllDocuments(manifests.stdout.trim());
documents = documents.filter(
(doc) => doc.get("kind") !== "CustomResourceDefinition"
);
documents.forEach((doc) => {
const del = (path) => (doc.hasIn(path) ? doc.deleteIn(path) : false);
del(["metadata", "labels", "app.kubernetes.io/version"]);
del(["metadata", "labels", "chart"]);
del(["metadata", "labels", "helm.sh/chart"]);
del([
"spec",
"template",
"metadata",
"labels",
"app.kubernetes.io/version",
]);
del(["spec", "template", "metadata", "labels", "chart"]);
del(["spec", "template", "metadata", "labels", "helm.sh/chart"]);
});
await fs.writeFile(
manifestsFile.stdout.trim(),
documents.map((doc) => doc.toString({ directives: true })).join("\n")
);
return manifestsFile.stdout.trim();
}
// Generate current template from Helm values
const currentRelease = await helmRelease(CurrentRelease);
const currentBuild = await kustomizeBuild(
path.dirname(CurrentRelease),
currentRelease.metadata.name
);
const currentRepositoryUrl = await helmRepositoryUrl(
KubernetesDir,
currentBuild.spec.chart.spec.sourceRef.name
);
await helmRepoAdd(
currentBuild.spec.chart.spec.sourceRef.name,
currentRepositoryUrl
);
const currentManifests = await helmTemplate(
currentBuild.metadata.name,
currentBuild.spec.chart.spec.sourceRef.name,
currentBuild.spec.chart.spec.chart,
currentBuild.spec.chart.spec.version,
currentBuild.spec.values
);
// Generate incoming template from Helm values
const incomingRelease = await helmRelease(IncomingRelease);
const incomingBuild = await kustomizeBuild(
path.dirname(IncomingRelease),
incomingRelease.metadata.name
);
const incomingRepositoryUrl = await helmRepositoryUrl(
KubernetesDir,
incomingBuild.spec.chart.spec.sourceRef.name
);
await helmRepoAdd(
incomingBuild.spec.chart.spec.sourceRef.name,
incomingRepositoryUrl
);
const incomingManifests = await helmTemplate(
incomingBuild.metadata.name,
incomingBuild.spec.chart.spec.sourceRef.name,
incomingBuild.spec.chart.spec.chart,
incomingBuild.spec.chart.spec.version,
incomingBuild.spec.values
);
// Print diff using dyff
const diff =
await $`${dyff} --color=off --truecolor=off between --omit-header --ignore-order-changes --detect-kubernetes=true --output=human ${currentManifests} ${incomingManifests}`;
echo(diff.stdout.trim());