diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index db30f841bb4edc..664444223f467c 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -37,6 +37,7 @@ import * as gitSubmodules from './git-submodules'; import * as githubActions from './github-actions'; import * as gitlabci from './gitlabci'; import * as gitlabciInclude from './gitlabci-include'; +import * as glasskube from './glasskube'; import * as gleam from './gleam'; import * as gomod from './gomod'; import * as gradle from './gradle'; @@ -138,6 +139,7 @@ api.set('git-submodules', gitSubmodules); api.set('github-actions', githubActions); api.set('gitlabci', gitlabci); api.set('gitlabci-include', gitlabciInclude); +api.set('glasskube', glasskube); api.set('gleam', gleam); api.set('gomod', gomod); api.set('gradle', gradle); diff --git a/lib/modules/manager/glasskube/__fixtures__/package-and-repo.yaml b/lib/modules/manager/glasskube/__fixtures__/package-and-repo.yaml new file mode 100644 index 00000000000000..5b32bdf4765136 --- /dev/null +++ b/lib/modules/manager/glasskube/__fixtures__/package-and-repo.yaml @@ -0,0 +1,27 @@ +apiVersion: packages.glasskube.dev/v1alpha1 +kind: PackageRepository +metadata: + annotations: + packages.glasskube.dev/default-repository: "true" + name: glasskube +spec: + url: https://packages.dl.glasskube.dev/packages + +--- +apiVersion: packages.glasskube.dev/v1alpha1 +kind: PackageRepository +metadata: + name: local +spec: + url: http://localhost:9090/packages + +--- +apiVersion: packages.glasskube.dev/v1alpha1 +kind: ClusterPackage +metadata: + name: argo-cd +spec: + packageInfo: + name: argo-cd + repositoryName: glasskube + version: v2.11.7+1 diff --git a/lib/modules/manager/glasskube/extract.spec.ts b/lib/modules/manager/glasskube/extract.spec.ts new file mode 100644 index 00000000000000..7d8455de1acbe3 --- /dev/null +++ b/lib/modules/manager/glasskube/extract.spec.ts @@ -0,0 +1,161 @@ +import { codeBlock } from 'common-tags'; +import { Fixtures } from '../../../../test/fixtures'; +import { fs } from '../../../../test/util'; +import { GlobalConfig } from '../../../config/global'; +import type { RepoGlobalConfig } from '../../../config/types'; +import { GlasskubePackagesDatasource } from '../../datasource/glasskube-packages'; +import type { ExtractConfig } from '../types'; +import { extractAllPackageFiles, extractPackageFile } from './extract'; + +const config: ExtractConfig = {}; +const adminConfig: RepoGlobalConfig = { localDir: '' }; + +const packageWithRepoName = codeBlock` +apiVersion: packages.glasskube.dev/v1alpha1 +kind: ClusterPackage +metadata: + name: argo-cd +spec: + packageInfo: + name: argo-cd + repositoryName: glasskube + version: v2.11.7+1 +`; +const repository = codeBlock` +apiVersion: packages.glasskube.dev/v1alpha1 +kind: PackageRepository +metadata: + annotations: + packages.glasskube.dev/default-repository: "true" + name: glasskube +spec: + url: https://packages.dl.glasskube.dev/packages +`; + +jest.mock('../../../util/fs'); + +describe('modules/manager/glasskube/extract', () => { + beforeEach(() => { + GlobalConfig.set(adminConfig); + }); + + describe('extractPackageFile()', () => { + it('should extract version and registryUrl', () => { + const deps = extractPackageFile( + Fixtures.get('package-and-repo.yaml'), + 'package-and-repo.yaml', + ); + expect(deps).toEqual({ + deps: [ + { + depName: 'argo-cd', + currentValue: 'v2.11.7+1', + datasource: GlasskubePackagesDatasource.id, + registryUrls: ['https://packages.dl.glasskube.dev/packages'], + }, + ], + }); + }); + }); + + describe('extractAllPackageFiles()', () => { + it('should return null for empty packageFiles', async () => { + const deps = await extractAllPackageFiles(config, []); + expect(deps).toBeNull(); + }); + + it('should skip package with non-existing repo', async () => { + fs.readLocalFile.mockResolvedValueOnce(packageWithRepoName); + const deps = await extractAllPackageFiles(config, ['package.yaml']); + expect(deps).toEqual([ + { + packageFile: 'package.yaml', + deps: [ + { + depName: 'argo-cd', + currentValue: 'v2.11.7+1', + datasource: GlasskubePackagesDatasource.id, + skipReason: 'unknown-registry', + }, + ], + }, + ]); + }); + + it('should extract registryUrl from repo in other file', async () => { + fs.readLocalFile.mockResolvedValueOnce(packageWithRepoName); + fs.readLocalFile.mockResolvedValueOnce(repository); + const deps = await extractAllPackageFiles(config, [ + 'package.yaml', + 'repo.yaml', + ]); + expect(deps).toEqual([ + { + packageFile: 'package.yaml', + deps: [ + { + depName: 'argo-cd', + currentValue: 'v2.11.7+1', + datasource: GlasskubePackagesDatasource.id, + registryUrls: ['https://packages.dl.glasskube.dev/packages'], + }, + ], + }, + ]); + }); + + it('should extract registryUrl from default repo in other file', async () => { + fs.readLocalFile.mockResolvedValueOnce(codeBlock` + apiVersion: packages.glasskube.dev/v1alpha1 + kind: ClusterPackage + metadata: + name: argo-cd + spec: + packageInfo: + name: argo-cd + version: v2.11.7+1 + repositoryName: "" + `); + fs.readLocalFile.mockResolvedValueOnce(codeBlock` + apiVersion: packages.glasskube.dev/v1alpha1 + kind: ClusterPackage + metadata: + name: argo-cd + spec: + packageInfo: + name: argo-cd + version: v2.11.7+1 + `); + fs.readLocalFile.mockResolvedValueOnce(repository); + const deps = await extractAllPackageFiles(config, [ + 'package-with-empty-reponame.yaml', + 'package-with-missing-reponame.yaml', + 'repo.yaml', + ]); + expect(deps).toEqual([ + { + packageFile: 'package-with-empty-reponame.yaml', + deps: [ + { + depName: 'argo-cd', + currentValue: 'v2.11.7+1', + datasource: GlasskubePackagesDatasource.id, + registryUrls: ['https://packages.dl.glasskube.dev/packages'], + }, + ], + }, + { + packageFile: 'package-with-missing-reponame.yaml', + deps: [ + { + depName: 'argo-cd', + currentValue: 'v2.11.7+1', + datasource: GlasskubePackagesDatasource.id, + registryUrls: ['https://packages.dl.glasskube.dev/packages'], + }, + ], + }, + ]); + }); + }); +}); diff --git a/lib/modules/manager/glasskube/extract.ts b/lib/modules/manager/glasskube/extract.ts new file mode 100644 index 00000000000000..38f89ceaccfb6e --- /dev/null +++ b/lib/modules/manager/glasskube/extract.ts @@ -0,0 +1,126 @@ +import is from '@sindresorhus/is'; +import { readLocalFile } from '../../../util/fs'; +import { parseYaml } from '../../../util/yaml'; +import { GlasskubePackagesDatasource } from '../../datasource/glasskube-packages'; +import type { + ExtractConfig, + PackageDependency, + PackageFile, + PackageFileContent, +} from '../types'; +import { + GlasskubeResource, + type Package, + type PackageRepository, +} from './schema'; +import type { GlasskubeResources } from './types'; + +function parseResources( + content: string, + packageFile: string, +): GlasskubeResources { + const resources: GlasskubeResource[] = parseYaml(content, { + json: true, + customSchema: GlasskubeResource, + failureBehaviour: 'filter', + }); + + const packages: Package[] = []; + const repositories: PackageRepository[] = []; + + for (const resource of resources) { + if (resource.kind === 'ClusterPackage' || resource.kind === 'Package') { + packages.push(resource); + } else if (resource.kind === 'PackageRepository') { + repositories.push(resource); + } + } + + return { packageFile, repositories, packages }; +} + +function resolvePackageDependencies( + packages: Package[], + repositories: PackageRepository[], +): PackageDependency[] { + const deps: PackageDependency[] = []; + for (const pkg of packages) { + const dep: PackageDependency = { + depName: pkg.spec.packageInfo.name, + currentValue: pkg.spec.packageInfo.version, + datasource: GlasskubePackagesDatasource.id, + }; + + const repository = findRepository( + pkg.spec.packageInfo.repositoryName ?? null, + repositories, + ); + + if (repository === null) { + dep.skipReason = 'unknown-registry'; + } else { + dep.registryUrls = [repository.spec.url]; + } + + deps.push(dep); + } + return deps; +} + +function findRepository( + name: string | null, + repositories: PackageRepository[], +): PackageRepository | null { + for (const repository of repositories) { + if (name === repository.metadata.name) { + return repository; + } + if (is.falsy(name) && isDefaultRepository(repository)) { + return repository; + } + } + return null; +} + +function isDefaultRepository(repository: PackageRepository): boolean { + return ( + repository.metadata.annotations?.[ + 'packages.glasskube.dev/default-repository' + ] === 'true' + ); +} + +export function extractPackageFile( + content: string, + packageFile: string, + config?: ExtractConfig, +): PackageFileContent | null { + const { packages, repositories } = parseResources(content, packageFile); + const deps = resolvePackageDependencies(packages, repositories); + return { deps }; +} + +export async function extractAllPackageFiles( + config: ExtractConfig, + packageFiles: string[], +): Promise { + const allRepositories: PackageRepository[] = []; + const glasskubeResourceFiles: GlasskubeResources[] = []; + for (const packageFile of packageFiles) { + const content = await readLocalFile(packageFile, 'utf8'); + if (content !== null) { + const resources = parseResources(content, packageFile); + allRepositories.push(...resources.repositories); + glasskubeResourceFiles.push(resources); + } + } + + const result: PackageFile[] = []; + for (const file of glasskubeResourceFiles) { + const deps = resolvePackageDependencies(file.packages, allRepositories); + if (deps.length > 0) { + result.push({ packageFile: file.packageFile, deps }); + } + } + return result.length ? result : null; +} diff --git a/lib/modules/manager/glasskube/index.ts b/lib/modules/manager/glasskube/index.ts new file mode 100644 index 00000000000000..5004e61d3fc18e --- /dev/null +++ b/lib/modules/manager/glasskube/index.ts @@ -0,0 +1,9 @@ +import type { Category } from '../../../constants'; +import { GlasskubePackagesDatasource } from '../../datasource/glasskube-packages'; + +export { extractAllPackageFiles, extractPackageFile } from './extract'; +export const defaultConfig = { + fileMatch: [], +}; +export const categories: Category[] = ['kubernetes', 'cd']; +export const supportedDatasources = [GlasskubePackagesDatasource.id]; diff --git a/lib/modules/manager/glasskube/readme.md b/lib/modules/manager/glasskube/readme.md new file mode 100644 index 00000000000000..e115219868e0ec --- /dev/null +++ b/lib/modules/manager/glasskube/readme.md @@ -0,0 +1,5 @@ +Extract version data from Packages/ClusterPackages and repository data from PackageRepositories. + +To use the `glasskube` manager you must set your own `fileMatch` pattern. +The `glasskube` manager has no default `fileMatch` pattern, because there is no common filename or directory name convention for Glasskube YAML files. +By setting your own `fileMatch` Renovate avoids having to check each `*.yaml` file in a repository for a Glasskube definition. diff --git a/lib/modules/manager/glasskube/schema.ts b/lib/modules/manager/glasskube/schema.ts new file mode 100644 index 00000000000000..20d32fd5998c2a --- /dev/null +++ b/lib/modules/manager/glasskube/schema.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +export const Package = z.object({ + apiVersion: z.string().startsWith('packages.glasskube.dev/'), + kind: z.literal('Package').or(z.literal('ClusterPackage')), + spec: z.object({ + packageInfo: z.object({ + name: z.string(), + version: z.string(), + repositoryName: z.string().optional(), + }), + }), +}); + +export const PackageRepository = z.object({ + apiVersion: z.string().startsWith('packages.glasskube.dev/'), + kind: z.literal('PackageRepository'), + metadata: z.object({ + name: z.string(), + annotations: z.record(z.string(), z.string()).optional(), + }), + spec: z.object({ + url: z.string(), + }), +}); + +export const GlasskubeResource = Package.or(PackageRepository); + +export type Package = z.infer; +export type PackageRepository = z.infer; +export type GlasskubeResource = z.infer; diff --git a/lib/modules/manager/glasskube/types.ts b/lib/modules/manager/glasskube/types.ts new file mode 100644 index 00000000000000..304dcd6174fc89 --- /dev/null +++ b/lib/modules/manager/glasskube/types.ts @@ -0,0 +1,7 @@ +import type { Package, PackageRepository } from './schema'; + +export type GlasskubeResources = { + packageFile: string; + packages: Package[]; + repositories: PackageRepository[]; +};