Skip to content

Commit

Permalink
feat(manager): add glasskube manager (#30774)
Browse files Browse the repository at this point in the history
Signed-off-by: Jakob Steiner <[email protected]>
Co-authored-by: Sebastian Poxhofer <[email protected]>
Co-authored-by: Michael Kriese <[email protected]>
  • Loading branch information
3 people authored Aug 20, 2024
1 parent 42f597a commit 0d20f17
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/modules/manager/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions lib/modules/manager/glasskube/__fixtures__/package-and-repo.yaml
Original file line number Diff line number Diff line change
@@ -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 changes: 161 additions & 0 deletions lib/modules/manager/glasskube/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 126 additions & 0 deletions lib/modules/manager/glasskube/extract.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions lib/modules/manager/glasskube/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions lib/modules/manager/glasskube/readme.md
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions lib/modules/manager/glasskube/schema.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions lib/modules/manager/glasskube/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Package, PackageRepository } from './schema';

export type GlasskubeResources = {
packageFile: string;
packages: Package[];
repositories: PackageRepository[];
};

0 comments on commit 0d20f17

Please sign in to comment.