From a5b729220cb7474fda98279dba79b5f81c7064c6 Mon Sep 17 00:00:00 2001 From: Morre <morre@mor.re> Date: Thu, 1 Jun 2023 18:41:39 +0200 Subject: [PATCH] feat(datasource/endoflife-date): add endoflife.date datasource (#21994) Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com> Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: secustor <sebastian@poxhofer.at> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- lib/modules/datasource/api.ts | 2 + .../__fixtures__/apache-cassandra.json | 40 ++++ .../endoflife-date/__fixtures__/eks.json | 74 +++++++ .../__fixtures__/fairphone.json | 42 ++++ .../datasource/endoflife-date/common.ts | 2 + .../datasource/endoflife-date/index.spec.ts | 193 ++++++++++++++++++ .../datasource/endoflife-date/index.ts | 56 +++++ .../datasource/endoflife-date/readme.md | 46 +++++ .../datasource/endoflife-date/schema.ts | 42 ++++ 9 files changed, 497 insertions(+) create mode 100644 lib/modules/datasource/endoflife-date/__fixtures__/apache-cassandra.json create mode 100644 lib/modules/datasource/endoflife-date/__fixtures__/eks.json create mode 100644 lib/modules/datasource/endoflife-date/__fixtures__/fairphone.json create mode 100644 lib/modules/datasource/endoflife-date/common.ts create mode 100644 lib/modules/datasource/endoflife-date/index.spec.ts create mode 100644 lib/modules/datasource/endoflife-date/index.ts create mode 100644 lib/modules/datasource/endoflife-date/readme.md create mode 100644 lib/modules/datasource/endoflife-date/schema.ts diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index e75ad8aa10..413c8dbf17 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -16,6 +16,7 @@ import { DartVersionDatasource } from './dart-version'; import { DenoDatasource } from './deno'; import { DockerDatasource } from './docker'; import { DotnetVersionDatasource } from './dotnet-version'; +import { EndoflifeDatePackagesource } from './endoflife-date'; import { FlutterVersionDatasource } from './flutter-version'; import { GalaxyDatasource } from './galaxy'; import { GalaxyCollectionDatasource } from './galaxy-collection'; @@ -76,6 +77,7 @@ api.set(DartVersionDatasource.id, new DartVersionDatasource()); api.set(DenoDatasource.id, new DenoDatasource()); api.set(DockerDatasource.id, new DockerDatasource()); api.set(DotnetVersionDatasource.id, new DotnetVersionDatasource()); +api.set(EndoflifeDatePackagesource.id, new EndoflifeDatePackagesource()); api.set(FlutterVersionDatasource.id, new FlutterVersionDatasource()); api.set(GalaxyDatasource.id, new GalaxyDatasource()); api.set(GalaxyCollectionDatasource.id, new GalaxyCollectionDatasource()); diff --git a/lib/modules/datasource/endoflife-date/__fixtures__/apache-cassandra.json b/lib/modules/datasource/endoflife-date/__fixtures__/apache-cassandra.json new file mode 100644 index 0000000000..919e7fef24 --- /dev/null +++ b/lib/modules/datasource/endoflife-date/__fixtures__/apache-cassandra.json @@ -0,0 +1,40 @@ +[ + { + "cycle": "4.1", + "eol": "2025-05-01", + "support": true, + "releaseDate": "2022-12-13", + "latest": "4.1.1", + "latestReleaseDate": "2023-03-21", + "lts": false + }, + { + "cycle": "4.0", + "eol": "2024-05-01", + "support": true, + "releaseDate": "2021-07-26", + "latest": "4.0.9", + "latestReleaseDate": "2023-04-14", + "lts": false + }, + { + "cycle": "3.11", + "eol": "2023-05-01", + "support": true, + "releaseDate": "2017-06-23", + "latest": "3.11.15", + "discontinued": true, + "latestReleaseDate": "2023-05-05", + "lts": false + }, + { + "cycle": "3.0", + "eol": "2023-05-01", + "support": true, + "releaseDate": "2015-11-09", + "latest": "3.0.29", + "discontinued": true, + "latestReleaseDate": "2023-05-15", + "lts": false + } +] diff --git a/lib/modules/datasource/endoflife-date/__fixtures__/eks.json b/lib/modules/datasource/endoflife-date/__fixtures__/eks.json new file mode 100644 index 0000000000..ce0291509b --- /dev/null +++ b/lib/modules/datasource/endoflife-date/__fixtures__/eks.json @@ -0,0 +1,74 @@ +[ + { + "cycle": "1.26", + "releaseDate": "2023-04-11", + "eol": false, + "latest": "1.26-eks-1", + "latestReleaseDate": "2023-04-11", + "lts": false + }, + { + "cycle": "1.25", + "releaseDate": "2023-02-21", + "eol": "2024-05-01", + "latest": "1.25-eks-3", + "latestReleaseDate": "2023-04-06", + "lts": false + }, + { + "cycle": "1.24", + "eol": "2024-01-01", + "latest": "1.24-eks-6", + "latestReleaseDate": "2023-04-06", + "releaseDate": "2022-11-15", + "lts": false + }, + { + "cycle": "1.23", + "eol": "2023-10-01", + "latest": "1.23-eks-8", + "releaseDate": "2022-08-11", + "latestReleaseDate": "2023-04-06", + "lts": false + }, + { + "cycle": "1.22", + "eol": "2023-06-04", + "latest": "1.22-eks-12", + "releaseDate": "2022-04-04", + "latestReleaseDate": "2023-04-06", + "lts": false + }, + { + "cycle": "1.21", + "eol": "2023-02-15", + "latest": "1.21-eks-17", + "releaseDate": "2021-07-19", + "latestReleaseDate": "2023-04-06", + "lts": false + }, + { + "cycle": "1.20", + "eol": "2022-11-01", + "latest": "1.20-eks-14", + "releaseDate": "2021-05-18", + "latestReleaseDate": "2023-04-06", + "lts": false + }, + { + "cycle": "1.19", + "eol": "2022-08-01", + "latest": "1.19-eks-11", + "releaseDate": "2021-02-16", + "latestReleaseDate": "2022-08-15", + "lts": false + }, + { + "cycle": "1.18", + "eol": "2022-03-31", + "latest": "1.18-eks-13", + "releaseDate": "2020-10-13", + "latestReleaseDate": "2022-08-15", + "lts": false + } +] diff --git a/lib/modules/datasource/endoflife-date/__fixtures__/fairphone.json b/lib/modules/datasource/endoflife-date/__fixtures__/fairphone.json new file mode 100644 index 0000000000..c4fc18537f --- /dev/null +++ b/lib/modules/datasource/endoflife-date/__fixtures__/fairphone.json @@ -0,0 +1,42 @@ +[ + { + "cycle": "4", + "discontinued": false, + "eol": "2026-09-30", + "releaseDate": "2021-09-30", + "link": "https://shop.fairphone.com/buy-fairphone-4", + "lts": false + }, + { + "cycle": "3+", + "discontinued": "2022-11-01", + "eol": "2025-09-30", + "releaseDate": "2020-09-30", + "link": "https://shop.fairphone.com/fairphone-3-plus", + "lts": false + }, + { + "cycle": "3", + "discontinued": "2021-09-01", + "eol": "2024-09-30", + "releaseDate": "2019-09-30", + "link": "https://shop.fairphone.com/fairphone-3", + "lts": false + }, + { + "cycle": "2", + "discontinued": "2019-03-31", + "eol": "2023-03-07", + "releaseDate": "2015-12-21", + "link": "https://support.fairphone.com/hc/articles/213290023-FP2-Fairphone-OS-downloads", + "lts": false + }, + { + "cycle": "1", + "discontinued": "2017-07-13", + "eol": "2017-07-13", + "releaseDate": "2013-12-01", + "link": "https://support.fairphone.com/hc/articles/6217522827281-Fairphone-1-Frequently-Asked-Questions-FAQ-", + "lts": false + } +] diff --git a/lib/modules/datasource/endoflife-date/common.ts b/lib/modules/datasource/endoflife-date/common.ts new file mode 100644 index 0000000000..71b3a4c953 --- /dev/null +++ b/lib/modules/datasource/endoflife-date/common.ts @@ -0,0 +1,2 @@ +export const registryUrl = 'https://endoflife.date/api'; +export const datasource = 'endoflife-date'; diff --git a/lib/modules/datasource/endoflife-date/index.spec.ts b/lib/modules/datasource/endoflife-date/index.spec.ts new file mode 100644 index 0000000000..6e5bc45123 --- /dev/null +++ b/lib/modules/datasource/endoflife-date/index.spec.ts @@ -0,0 +1,193 @@ +import { getPkgReleases } from '..'; +import { Fixtures } from '../../../../test/fixtures'; +import * as httpMock from '../../../../test/http-mock'; +import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages'; +import { registryUrl } from './common'; +import { EndoflifeDatePackagesource } from './index'; + +const datasource = EndoflifeDatePackagesource.id; + +// Default package name and mock path to test with +const packageName = 'amazon-eks'; +const eksMockPath = `/${packageName}.json`; + +describe('modules/datasource/endoflife-date/index', () => { + describe('getReleases', () => { + it('processes real data', async () => { + httpMock + .scope(registryUrl) + .get(eksMockPath) + .reply(200, Fixtures.getJson(`eks.json`)); + const res = await getPkgReleases({ + datasource, + packageName, + }); + expect(res).toEqual({ + registryUrl: 'https://endoflife.date/api', + releases: [ + { + isDeprecated: true, + releaseTimestamp: '2020-10-13T00:00:00.000Z', + version: '1.18-eks-13', + }, + { + isDeprecated: true, + releaseTimestamp: '2021-02-16T00:00:00.000Z', + version: '1.19-eks-11', + }, + { + isDeprecated: true, + releaseTimestamp: '2021-05-18T00:00:00.000Z', + version: '1.20-eks-14', + }, + { + isDeprecated: true, + releaseTimestamp: '2021-07-19T00:00:00.000Z', + version: '1.21-eks-17', + }, + { + isDeprecated: false, + releaseTimestamp: '2022-04-04T00:00:00.000Z', + version: '1.22-eks-12', + }, + { + isDeprecated: false, + releaseTimestamp: '2022-08-11T00:00:00.000Z', + version: '1.23-eks-8', + }, + { + isDeprecated: false, + releaseTimestamp: '2022-11-15T00:00:00.000Z', + version: '1.24-eks-6', + }, + { + isDeprecated: false, + releaseTimestamp: '2023-02-21T00:00:00.000Z', + version: '1.25-eks-3', + }, + { + isDeprecated: false, + releaseTimestamp: '2023-04-11T00:00:00.000Z', + version: '1.26-eks-1', + }, + ], + }); + }); + + it('returns null without registryUrl', async () => { + const endoflifeDateDatasource = new EndoflifeDatePackagesource(); + const res = await endoflifeDateDatasource.getReleases({ + registryUrl: '', + packageName, + }); + expect(res).toBeNull(); + }); + + it('returns null for 404', async () => { + httpMock.scope(registryUrl).get(eksMockPath).reply(404); + expect( + await getPkgReleases({ + datasource, + packageName, + }) + ).toBeNull(); + }); + + it('returns null for empty result', async () => { + httpMock.scope(registryUrl).get(eksMockPath).reply(200, {}); + expect( + await getPkgReleases({ + datasource, + packageName, + }) + ).toBeNull(); + }); + + it('throws for 5xx', async () => { + httpMock.scope(registryUrl).get(eksMockPath).reply(502); + await expect( + getPkgReleases({ + datasource, + packageName, + }) + ).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); + + it('detects boolean discontinuation', async () => { + httpMock + .scope(registryUrl) + .get('/apache-cassandra.json') + .reply(200, Fixtures.getJson(`apache-cassandra.json`)); + const res = await getPkgReleases({ + datasource, + packageName: 'apache-cassandra', + }); + expect(res).toEqual({ + registryUrl: 'https://endoflife.date/api', + releases: [ + { + isDeprecated: true, + releaseTimestamp: '2015-11-09T00:00:00.000Z', + version: '3.0.29', + }, + { + isDeprecated: true, + releaseTimestamp: '2017-06-23T00:00:00.000Z', + version: '3.11.15', + }, + { + isDeprecated: false, + releaseTimestamp: '2021-07-26T00:00:00.000Z', + version: '4.0.9', + }, + { + isDeprecated: false, + releaseTimestamp: '2022-12-13T00:00:00.000Z', + version: '4.1.1', + }, + ], + }); + }); + + it('detects date discontinuation', async () => { + httpMock + .scope(registryUrl) + .get('/fairphone.json') + .reply(200, Fixtures.getJson(`fairphone.json`)); + const res = await getPkgReleases({ + datasource, + packageName: 'fairphone', + }); + expect(res).toEqual({ + registryUrl: 'https://endoflife.date/api', + releases: [ + { + isDeprecated: true, + releaseTimestamp: '2013-12-01T00:00:00.000Z', + version: '1', + }, + { + isDeprecated: true, + releaseTimestamp: '2015-12-21T00:00:00.000Z', + version: '2', + }, + { + isDeprecated: true, + releaseTimestamp: '2020-09-30T00:00:00.000Z', + version: '3+', + }, + { + isDeprecated: true, + releaseTimestamp: '2019-09-30T00:00:00.000Z', + version: '3', + }, + { + isDeprecated: false, + releaseTimestamp: '2021-09-30T00:00:00.000Z', + version: '4', + }, + ], + }); + }); + }); +}); diff --git a/lib/modules/datasource/endoflife-date/index.ts b/lib/modules/datasource/endoflife-date/index.ts new file mode 100644 index 0000000000..9eba4d50eb --- /dev/null +++ b/lib/modules/datasource/endoflife-date/index.ts @@ -0,0 +1,56 @@ +import is from '@sindresorhus/is'; +import { logger } from '../../../logger'; +import { cache } from '../../../util/cache/package/decorator'; +import { joinUrlParts } from '../../../util/url'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, ReleaseResult } from '../types'; +import { datasource, registryUrl } from './common'; +import { EndoflifeHttpResponseScheme } from './schema'; + +export class EndoflifeDatePackagesource extends Datasource { + static readonly id = datasource; + + override readonly defaultRegistryUrls = [registryUrl]; + override readonly caching = true; + override readonly defaultVersioning = 'loose'; + + constructor() { + super(EndoflifeDatePackagesource.id); + } + + @cache({ + namespace: `datasource-${datasource}`, + key: ({ registryUrl, packageName }: GetReleasesConfig) => + // TODO: types (#7154) + `${registryUrl!}:${packageName}`, + }) + async getReleases({ + registryUrl, + packageName, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + if (!is.nonEmptyString(registryUrl)) { + return null; + } + + logger.trace(`${datasource}.getReleases(${registryUrl}, ${packageName})`); + + const result: ReleaseResult = { + releases: [], + }; + + const url = joinUrlParts(registryUrl, `${packageName}.json`); + + try { + const response = await this.http.getJson( + url, + EndoflifeHttpResponseScheme + ); + + result.releases.push(...response.body); + + return result.releases.length ? result : null; + } catch (err) { + this.handleGenericErrors(err); + } + } +} diff --git a/lib/modules/datasource/endoflife-date/readme.md b/lib/modules/datasource/endoflife-date/readme.md new file mode 100644 index 0000000000..0c79625572 --- /dev/null +++ b/lib/modules/datasource/endoflife-date/readme.md @@ -0,0 +1,46 @@ +[endoflife.date](https://endoflife.date) provides version and end-of-life information for different packages. + +To find the appropriate "package" name for the software you're trying to update, use the endoflife.date "All packages" API endpoint. +You can find it in [the endoflife.date API documentation](https://endoflife.date/docs/api). + +By default, this datasource uses `loose` versioning. +If possible, we recommend you use a stricter versioning like `semver` instead of `loose`. + +**Usage Example** + +Say you're using Amazon EKS and want Renovate to update the versions in a Terraform `.tfvars` file. +For example, you have this `.tfvars` file: + +```hcl +# renovate: datasource=endoflife-date depName=amazon-eks versioning=loose +kubernetes_version = "1.26" +``` + +Given the above `.tfvars` file, you put this in your `renovate.json`: + +```json +{ + "regexManagers": [ + { + "description": "Update Kubernetes version for Amazon EKS in tfvars files", + "fileMatch": [".+\\.tfvars$"], + "matchStrings": [ + "#\\s*renovate:\\s*datasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s.*?_version\\s*=\\s*\"(?<currentValue>.*)\"" + ], + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{/if}}" + } + ], + "packageRules": [ + { + "matchDatasources": ["endoflife-date"], + "matchPackageNames": ["amazon-eks"], + "extractVersion": "^(?<version>.*)-eks.+$" + } + ] +} +``` + +With this configuration, Renovate will parse all `*.tfvars` files in the repository. +It will then update variables that end with `_version` and have the `# renovate: datasource=endoflife-date depName=dependency-name versioning=versioning` comment set in the line above when any new versions are available. + +For `amazon-eks`, the defined `packageRule` above will also strip the `-eks-${eks-release-version}` suffix to only set the Kubernetes minor version. diff --git a/lib/modules/datasource/endoflife-date/schema.ts b/lib/modules/datasource/endoflife-date/schema.ts new file mode 100644 index 0000000000..38b58c98e5 --- /dev/null +++ b/lib/modules/datasource/endoflife-date/schema.ts @@ -0,0 +1,42 @@ +import { DateTime } from 'luxon'; +import { z } from 'zod'; +import type { Release } from '../types'; + +const EndoflifeDateVersionScheme = z + .object({ + cycle: z.string(), + latest: z.optional(z.string()), + releaseDate: z.optional(z.string()), + eol: z.optional(z.union([z.string(), z.boolean()])), + discontinued: z.optional(z.union([z.string(), z.boolean()])), + }) + .transform(({ cycle, latest, releaseDate, eol, discontinued }): Release => { + let isDeprecated = false; + + // If "eol" date or "discontinued" date has passed or any of the values is explicitly true, set to deprecated + // "support" is not checked because support periods sometimes end before the EOL. + if ( + eol === true || + discontinued === true || + (typeof eol === 'string' && + DateTime.fromISO(eol, { zone: 'utc' }) <= DateTime.now().toUTC()) || + (typeof discontinued === 'string' && + DateTime.fromISO(discontinued, { zone: 'utc' }) <= + DateTime.now().toUTC()) + ) { + isDeprecated = true; + } + + let version = cycle; + if (latest !== undefined) { + version = latest; + } + + return { + version, + releaseTimestamp: releaseDate, + isDeprecated, + }; + }); + +export const EndoflifeHttpResponseScheme = z.array(EndoflifeDateVersionScheme); -- GitLab