Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(manager): add glasskube manager #30774

Merged
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
kosmoz marked this conversation as resolved.
Show resolved Hide resolved
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'],
},
],
},
]);
});
});
});
123 changes: 123 additions & 0 deletions lib/modules/manager/glasskube/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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 {
return (
repositories.find(
(it) =>
name === it.metadata.name ||
((name === null || name === '') && isDefaultRepository(it)),
) ?? null
);
kosmoz marked this conversation as resolved.
Show resolved Hide resolved
}

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 };
}
secustor marked this conversation as resolved.
Show resolved Hide resolved

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[];
};
Loading