renovate/lib/modules/manager/flux/extract.ts

331 lines
9.8 KiB
TypeScript

import is from '@sindresorhus/is';
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { readLocalFile } from '../../../util/fs';
import { regEx } from '../../../util/regex';
import { parseYaml } from '../../../util/yaml';
import { BitbucketTagsDatasource } from '../../datasource/bitbucket-tags';
import { DockerDatasource } from '../../datasource/docker';
import { GitRefsDatasource } from '../../datasource/git-refs';
import { GitTagsDatasource } from '../../datasource/git-tags';
import { GithubReleasesDatasource } from '../../datasource/github-releases';
import { GithubTagsDatasource } from '../../datasource/github-tags';
import { GitlabTagsDatasource } from '../../datasource/gitlab-tags';
import { HelmDatasource } from '../../datasource/helm';
import { getDep } from '../dockerfile/extract';
import { isOCIRegistry, removeOCIPrefix } from '../helmv3/oci';
import { extractImage } from '../kustomize/extract';
import type {
ExtractConfig,
PackageDependency,
PackageFile,
PackageFileContent,
} from '../types';
import { isSystemManifest, systemManifestHeaderRegex } from './common';
import { FluxResource, type HelmRepository } from './schema';
import type {
FluxManagerData,
FluxManifest,
ResourceFluxManifest,
SystemFluxManifest,
} from './types';
function readManifest(
content: string,
packageFile: string,
): FluxManifest | null {
if (isSystemManifest(packageFile)) {
const versionMatch = regEx(systemManifestHeaderRegex).exec(content);
if (!versionMatch) {
return null;
}
return {
kind: 'system',
file: packageFile,
version: versionMatch[1],
components: versionMatch[2],
};
}
try {
const manifest: FluxManifest = {
kind: 'resource',
file: packageFile,
resources: parseYaml(content, {
json: true,
customSchema: FluxResource,
failureBehaviour: 'filter',
}),
};
return manifest;
} catch (err) {
logger.debug({ err, packageFile }, 'Failed to parse Flux manifest');
return null;
}
}
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: PackageDependency,
gitUrl: string,
): void {
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 (gitUrl.startsWith('https://')) {
dep.sourceUrl = gitUrl.replace(/\.git$/, '');
}
}
function resolveSystemManifest(
manifest: SystemFluxManifest,
): PackageDependency<FluxManagerData>[] {
return [
{
depName: 'fluxcd/flux2',
datasource: GithubReleasesDatasource.id,
currentValue: manifest.version,
managerData: {
components: manifest.components,
},
},
];
}
function resolveResourceManifest(
manifest: ResourceFluxManifest,
helmRepositories: HelmRepository[],
registryAliases: Record<string, string> | undefined,
): PackageDependency[] {
const deps: PackageDependency[] = [];
for (const resource of manifest.resources) {
switch (resource.kind) {
case 'HelmRelease': {
const dep: PackageDependency = {
depName: resource.spec.chart.spec.chart,
currentValue: resource.spec.chart.spec.version,
datasource: HelmDatasource.id,
};
const matchingRepositories = helmRepositories.filter(
(rep) =>
rep.kind === resource.spec.chart.spec.sourceRef?.kind &&
rep.metadata.name === resource.spec.chart.spec.sourceRef.name &&
rep.metadata.namespace ===
(resource.spec.chart.spec.sourceRef.namespace ??
resource.metadata?.namespace),
);
if (matchingRepositories.length) {
dep.registryUrls = matchingRepositories
.map((repo) => {
if (repo.spec.type === 'oci' || isOCIRegistry(repo.spec.url)) {
// Change datasource to Docker
dep.datasource = DockerDatasource.id;
// Ensure the URL is a valid OCI path
dep.packageName = getDep(
`${removeOCIPrefix(repo.spec.url)}/${
resource.spec.chart.spec.chart
}`,
false,
registryAliases,
).depName;
return null;
} else {
return repo.spec.url;
}
})
.filter(is.string);
// if registryUrls is empty, delete it from dep
if (!dep.registryUrls?.length) {
delete dep.registryUrls;
}
} else {
dep.skipReason = 'unknown-registry';
}
deps.push(dep);
break;
}
case 'GitRepository': {
const dep: PackageDependency = {
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 (gitUrl.startsWith('https://')) {
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);
let dep = getDep(container, false, registryAliases);
if (resource.spec.ref?.digest) {
dep = getDep(
`${container}@${resource.spec.ref.digest}`,
false,
registryAliases,
);
if (resource.spec.ref?.tag) {
logger.debug('A digest and tag was found, ignoring tag');
}
} else if (resource.spec.ref?.tag) {
dep = getDep(
`${container}:${resource.spec.ref.tag}`,
false,
registryAliases,
);
dep.autoReplaceStringTemplate =
'{{#if newValue}}{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}';
dep.replaceString = resource.spec.ref.tag;
} else {
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;
}
export function extractPackageFile(
content: string,
packageFile: string,
config?: ExtractConfig,
): PackageFileContent<FluxManagerData> | null {
const manifest = readManifest(content, packageFile);
if (!manifest) {
return null;
}
const helmRepositories: HelmRepository[] = [];
if (manifest.kind === 'resource') {
for (const resource of manifest.resources) {
if (resource.kind === 'HelmRepository') {
helmRepositories.push(resource);
}
}
}
let deps: PackageDependency[] | null = null;
switch (manifest.kind) {
case 'system':
deps = resolveSystemManifest(manifest);
break;
case 'resource': {
deps = resolveResourceManifest(
manifest,
helmRepositories,
config?.registryAliases,
);
break;
}
}
return deps?.length ? { deps } : null;
}
export async function extractAllPackageFiles(
config: ExtractConfig,
packageFiles: string[],
): Promise<PackageFile<FluxManagerData>[] | null> {
const manifests: FluxManifest[] = [];
const results: PackageFile<FluxManagerData>[] = [];
for (const file of packageFiles) {
const content = await readLocalFile(file, 'utf8');
// TODO #22198
const manifest = readManifest(content!, file);
if (manifest) {
manifests.push(manifest);
}
}
const helmRepositories: HelmRepository[] = [];
for (const manifest of manifests) {
if (manifest.kind === 'resource') {
for (const resource of manifest.resources) {
if (resource.kind === 'HelmRepository') {
helmRepositories.push(resource);
}
}
}
}
for (const manifest of manifests) {
let deps: PackageDependency[] | null = null;
switch (manifest.kind) {
case 'system':
deps = resolveSystemManifest(manifest);
break;
case 'resource': {
deps = resolveResourceManifest(
manifest,
helmRepositories,
config.registryAliases,
);
break;
}
}
if (deps?.length) {
results.push({
packageFile: manifest.file,
deps,
});
}
}
return results.length ? results : null;
}