renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
321 lines (320 loc) • 13.3 kB
JavaScript
import { regEx } from "../../../util/regex.js";
import { logger } from "../../../logger/index.js";
import { isHttpUrl } from "../../../util/url.js";
import { parseYaml } from "../../../util/yaml.js";
import { coerceArray } from "../../../util/array.js";
import { readLocalFile } from "../../../util/fs/index.js";
import { BitbucketTagsDatasource } from "../../datasource/bitbucket-tags/index.js";
import { DockerDatasource } from "../../datasource/docker/index.js";
import { GitRefsDatasource } from "../../datasource/git-refs/index.js";
import { GitTagsDatasource } from "../../datasource/git-tags/index.js";
import { GithubReleasesDatasource } from "../../datasource/github-releases/index.js";
import { GithubTagsDatasource } from "../../datasource/github-tags/index.js";
import { GitlabTagsDatasource } from "../../datasource/gitlab-tags/index.js";
import { HelmDatasource } from "../../datasource/helm/index.js";
import { getDep } from "../dockerfile/extract.js";
import { isOCIRegistry, removeOCIPrefix } from "../helmv3/oci.js";
import { collectHelmRepos, isSystemManifest, systemManifestHeaderRegex } from "./common.js";
import { findDependencies } from "../helm-values/extract.js";
import { extractImage } from "../kustomize/extract.js";
import { FluxResource } from "./schema.js";
import { isString } from "@sindresorhus/is";
import { isMap, isPair, isScalar, parseAllDocuments } from "yaml";
//#region lib/modules/manager/flux/extract.ts
function readManifest(content, packageFile) {
if (isSystemManifest(packageFile)) {
const versionMatch = regEx(systemManifestHeaderRegex).exec(content);
if (!versionMatch) return null;
return {
kind: "system",
file: packageFile,
content,
version: versionMatch[1],
components: versionMatch[2]
};
}
return {
kind: "resource",
file: packageFile,
content,
resources: parseYaml(content, {
customSchema: FluxResource,
failureBehaviour: "filter"
})
};
}
const githubUrlRegex = regEx(/^(?:https:\/\/|git@)github\.com[/:](?<packageName>[^/]+\/[^/]+?)(?:\.git)?$/);
const gitlabUrlRegex = regEx(/^(?:https:\/\/|git@)gitlab\.com[/:](?<packageName>[^/]+\/[^/]+?)(?:\.git)?$/);
const bitbucketUrlRegex = regEx(/^(?:https:\/\/|git@)bitbucket\.org[/:](?<packageName>[^/]+\/[^/]+?)(?:\.git)?$/);
function resolveGitRepositoryPerSourceTag(dep, gitUrl) {
const githubMatchGroups = githubUrlRegex.exec(gitUrl)?.groups;
if (githubMatchGroups) {
dep.datasource = GithubTagsDatasource.id;
dep.packageName = githubMatchGroups.packageName;
dep.sourceUrl = `https://github.com/${dep.packageName}`;
return;
}
const gitlabMatchGroups = gitlabUrlRegex.exec(gitUrl)?.groups;
if (gitlabMatchGroups) {
dep.datasource = GitlabTagsDatasource.id;
dep.packageName = gitlabMatchGroups.packageName;
dep.sourceUrl = `https://gitlab.com/${dep.packageName}`;
return;
}
const bitbucketMatchGroups = bitbucketUrlRegex.exec(gitUrl)?.groups;
if (bitbucketMatchGroups) {
dep.datasource = BitbucketTagsDatasource.id;
dep.packageName = bitbucketMatchGroups.packageName;
dep.sourceUrl = `https://bitbucket.org/${dep.packageName}`;
return;
}
dep.datasource = GitTagsDatasource.id;
dep.packageName = gitUrl;
if (isHttpUrl(gitUrl)) dep.sourceUrl = gitUrl.replace(/\.git$/, "");
}
function resolveHelmRepository(dep, matchingRepositories, registryAliases, sourceRefName) {
if (matchingRepositories.length) {
dep.registryUrls = matchingRepositories.map((repo) => {
if (repo.spec.type === "oci" || isOCIRegistry(repo.spec.url)) {
dep.datasource = DockerDatasource.id;
dep.packageName = getDep(`${removeOCIPrefix(repo.spec.url)}/${dep.depName}`, false, registryAliases).packageName;
return null;
} else return repo.spec.url;
}).filter(isString);
if (!dep.registryUrls?.length) delete dep.registryUrls;
return;
}
if (sourceRefName && registryAliases) {
const aliasUrl = registryAliases[sourceRefName];
if (aliasUrl) {
if (isOCIRegistry(aliasUrl)) {
dep.datasource = DockerDatasource.id;
dep.packageName = getDep(`${removeOCIPrefix(aliasUrl)}/${dep.depName}`, false, registryAliases).packageName;
} else dep.registryUrls = [aliasUrl];
return;
}
}
dep.skipReason = "unknown-registry";
}
function resolveSystemManifest(manifest) {
return [{
depName: "fluxcd/flux2",
datasource: GithubReleasesDatasource.id,
currentValue: manifest.version,
managerData: { components: manifest.components }
}];
}
/**
* Returns all `spec.ref` map nodes for OCIRepository resources matching `resourceName`.
*/
function findOCIRefNodes(docs, resourceName) {
const refNodes = [];
for (const doc of docs) {
const docContents = doc.contents;
if (!isMap(docContents)) continue;
const kindNode = docContents.get("kind", true);
if (!isScalar(kindNode) || kindNode.value !== "OCIRepository") continue;
const nameNode = docContents.getIn(["metadata", "name"], true);
if (!isScalar(nameNode) || nameNode.value !== resourceName) continue;
const specNode = docContents.get("spec");
if (!isMap(specNode)) continue;
const refNode = specNode.get("ref");
if (isMap(refNode)) refNodes.push(refNode);
}
return refNodes;
}
function extractOCIRefTagAndDigestRange(docs, content, resourceName) {
for (const refNode of findOCIRefNodes(docs, resourceName)) {
let tagKey;
let tagValue;
let digestKey;
let digestValue;
for (const item of refNode.items) {
if (!isPair(item) || !isScalar(item.key)) continue;
if (item.key.value === "tag" && isScalar(item.value)) {
tagKey = item.key;
tagValue = item.value;
} else if (item.key.value === "digest" && isScalar(item.value)) {
digestKey = item.key;
digestValue = item.value;
}
}
if (!tagKey?.range || !tagValue?.range || !digestKey?.range || !digestValue?.range) continue;
const tagFirst = tagKey.range[0] < digestKey.range[0];
const start = tagFirst ? tagKey.range[0] : digestKey.range[0];
const end = tagFirst ? digestValue.range[1] : tagValue.range[1];
return {
replaceString: content.slice(start, end),
tagFirst
};
}
return null;
}
function extractOCIRefTagRange(docs, content, resourceName) {
for (const refNode of findOCIRefNodes(docs, resourceName)) for (const item of refNode.items) if (isPair(item) && isScalar(item.key) && item.key.value === "tag" && isScalar(item.value) && item.key.range && item.value.range) {
const keyStart = item.key.range[0];
const valueEnd = item.value.range[1];
const lineStart = content.lastIndexOf("\n", keyStart - 1) + 1;
return {
replaceString: content.slice(keyStart, valueEnd),
indentation: content.slice(lineStart, keyStart)
};
}
return null;
}
function resolveResourceManifest(manifest, helmRepositories, registryAliases, content) {
let docs;
const deps = [];
for (const resource of manifest.resources) switch (resource.kind) {
case "HelmRelease":
if (resource.spec.chartRef) logger.trace("HelmRelease using chartRef was found, skipping as version will be handled via referenced resource directly");
else if (resource.spec.chart) {
const chartSpec = resource.spec.chart.spec;
const depName = chartSpec.chart;
const dep = {
depName,
currentValue: resource.spec.chart.spec.version,
datasource: HelmDatasource.id
};
if (depName.startsWith("./")) {
dep.skipReason = "local-chart";
delete dep.datasource;
} else {
const sourceRef = chartSpec.sourceRef;
resolveHelmRepository(dep, helmRepositories.filter((rep) => rep.kind === sourceRef?.kind && rep.metadata.name === sourceRef.name && rep.metadata.namespace === (sourceRef?.namespace ?? resource.metadata?.namespace)), registryAliases, sourceRef?.name);
}
deps.push(dep);
} else logger.debug(`invalid or incomplete ${resource.metadata.name} HelmRelease spec, skipping`);
if (resource.spec.values) {
logger.trace("detecting dependencies in HelmRelease values");
deps.push(...findDependencies(resource.spec.values, registryAliases));
}
break;
case "HelmChart": {
if (resource.spec.sourceRef.kind === "GitRepository") {
logger.trace("HelmChart using GitRepository was found, skipping as version will be handled via referenced resource directly");
continue;
}
const dep = { depName: resource.spec.chart };
if (resource.spec.sourceRef.kind === "HelmRepository") {
dep.currentValue = resource.spec.version;
dep.datasource = HelmDatasource.id;
const sourceRef = resource.spec.sourceRef;
resolveHelmRepository(dep, helmRepositories.filter((rep) => rep.kind === sourceRef?.kind && rep.metadata.name === sourceRef.name && rep.metadata.namespace === resource.metadata?.namespace), registryAliases, sourceRef?.name);
} else dep.skipReason = "unsupported-datasource";
deps.push(dep);
break;
}
case "GitRepository": {
const dep = { depName: resource.metadata.name };
if (resource.spec.ref?.commit) {
const gitUrl = resource.spec.url;
dep.currentDigest = resource.spec.ref.commit;
dep.datasource = GitRefsDatasource.id;
dep.packageName = gitUrl;
dep.replaceString = resource.spec.ref.commit;
if (isHttpUrl(gitUrl)) dep.sourceUrl = gitUrl.replace(/\.git$/, "");
} else if (resource.spec.ref?.tag) {
dep.currentValue = resource.spec.ref.tag;
resolveGitRepositoryPerSourceTag(dep, resource.spec.url);
} else dep.skipReason = "unversioned-reference";
deps.push(dep);
break;
}
case "OCIRepository": {
const container = removeOCIPrefix(resource.spec.url);
if (resource.spec.ref?.digest && resource.spec.ref?.tag) {
const combinedDep = getDep(`${container}@${resource.spec.ref.digest}`, false, registryAliases);
combinedDep.currentValue = resource.spec.ref.tag;
const refRange = extractOCIRefTagAndDigestRange(docs ??= parseAllDocuments(content, { strict: false }), content, resource.metadata.name);
if (refRange) {
combinedDep.replaceString = refRange.replaceString;
if (refRange.tagFirst) combinedDep.autoReplaceStringTemplate = refRange.replaceString.replace(resource.spec.ref.tag, "{{newValue}}").replace(resource.spec.ref.digest, "{{newDigest}}");
else combinedDep.autoReplaceStringTemplate = refRange.replaceString.replace(resource.spec.ref.digest, "{{newDigest}}").replace(resource.spec.ref.tag, "{{newValue}}");
} else {
logger.debug({
file: manifest.file,
name: resource.metadata.name
}, "Could not find tag/digest nodes in content, skipping replacement");
combinedDep.skipReason = "invalid-value";
}
deps.push(combinedDep);
} else if (resource.spec.ref?.digest) {
const dep = getDep(`${container}@${resource.spec.ref.digest}`, false, registryAliases);
deps.push(dep);
} else if (resource.spec.ref?.tag) {
const dep = getDep(`${container}:${resource.spec.ref.tag}`, false, registryAliases);
const refTagRange = extractOCIRefTagRange(docs ??= parseAllDocuments(content, { strict: false }), content, resource.metadata.name);
if (refTagRange) {
dep.replaceString = refTagRange.replaceString;
const newline = content.includes("\r\n") ? "\r\n" : "\n";
dep.autoReplaceStringTemplate = `${refTagRange.replaceString.replace(resource.spec.ref.tag, "{{newValue}}")}{{#if newDigest}}${newline}${refTagRange.indentation}digest: {{newDigest}}{{/if}}`;
} else {
logger.debug({
file: manifest.file,
name: resource.metadata.name
}, "Unable to locate tag node for replacement (may be YAML alias or alias reference), digest pinning will not be possible");
dep.replaceString = resource.spec.ref.tag;
dep.autoReplaceStringTemplate = "{{newValue}}";
}
deps.push(dep);
} else {
const dep = getDep(container, false, registryAliases);
dep.skipReason = "unversioned-reference";
deps.push(dep);
}
break;
}
case "Kustomization": for (const image of coerceArray(resource.spec.images)) {
const dep = extractImage(image, registryAliases);
if (dep) deps.push(dep);
}
}
return deps;
}
function extractPackageFile(content, packageFile, config) {
const manifest = readManifest(content, packageFile);
if (!manifest) return null;
const helmRepositories = collectHelmRepos([manifest]);
let deps = null;
switch (manifest.kind) {
case "system":
deps = resolveSystemManifest(manifest);
break;
case "resource":
deps = resolveResourceManifest(manifest, helmRepositories, config?.registryAliases, content);
break;
}
return deps?.length ? { deps } : null;
}
async function extractAllPackageFiles(config, packageFiles) {
const manifests = [];
const results = [];
for (const file of packageFiles) {
const content = await readLocalFile(file, "utf8");
if (content) {
const manifest = readManifest(content, file);
if (manifest) manifests.push(manifest);
}
}
const helmRepositories = collectHelmRepos(manifests);
for (const manifest of manifests) {
let deps = null;
switch (manifest.kind) {
case "system":
deps = resolveSystemManifest(manifest);
break;
case "resource":
deps = resolveResourceManifest(manifest, helmRepositories, config.registryAliases, manifest.content);
break;
}
if (deps?.length) results.push({
packageFile: manifest.file,
deps
});
}
return results.length ? results : null;
}
//#endregion
export { extractAllPackageFiles, extractPackageFile };
//# sourceMappingURL=extract.js.map