diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 2e7ccf55d2fa6e9a5532a5ab598f7ee2865fc656..466ce2f0f617642a18d90fcafdc0794087b60bdc 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -86,6 +86,7 @@ import * as runtimeVersion from './runtime-version'; import * as sbt from './sbt'; import * as scalafmt from './scalafmt'; import * as setupCfg from './setup-cfg'; +import * as sveltos from './sveltos'; import * as swift from './swift'; import * as tekton from './tekton'; import * as terraform from './terraform'; @@ -190,6 +191,7 @@ api.set('runtime-version', runtimeVersion); api.set('sbt', sbt); api.set('scalafmt', scalafmt); api.set('setup-cfg', setupCfg); +api.set('sveltos', sveltos); api.set('swift', swift); api.set('tekton', tekton); api.set('terraform', terraform); diff --git a/lib/modules/manager/sveltos/extract.spec.ts b/lib/modules/manager/sveltos/extract.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5655531f43f77418e44a44d05e9d902fc707d7e8 --- /dev/null +++ b/lib/modules/manager/sveltos/extract.spec.ts @@ -0,0 +1,522 @@ +import { codeBlock } from 'common-tags'; +import { extractDefinition } from './extract'; +import type { ProfileDefinition } from './schema'; +import { extractPackageFile } from '.'; + +const validProfile = codeBlock` +--- +apiVersion: config.projectsveltos.io/v1beta1 +kind: Profile +metadata: + name: baseline +spec: + helmCharts: + - repositoryURL: https://prometheus-community.github.io/helm-charts + repositoryName: prometheus-community + chartName: prometheus-community/prometheus + chartVersion: "23.4.0" + - repositoryURL: https://kyverno.github.io/kyverno/ + repositoryName: kyverno + chartName: kyverno/kyverno + chartVersion: "v3.2.5" +--- +apiVersion: config.projectsveltos.io/v1beta1 +kind: Profile +metadata: + name: kyverno +spec: + helmCharts: + - repositoryURL: https://kyverno.github.io/kyverno/ + repositoryName: kyverno + chartName: kyverno/kyverno-policies + chartVersion: v3.2.0 + releaseName: kyverno-latest + releaseNamespace: kyverno + helmChartAction: Install + values: | + admissionController: + replicas: 1 +--- +apiVersion: config.projectsveltos.io/v1beta1 +kind: Profile +metadata: + name: vault +spec: + syncMode: Continuous + helmCharts: + - repositoryURL: oci://registry-1.docker.io/bitnamicharts/vault + repositoryName: oci-vault + chartName: oci://registry-1.docker.io/bitnamicharts/vault + chartVersion: 0.7.2 + - repositoryURL: oci://custom-registry:443/charts/vault-sidecar + repositoryName: oci-custom-vault + chartName: oci://custom-registry:443/charts/vault-sidecar + chartVersion: 0.5.0 +`; +const validClusterProfile = codeBlock` +--- +apiVersion: config.projectsveltos.io/v1beta1 +kind: ClusterProfile +metadata: + name: baseline +spec: + helmCharts: + - repositoryURL: https://prometheus-community.github.io/helm-charts + repositoryName: prometheus-community + chartName: prometheus-community/prometheus + chartVersion: "23.4.0" + - repositoryURL: https://kyverno.github.io/kyverno/ + repositoryName: kyverno + chartName: kyverno/kyverno + chartVersion: "v3.2.5" +--- +apiVersion: config.projectsveltos.io/v1beta1 +kind: ClusterProfile +metadata: + name: kyverno +spec: + helmCharts: + - repositoryURL: https://kyverno.github.io/kyverno/ + repositoryName: kyverno + chartName: kyverno/kyverno-policies + chartVersion: v3.2.0 + releaseName: kyverno-latest + releaseNamespace: kyverno + helmChartAction: Install + values: | + admissionController: + replicas: 1 +--- +apiVersion: config.projectsveltos.io/v1beta1 +kind: ClusterProfile +metadata: + name: vault +spec: + syncMode: Continuous + helmCharts: + - repositoryURL: oci://registry-1.docker.io/bitnamicharts/vault + repositoryName: oci-vault + chartName: oci://registry-1.docker.io/bitnamicharts/vault + chartVersion: 0.7.2 + - repositoryURL: oci://custom-registry:443/charts/vault-sidecar + repositoryName: oci-custom-vault + chartName: oci://custom-registry:443/charts/vault-sidecar + chartVersion: 0.5.0 +`; +const validClusterProfileOCI = codeBlock` +--- +apiVersion: config.projectsveltos.io/v1beta1 +kind: ClusterProfile +metadata: + name: vault +spec: + syncMode: Continuous + helmCharts: + - repositoryURL: oci://registry-1.docker.io/bitnamicharts/vault + repositoryName: oci-vault + chartName: oci://registry-1.docker.io/bitnamicharts/vault + chartVersion: 0.7.2 +`; +const validEventTrigger = codeBlock` +--- +apiVersion: lib.projectsveltos.io/v1beta1 +kind: EventTrigger +metadata: + name: baseline +spec: + helmCharts: + - repositoryURL: https://prometheus-community.github.io/helm-charts + repositoryName: prometheus-community + chartName: prometheus-community/prometheus + chartVersion: "23.4.0" + - repositoryURL: https://kyverno.github.io/kyverno/ + repositoryName: kyverno + chartName: kyverno/kyverno + chartVersion: "v3.2.5" +--- +apiVersion: lib.projectsveltos.io/v1beta1 +kind: EventTrigger +metadata: + name: kyverno +spec: + helmCharts: + - repositoryURL: https://kyverno.github.io/kyverno/ + repositoryName: kyverno + chartName: kyverno/kyverno-policies + chartVersion: v3.2.0 + releaseName: kyverno-latest + releaseNamespace: kyverno + helmChartAction: Install + values: | + admissionController: + replicas: 1 +--- +apiVersion: lib.projectsveltos.io/v1beta1 +kind: EventTrigger +metadata: + name: vault +spec: + syncMode: Continuous + helmCharts: + - repositoryURL: oci://registry-1.docker.io/bitnamicharts/vault + repositoryName: oci-vault + chartName: oci://registry-1.docker.io/bitnamicharts/vault + chartVersion: 0.7.2 + - repositoryURL: oci://custom-registry:443/charts/vault-sidecar + repositoryName: oci-custom-vault + chartName: oci://custom-registry:443/charts/vault-sidecar + chartVersion: 0.5.0 +`; +const malformedProfiles = codeBlock` +--- +# malformed eventtrigger as the source is null +apiVersion: lib.projectsveltos.io/v1beta1 +kind: EventTrigger +spec: + helmCharts: [] +--- +# malformed eventtrigger as the source is empty +apiVersion: lib.projectsveltos.io/v1beta1 +kind: EventTrigger +spec: + helmCharts: null +--- +# malformed clusterprofile as the sources array is empty +apiVersion: config.projectsveltos.io/v1beta1 +kind: ClusterProfile +spec: + helmCharts: [] +--- +# malformed clusterprofile as the source is null +apiVersion: config.projectsveltos.io/v1beta1 +kind: ClusterProfile +spec: + helmCharts: null +--- +# malformed profile as the sources array is empty +apiVersion: config.projectsveltos.io/v1beta1 +kind: Profile +spec: + helmCharts: [] +--- +# malformed profile as the source is null +apiVersion: config.projectsveltos.io/v1beta1 +kind: Profile +spec: + helmCharts: null +`; +const randomManifest = codeBlock` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 +`; + +describe('modules/manager/sveltos/extract', () => { + describe('extractDefinition()', () => { + it('returns an empty array when parsing fails', () => { + const invalidDefinition = {}; + const result = extractDefinition(invalidDefinition as ProfileDefinition); + expect(result).toEqual([]); + }); + + it('returns null if extractDefinition returns an empty array', () => { + const validYAML = codeBlock` + apiVersion: "config.projectsveltos.io/v1beta1" + kind: ClusterProfile + metadata: + name: empty-profile + `; + + const result = extractPackageFile(validYAML, 'valid-yaml.yml'); + expect(result).toBeNull(); + }); + }); + + describe('extractPackageFile()', () => { + it('returns null for empty', () => { + expect(extractPackageFile('nothing here', 'sveltos.yml')).toBeNull(); + }); + + it('returns null for invalid', () => { + expect( + extractPackageFile(`${malformedProfiles}\n123`, 'sveltos.yml'), + ).toBeNull(); + }); + + it('return null for Kubernetes manifest', () => { + const result = extractPackageFile(randomManifest, 'sveltos.yml'); + expect(result).toBeNull(); + }); + + it('return null if deps array would be empty', () => { + const result = extractPackageFile(malformedProfiles, 'applications.yml'); + expect(result).toBeNull(); + }); + + it('return null if YAML is invalid', () => { + const result = extractPackageFile( + codeBlock` + ---- + apiVersion: "config.projectsveltos.io/v1beta1" + kind ClusterProfile + metadata: + name: prometheus + `, + 'invalid-yaml.yml', + ); + expect(result).toBeNull(); + }); + + it('return result for double quoted projectsveltos.io apiVersion reference', () => { + const result = extractPackageFile( + codeBlock` + apiVersion: "config.projectsveltos.io/v1beta1" + kind: ClusterProfile + metadata: + name: prometheus + spec: + helmCharts: + - repositoryURL: https://prometheus-community.github.io/helm-charts + repositoryName: prometheus-community + chartName: prometheus-community/prometheus + chartVersion: "23.4.0" + `, + 'sveltos.yml', + ); + expect(result).toMatchObject({ + deps: [ + { + currentValue: '23.4.0', + datasource: 'helm', + depType: 'ClusterProfile', + depName: 'prometheus-community/prometheus', + packageName: 'prometheus', + registryUrls: [ + 'https://prometheus-community.github.io/helm-charts', + ], + }, + ], + }); + }); + + it('return result for single quoted projectsveltos.io apiVersion reference', () => { + const result = extractPackageFile( + codeBlock` + apiVersion: 'config.projectsveltos.io/v1beta1' + kind: ClusterProfile + metadata: + name: prometheus + spec: + helmCharts: + - repositoryURL: https://prometheus-community.github.io/helm-charts + repositoryName: prometheus-community + chartName: prometheus-community/prometheus + chartVersion: "23.4.0" + `, + 'applications.yml', + ); + expect(result).toMatchObject({ + deps: [ + { + currentValue: '23.4.0', + datasource: 'helm', + depType: 'ClusterProfile', + depName: 'prometheus-community/prometheus', + packageName: 'prometheus', + registryUrls: [ + 'https://prometheus-community.github.io/helm-charts', + ], + }, + ], + }); + }); + + it('supports profiles', () => { + const result = extractPackageFile(validProfile, 'profiles.yml'); + expect(result).toEqual({ + deps: [ + { + currentValue: '23.4.0', + datasource: 'helm', + depType: 'Profile', + depName: 'prometheus-community/prometheus', + packageName: 'prometheus', + registryUrls: [ + 'https://prometheus-community.github.io/helm-charts', + ], + }, + { + currentValue: 'v3.2.5', + datasource: 'helm', + depType: 'Profile', + depName: 'kyverno/kyverno', + packageName: 'kyverno', + registryUrls: ['https://kyverno.github.io/kyverno/'], + }, + { + currentValue: 'v3.2.0', + datasource: 'helm', + depType: 'Profile', + depName: 'kyverno/kyverno-policies', + packageName: 'kyverno-policies', + registryUrls: ['https://kyverno.github.io/kyverno/'], + }, + { + currentValue: '0.7.2', + datasource: 'docker', + depType: 'Profile', + depName: 'oci://registry-1.docker.io/bitnamicharts/vault', + packageName: 'registry-1.docker.io/bitnamicharts/vault', + }, + { + currentValue: '0.5.0', + datasource: 'docker', + depType: 'Profile', + depName: 'oci://custom-registry:443/charts/vault-sidecar', + packageName: 'custom-registry:443/charts/vault-sidecar', + }, + ], + }); + }); + + it('supports clusterprofiles', () => { + const result = extractPackageFile( + validClusterProfile, + 'clusterprofiles.yml', + ); + expect(result).toEqual({ + deps: [ + { + currentValue: '23.4.0', + datasource: 'helm', + depType: 'ClusterProfile', + depName: 'prometheus-community/prometheus', + packageName: 'prometheus', + registryUrls: [ + 'https://prometheus-community.github.io/helm-charts', + ], + }, + { + currentValue: 'v3.2.5', + datasource: 'helm', + depType: 'ClusterProfile', + depName: 'kyverno/kyverno', + packageName: 'kyverno', + registryUrls: ['https://kyverno.github.io/kyverno/'], + }, + { + currentValue: 'v3.2.0', + datasource: 'helm', + depType: 'ClusterProfile', + depName: 'kyverno/kyverno-policies', + packageName: 'kyverno-policies', + registryUrls: ['https://kyverno.github.io/kyverno/'], + }, + { + currentValue: '0.7.2', + datasource: 'docker', + depType: 'ClusterProfile', + depName: 'oci://registry-1.docker.io/bitnamicharts/vault', + packageName: 'registry-1.docker.io/bitnamicharts/vault', + }, + { + currentValue: '0.5.0', + datasource: 'docker', + depType: 'ClusterProfile', + depName: 'oci://custom-registry:443/charts/vault-sidecar', + packageName: 'custom-registry:443/charts/vault-sidecar', + }, + ], + }); + }); + + it('considers registryAliases', () => { + const result = extractPackageFile( + validClusterProfileOCI, + 'clusterprofiles.yml', + { + registryAliases: { + 'registry-1.docker.io': 'docker.proxy.test/some/path', + }, + }, + ); + expect(result).toEqual({ + deps: [ + { + currentValue: '0.7.2', + depName: 'oci://registry-1.docker.io/bitnamicharts/vault', + packageName: 'docker.proxy.test/some/path/bitnamicharts/vault', + datasource: 'docker', + depType: 'ClusterProfile', + }, + ], + }); + }); + + it('supports eventtriggers', () => { + const result = extractPackageFile(validEventTrigger, 'eventtriggers.yml'); + expect(result).toEqual({ + deps: [ + { + currentValue: '23.4.0', + datasource: 'helm', + depName: 'prometheus-community/prometheus', + packageName: 'prometheus', + depType: 'EventTrigger', + registryUrls: [ + 'https://prometheus-community.github.io/helm-charts', + ], + }, + { + currentValue: 'v3.2.5', + datasource: 'helm', + depName: 'kyverno/kyverno', + packageName: 'kyverno', + depType: 'EventTrigger', + registryUrls: ['https://kyverno.github.io/kyverno/'], + }, + { + currentValue: 'v3.2.0', + datasource: 'helm', + depName: 'kyverno/kyverno-policies', + packageName: 'kyverno-policies', + depType: 'EventTrigger', + registryUrls: ['https://kyverno.github.io/kyverno/'], + }, + { + currentValue: '0.7.2', + datasource: 'docker', + depType: 'EventTrigger', + depName: 'oci://registry-1.docker.io/bitnamicharts/vault', + packageName: 'registry-1.docker.io/bitnamicharts/vault', + }, + { + currentValue: '0.5.0', + datasource: 'docker', + depType: 'EventTrigger', + depName: 'oci://custom-registry:443/charts/vault-sidecar', + packageName: 'custom-registry:443/charts/vault-sidecar', + }, + ], + }); + }); + }); +}); diff --git a/lib/modules/manager/sveltos/extract.ts b/lib/modules/manager/sveltos/extract.ts new file mode 100644 index 0000000000000000000000000000000000000000..35d39cceef524502f9ff3b1fa5bbcf04d2ad077b --- /dev/null +++ b/lib/modules/manager/sveltos/extract.ts @@ -0,0 +1,87 @@ +import { coerceArray } from '../../../util/array'; +import { trimTrailingSlash } from '../../../util/url'; +import { parseYaml } from '../../../util/yaml'; +import { DockerDatasource } from '../../datasource/docker'; +import { HelmDatasource } from '../../datasource/helm'; +import { getDep } from '../dockerfile/extract'; +import { isOCIRegistry, removeOCIPrefix } from '../helmv3/oci'; +import type { + ExtractConfig, + PackageDependency, + PackageFileContent, +} from '../types'; +import { ProfileDefinition, type SveltosHelmSource } from './schema'; +import { removeRepositoryName } from './util'; + +export function extractPackageFile( + content: string, + packageFile: string, + config?: ExtractConfig, +): PackageFileContent | null { + const definitions = parseYaml(content, { + customSchema: ProfileDefinition, + failureBehaviour: 'filter', + }); + + const deps: PackageDependency[] = []; + + for (const definition of definitions) { + const extractedDeps = extractDefinition(definition, config); + deps.push(...extractedDeps); + } + + return deps.length ? { deps } : null; +} + +export function extractDefinition( + definition: ProfileDefinition, + config?: ExtractConfig, +): PackageDependency[] { + return processAppSpec(definition, config); +} + +function processHelmCharts( + source: SveltosHelmSource, + registryAliases: Record<string, string> | undefined, +): PackageDependency | null { + const dep: PackageDependency = { + depName: source.chartName, + currentValue: source.chartVersion, + datasource: HelmDatasource.id, + }; + + if (isOCIRegistry(source.repositoryURL)) { + const image = trimTrailingSlash(removeOCIPrefix(source.repositoryURL)); + + dep.datasource = DockerDatasource.id; + dep.packageName = getDep(image, false, registryAliases).depName; + } else { + dep.packageName = removeRepositoryName( + source.repositoryName, + source.chartName, + ); + dep.registryUrls = [source.repositoryURL]; + dep.datasource = HelmDatasource.id; + } + + return dep; +} + +function processAppSpec( + definition: ProfileDefinition, + config?: ExtractConfig, +): PackageDependency[] { + const deps: PackageDependency[] = []; + + const depType = definition.kind; + + for (const source of coerceArray(definition.spec?.helmCharts)) { + const dep = processHelmCharts(source, config?.registryAliases); + if (dep) { + dep.depType = depType; + deps.push(dep); + } + } + + return deps; +} diff --git a/lib/modules/manager/sveltos/index.ts b/lib/modules/manager/sveltos/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..af87c6a62959bda6934aa73ebc995fc2d1516209 --- /dev/null +++ b/lib/modules/manager/sveltos/index.ts @@ -0,0 +1,16 @@ +import type { Category } from '../../../constants'; +import { DockerDatasource } from '../../datasource/docker'; +import { HelmDatasource } from '../../datasource/helm'; + +export { extractPackageFile } from './extract'; + +export const displayName = 'Sveltos'; +export const url = 'https://projectsveltos.github.io/sveltos/'; + +export const defaultConfig = { + fileMatch: [], +}; + +export const categories: Category[] = ['kubernetes', 'cd']; + +export const supportedDatasources = [DockerDatasource.id, HelmDatasource.id]; diff --git a/lib/modules/manager/sveltos/readme.md b/lib/modules/manager/sveltos/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..96c416a383e3547d1d891c7d9b9b79c8b1fcc024 --- /dev/null +++ b/lib/modules/manager/sveltos/readme.md @@ -0,0 +1,45 @@ +Renovate uses the [Sveltos](https://projectsveltos.github.io/sveltos/) manager to update the dependencies in Helm-Charts for Sveltos resources. + +Learn about Sveltos Helm-Charts by reading the [Sveltos documentation](https://projectsveltos.github.io/sveltos/addons/helm_charts/). + +### You must set a `fileMatch` pattern + +The `sveltos` manager has no default `fileMatch` pattern. +This is because there is are no common filename or directory name conventions for Sveltos YAML files. +You must set your own `fileMatch` rules, so Renovate knows which `*.yaml` files are Sveltos definitions. + +#### `fileMatch` pattern examples + +```json title="If most .yaml files in your repository are for Sveltos" +{ + "sveltos": { + "fileMatch": ["\\.yaml$"] + } +} +``` + +```json title="Sveltos YAML files are in a sveltos/ directory" +{ + "sveltos": { + "fileMatch": ["sveltos/.+\\.yaml$"] + } +} +``` + +```json title="One Sveltos file in a directory" +{ + "sveltos": { + "fileMatch": ["^config/sveltos\\.yaml$"] + } +} +``` + +### Disabling parts of the sveltos manager + +You can use these `depTypes` for fine-grained control, for example to disable parts of the Sveltos manager. + +| Resource | `depType` | +| :--------------------------------------------------------------------------------------------------- | :--------------- | +| [Cluster Profiles](https://projectsveltos.github.io/sveltos/addons/clusterprofile/) | `ClusterProfile` | +| [Profiles](https://projectsveltos.github.io/sveltos/addons/profile/) | `Profile` | +| [EventTrigger](https://projectsveltos.github.io/sveltos/events/addon_event_deployment/#eventtrigger) | `EventTrigger` | diff --git a/lib/modules/manager/sveltos/schema.ts b/lib/modules/manager/sveltos/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2041990fc2ae15ac1367fe8efef9848c3a06fc3 --- /dev/null +++ b/lib/modules/manager/sveltos/schema.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { LooseArray } from '../../../util/schema-utils'; +import { KubernetesResource } from '../flux/schema'; + +export const SveltosHelmSource = z.object({ + repositoryURL: z.string(), + repositoryName: z.string(), + chartName: z.string(), + chartVersion: z.string(), +}); + +export type SveltosHelmSource = z.infer<typeof SveltosHelmSource>; + +export const SveltosHelmSpec = z.object({ + helmCharts: LooseArray(SveltosHelmSource).optional(), +}); +export type SveltosHelmSpec = z.infer<typeof SveltosHelmSpec>; + +export const ClusterProfile = KubernetesResource.extend({ + apiVersion: z.string().startsWith('config.projectsveltos.io/'), + kind: z.literal('ClusterProfile'), + spec: SveltosHelmSpec, +}); + +export const Profile = KubernetesResource.extend({ + apiVersion: z.string().startsWith('config.projectsveltos.io/'), + kind: z.literal('Profile'), + spec: SveltosHelmSpec, +}); + +export const EventTrigger = KubernetesResource.extend({ + apiVersion: z.string().startsWith('lib.projectsveltos.io/'), + kind: z.literal('EventTrigger'), + spec: SveltosHelmSpec, +}); + +// Create a union schema for ProfileDefinition +export const ProfileDefinition = z.union([ + Profile, + ClusterProfile, + EventTrigger, +]); +export type ProfileDefinition = z.infer<typeof ProfileDefinition>; diff --git a/lib/modules/manager/sveltos/util.ts b/lib/modules/manager/sveltos/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a5765853c18758babeb00851e4884b984a37651 --- /dev/null +++ b/lib/modules/manager/sveltos/util.ts @@ -0,0 +1,13 @@ +import { regEx } from '../../../util/regex'; + +export function removeRepositoryName( + repositoryName: string, + chartName: string, +): string { + const repoNameWithSlash = regEx(`^${repositoryName}/`, undefined, false); + let modifiedChartName = chartName.replace(repoNameWithSlash, ''); + + modifiedChartName = modifiedChartName.replace(/\/+$/, ''); + + return modifiedChartName; +}