From bbde8073e8e30b3f266e3f73f2b937bf5236469a Mon Sep 17 00:00:00 2001 From: Jakob Steiner Date: Wed, 26 Jun 2024 15:33:40 +0200 Subject: [PATCH] feat(datasource): add glasskube packages datasource (#29430) Signed-off-by: Jakob Steiner Co-authored-by: Sebastian Poxhofer Co-authored-by: Michael Kriese --- lib/modules/datasource/api.ts | 2 + .../__fixtures__/package.yaml | 18 +++ .../__fixtures__/package_no_references.yaml | 11 ++ .../__fixtures__/versions.yaml | 4 + .../glasskube-packages/index.spec.ts | 153 ++++++++++++++++++ .../datasource/glasskube-packages/index.ts | 76 +++++++++ .../datasource/glasskube-packages/schema.ts | 23 +++ lib/util/cache/package/types.ts | 1 + 8 files changed, 288 insertions(+) create mode 100644 lib/modules/datasource/glasskube-packages/__fixtures__/package.yaml create mode 100644 lib/modules/datasource/glasskube-packages/__fixtures__/package_no_references.yaml create mode 100644 lib/modules/datasource/glasskube-packages/__fixtures__/versions.yaml create mode 100644 lib/modules/datasource/glasskube-packages/index.spec.ts create mode 100644 lib/modules/datasource/glasskube-packages/index.ts create mode 100644 lib/modules/datasource/glasskube-packages/schema.ts diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index ff31d16c158362..eff7da102edd81 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -32,6 +32,7 @@ import { GithubTagsDatasource } from './github-tags'; import { GitlabPackagesDatasource } from './gitlab-packages'; import { GitlabReleasesDatasource } from './gitlab-releases'; import { GitlabTagsDatasource } from './gitlab-tags'; +import { GlasskubePackagesDatasource } from './glasskube-packages'; import { GoDatasource } from './go'; import { GolangVersionDatasource } from './golang-version'; import { GradleVersionDatasource } from './gradle-version'; @@ -102,6 +103,7 @@ api.set(GithubTagsDatasource.id, new GithubTagsDatasource()); api.set(GitlabPackagesDatasource.id, new GitlabPackagesDatasource()); api.set(GitlabReleasesDatasource.id, new GitlabReleasesDatasource()); api.set(GitlabTagsDatasource.id, new GitlabTagsDatasource()); +api.set(GlasskubePackagesDatasource.id, new GlasskubePackagesDatasource()); api.set(GoDatasource.id, new GoDatasource()); api.set(GolangVersionDatasource.id, new GolangVersionDatasource()); api.set(GradleVersionDatasource.id, new GradleVersionDatasource()); diff --git a/lib/modules/datasource/glasskube-packages/__fixtures__/package.yaml b/lib/modules/datasource/glasskube-packages/__fixtures__/package.yaml new file mode 100644 index 00000000000000..f3a2c5e887dd4e --- /dev/null +++ b/lib/modules/datasource/glasskube-packages/__fixtures__/package.yaml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://glasskube.dev/schemas/v1/package-manifest.json +name: cloudnative-pg +iconUrl: https://github.com/glasskube/glasskube/assets/16959694/99f3192a-587f-4eb9-884d-62800c022992 +shortDescription: A comprehensive platform designed to seamlessly manage PostgreSQL databases +longDescription: | + **CloudNativePG** is an open source operator designed to manage PostgreSQL workloads on any supported Kubernetes + cluster running in private, public, hybrid, or multi-cloud environments. CloudNativePG adheres to DevOps principles + and concepts such as declarative configuration and immutable infrastructure. +references: + - label: GitHub + url: https://github.com/cloudnative-pg/cloudnative-pg + - label: Website + url: https://cloudnative-pg.io/ + - label: Documentation + url: https://cloudnative-pg.io/documentation/1.23/ +defaultNamespace: cnpg-system +manifests: + - url: https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.23.1/cnpg-1.23.1.yaml diff --git a/lib/modules/datasource/glasskube-packages/__fixtures__/package_no_references.yaml b/lib/modules/datasource/glasskube-packages/__fixtures__/package_no_references.yaml new file mode 100644 index 00000000000000..6ca871e6c6eb53 --- /dev/null +++ b/lib/modules/datasource/glasskube-packages/__fixtures__/package_no_references.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=https://glasskube.dev/schemas/v1/package-manifest.json +name: cloudnative-pg +iconUrl: https://github.com/glasskube/glasskube/assets/16959694/99f3192a-587f-4eb9-884d-62800c022992 +shortDescription: A comprehensive platform designed to seamlessly manage PostgreSQL databases +longDescription: | + **CloudNativePG** is an open source operator designed to manage PostgreSQL workloads on any supported Kubernetes + cluster running in private, public, hybrid, or multi-cloud environments. CloudNativePG adheres to DevOps principles + and concepts such as declarative configuration and immutable infrastructure. +defaultNamespace: cnpg-system +manifests: + - url: https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.23.1/cnpg-1.23.1.yaml diff --git a/lib/modules/datasource/glasskube-packages/__fixtures__/versions.yaml b/lib/modules/datasource/glasskube-packages/__fixtures__/versions.yaml new file mode 100644 index 00000000000000..23416688f31514 --- /dev/null +++ b/lib/modules/datasource/glasskube-packages/__fixtures__/versions.yaml @@ -0,0 +1,4 @@ +latestVersion: v1.23.1+1 +versions: + - version: v1.22.0+1 + - version: v1.23.1+1 diff --git a/lib/modules/datasource/glasskube-packages/index.spec.ts b/lib/modules/datasource/glasskube-packages/index.spec.ts new file mode 100644 index 00000000000000..a136cd13b4bc66 --- /dev/null +++ b/lib/modules/datasource/glasskube-packages/index.spec.ts @@ -0,0 +1,153 @@ +import { getPkgReleases } from '..'; +import { Fixtures } from '../../../../test/fixtures'; +import * as httpMock from '../../../../test/http-mock'; +import { GlasskubePackagesDatasource } from '.'; + +describe('modules/datasource/glasskube-packages/index', () => { + const customRegistryUrl = 'https://packages.test.example/packages'; + const customVersionsUrl = new URL( + `${customRegistryUrl}/cloudnative-pg/versions.yaml`, + ); + const defaultVersionUrl = new URL( + `${GlasskubePackagesDatasource.defaultRegistryUrl}/cloudnative-pg/versions.yaml`, + ); + const versionsYaml = Fixtures.get('versions.yaml'); + const customPackageManifestUrl = new URL( + `${customRegistryUrl}/cloudnative-pg/v1.23.1+1/package.yaml`, + ); + const defaultPackageManifestUrl = new URL( + `${GlasskubePackagesDatasource.defaultRegistryUrl}/cloudnative-pg/v1.23.1+1/package.yaml`, + ); + const packageManifestYaml = Fixtures.get('package.yaml'); + const packageManifestNoReferencesYaml = Fixtures.get( + 'package_no_references.yaml', + ); + + it('should handle error response on versions request', async () => { + httpMock + .scope(customVersionsUrl.origin) + .get(customVersionsUrl.pathname) + .reply(500, 'internal server error'); + await expect( + getPkgReleases({ + datasource: GlasskubePackagesDatasource.id, + packageName: 'cloudnative-pg', + registryUrls: [customRegistryUrl], + }), + ).rejects.toThrow(); + }); + + it('should handle empty response on versions request', async () => { + httpMock + .scope(customVersionsUrl.origin) + .get(customVersionsUrl.pathname) + .reply(200); + const response = await getPkgReleases({ + datasource: GlasskubePackagesDatasource.id, + packageName: 'cloudnative-pg', + registryUrls: [customRegistryUrl], + }); + expect(response).toBeNull(); + }); + + it('should handle error response on manifest request', async () => { + httpMock + .scope(customVersionsUrl.origin) + .get(customVersionsUrl.pathname) + .reply(200, versionsYaml); + httpMock + .scope(customPackageManifestUrl.origin) + .get(customPackageManifestUrl.pathname) + .reply(500, 'internal server error'); + await expect( + getPkgReleases({ + datasource: GlasskubePackagesDatasource.id, + packageName: 'cloudnative-pg', + registryUrls: [customRegistryUrl], + }), + ).rejects.toThrow(); + }); + + it('should handle empty response on manifest request', async () => { + httpMock + .scope(customVersionsUrl.origin) + .get(customVersionsUrl.pathname) + .reply(200, versionsYaml); + httpMock + .scope(customPackageManifestUrl.origin) + .get(customPackageManifestUrl.pathname) + .reply(200); + const response = await getPkgReleases({ + datasource: GlasskubePackagesDatasource.id, + packageName: 'cloudnative-pg', + registryUrls: [customRegistryUrl], + }); + expect(response).toBeNull(); + }); + + it('should handle package manifest without references', async () => { + httpMock + .scope(customVersionsUrl.origin) + .get(customVersionsUrl.pathname) + .reply(200, versionsYaml); + httpMock + .scope(customPackageManifestUrl.origin) + .get(customPackageManifestUrl.pathname) + .reply(200, packageManifestNoReferencesYaml); + const response = await getPkgReleases({ + datasource: GlasskubePackagesDatasource.id, + packageName: 'cloudnative-pg', + registryUrls: [customRegistryUrl], + }); + expect(response).toEqual({ + registryUrl: customRegistryUrl, + tags: { latest: 'v1.23.1+1' }, + releases: [{ version: 'v1.22.0+1' }, { version: 'v1.23.1+1' }], + }); + }); + + it('should handle package manifest with references and default url', async () => { + httpMock + .scope(defaultVersionUrl.origin) + .get(defaultVersionUrl.pathname) + .reply(200, versionsYaml); + httpMock + .scope(defaultPackageManifestUrl.origin) + .get(defaultPackageManifestUrl.pathname) + .reply(200, packageManifestYaml); + const response = await getPkgReleases({ + datasource: GlasskubePackagesDatasource.id, + packageName: 'cloudnative-pg', + }); + expect(response).toEqual({ + sourceUrl: 'https://github.com/cloudnative-pg/cloudnative-pg', + homepage: 'https://cloudnative-pg.io/', + registryUrl: GlasskubePackagesDatasource.defaultRegistryUrl, + tags: { latest: 'v1.23.1+1' }, + releases: [{ version: 'v1.22.0+1' }, { version: 'v1.23.1+1' }], + }); + }); + + it('should handle package manifest with references and custom url', async () => { + httpMock + .scope(customVersionsUrl.origin) + .get(customVersionsUrl.pathname) + .reply(200, versionsYaml); + httpMock + .scope(customPackageManifestUrl.origin) + .get(customPackageManifestUrl.pathname) + .reply(200, packageManifestYaml); + const response = await getPkgReleases({ + datasource: GlasskubePackagesDatasource.id, + packageName: 'cloudnative-pg', + registryUrls: [customRegistryUrl], + }); + expect(response).toEqual({ + sourceUrl: 'https://github.com/cloudnative-pg/cloudnative-pg', + homepage: 'https://cloudnative-pg.io/', + registryUrl: customRegistryUrl, + tags: { latest: 'v1.23.1+1' }, + releases: [{ version: 'v1.22.0+1' }, { version: 'v1.23.1+1' }], + }); + }); +}); diff --git a/lib/modules/datasource/glasskube-packages/index.ts b/lib/modules/datasource/glasskube-packages/index.ts new file mode 100644 index 00000000000000..fef07896ed5972 --- /dev/null +++ b/lib/modules/datasource/glasskube-packages/index.ts @@ -0,0 +1,76 @@ +import { cache } from '../../../util/cache/package/decorator'; +import { joinUrlParts } from '../../../util/url'; +import * as glasskubeVersioning from '../../versioning/glasskube'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, ReleaseResult } from '../types'; +import { + GlasskubePackageManifestYaml, + GlasskubePackageVersions, + GlasskubePackageVersionsYaml, +} from './schema'; + +export class GlasskubePackagesDatasource extends Datasource { + static readonly id = 'glasskube-packages'; + static readonly defaultRegistryUrl = + 'https://packages.dl.glasskube.dev/packages'; + override readonly customRegistrySupport = true; + override defaultVersioning = glasskubeVersioning.id; + + override defaultRegistryUrls = [ + GlasskubePackagesDatasource.defaultRegistryUrl, + ]; + + constructor() { + super(GlasskubePackagesDatasource.id); + } + + @cache({ + namespace: `datasource-${GlasskubePackagesDatasource.id}`, + key: ({ registryUrl, packageName }: GetReleasesConfig) => + `${registryUrl}:${packageName}`, + }) + override async getReleases({ + packageName, + registryUrl, + }: GetReleasesConfig): Promise { + let versions: GlasskubePackageVersions; + const result: ReleaseResult = { releases: [] }; + + try { + const response = await this.http.get( + joinUrlParts(registryUrl!, packageName, 'versions.yaml'), + ); + versions = GlasskubePackageVersionsYaml.parse(response.body); + } catch (err) { + this.handleGenericErrors(err); + } + + result.releases = versions.versions.map((it) => ({ + version: it.version, + })); + result.tags = { latest: versions.latestVersion }; + + try { + const response = await this.http.get( + joinUrlParts( + registryUrl!, + packageName, + versions.latestVersion, + 'package.yaml', + ), + ); + const latestManifest = GlasskubePackageManifestYaml.parse(response.body); + for (const ref of latestManifest?.references ?? []) { + if (ref.label.toLowerCase() === 'github') { + result.sourceUrl = ref.url; + } else if (ref.label.toLowerCase() === 'website') { + result.homepage = ref.url; + } + } + } catch (err) { + this.handleGenericErrors(err); + } + + return result; + } +} diff --git a/lib/modules/datasource/glasskube-packages/schema.ts b/lib/modules/datasource/glasskube-packages/schema.ts new file mode 100644 index 00000000000000..5e299304b4104f --- /dev/null +++ b/lib/modules/datasource/glasskube-packages/schema.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; +import { Yaml } from '../../../util/schema-utils'; + +const GlasskubePackageVersions = z.object({ + latestVersion: z.string(), + versions: z.array(z.object({ version: z.string() })), +}); + +const GlasskubePackageManifest = z.object({ + references: z.optional( + z.array( + z.object({ + label: z.string(), + url: z.string(), + }), + ), + ), +}); + +export const GlasskubePackageVersionsYaml = Yaml.pipe(GlasskubePackageVersions); +export const GlasskubePackageManifestYaml = Yaml.pipe(GlasskubePackageManifest); + +export type GlasskubePackageVersions = z.infer; diff --git a/lib/util/cache/package/types.ts b/lib/util/cache/package/types.ts index e301414c99be6e..e8a479bd1dce51 100644 --- a/lib/util/cache/package/types.ts +++ b/lib/util/cache/package/types.ts @@ -67,6 +67,7 @@ export type PackageCacheNamespace = | 'datasource-gitlab-releases' | 'datasource-gitlab-tags-commit' | 'datasource-gitlab-tags' + | 'datasource-glasskube-packages' | 'datasource-go-direct' | 'datasource-go-proxy' | 'datasource-go'