mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-11 22:46:27 +00:00
feat(manager): add glasskube manager (#30774)
Signed-off-by: Jakob Steiner <jakob.steiner@glasskube.eu> Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com> Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
parent
42f597ada4
commit
0d20f17078
8 changed files with 368 additions and 0 deletions
|
@ -37,6 +37,7 @@ import * as gitSubmodules from './git-submodules';
|
||||||
import * as githubActions from './github-actions';
|
import * as githubActions from './github-actions';
|
||||||
import * as gitlabci from './gitlabci';
|
import * as gitlabci from './gitlabci';
|
||||||
import * as gitlabciInclude from './gitlabci-include';
|
import * as gitlabciInclude from './gitlabci-include';
|
||||||
|
import * as glasskube from './glasskube';
|
||||||
import * as gleam from './gleam';
|
import * as gleam from './gleam';
|
||||||
import * as gomod from './gomod';
|
import * as gomod from './gomod';
|
||||||
import * as gradle from './gradle';
|
import * as gradle from './gradle';
|
||||||
|
@ -138,6 +139,7 @@ api.set('git-submodules', gitSubmodules);
|
||||||
api.set('github-actions', githubActions);
|
api.set('github-actions', githubActions);
|
||||||
api.set('gitlabci', gitlabci);
|
api.set('gitlabci', gitlabci);
|
||||||
api.set('gitlabci-include', gitlabciInclude);
|
api.set('gitlabci-include', gitlabciInclude);
|
||||||
|
api.set('glasskube', glasskube);
|
||||||
api.set('gleam', gleam);
|
api.set('gleam', gleam);
|
||||||
api.set('gomod', gomod);
|
api.set('gomod', gomod);
|
||||||
api.set('gradle', gradle);
|
api.set('gradle', gradle);
|
||||||
|
|
|
@ -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
|
161
lib/modules/manager/glasskube/extract.spec.ts
Normal file
161
lib/modules/manager/glasskube/extract.spec.ts
Normal file
|
@ -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'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
126
lib/modules/manager/glasskube/extract.ts
Normal file
126
lib/modules/manager/glasskube/extract.ts
Normal file
|
@ -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<PackageFile[] | null> {
|
||||||
|
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;
|
||||||
|
}
|
9
lib/modules/manager/glasskube/index.ts
Normal file
9
lib/modules/manager/glasskube/index.ts
Normal file
|
@ -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];
|
5
lib/modules/manager/glasskube/readme.md
Normal file
5
lib/modules/manager/glasskube/readme.md
Normal file
|
@ -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.
|
31
lib/modules/manager/glasskube/schema.ts
Normal file
31
lib/modules/manager/glasskube/schema.ts
Normal file
|
@ -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<typeof Package>;
|
||||||
|
export type PackageRepository = z.infer<typeof PackageRepository>;
|
||||||
|
export type GlasskubeResource = z.infer<typeof GlasskubeResource>;
|
7
lib/modules/manager/glasskube/types.ts
Normal file
7
lib/modules/manager/glasskube/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Package, PackageRepository } from './schema';
|
||||||
|
|
||||||
|
export type GlasskubeResources = {
|
||||||
|
packageFile: string;
|
||||||
|
packages: Package[];
|
||||||
|
repositories: PackageRepository[];
|
||||||
|
};
|
Loading…
Reference in a new issue