From 789caadbb49e232ef74f59f5c41c1a6fbffee5c0 Mon Sep 17 00:00:00 2001 From: Jamie Magee <jamie.magee@gmail.com> Date: Thu, 18 Nov 2021 11:55:21 -0800 Subject: [PATCH] refactor(jenkins-plugins): convert to class-based datasource (#12702) --- lib/datasource/api.ts | 4 +- lib/datasource/jenkins-plugins/common.ts | 1 - lib/datasource/jenkins-plugins/get.ts | 176 ------------------- lib/datasource/jenkins-plugins/index.spec.ts | 30 +--- lib/datasource/jenkins-plugins/index.ts | 107 ++++++++++- lib/datasource/jenkins-plugins/types.ts | 13 -- lib/manager/jenkins/extract.ts | 4 +- tsconfig.strict.json | 1 - 8 files changed, 109 insertions(+), 227 deletions(-) delete mode 100644 lib/datasource/jenkins-plugins/common.ts delete mode 100644 lib/datasource/jenkins-plugins/get.ts diff --git a/lib/datasource/api.ts b/lib/datasource/api.ts index d7619f3ac6..89b88645fc 100644 --- a/lib/datasource/api.ts +++ b/lib/datasource/api.ts @@ -19,7 +19,7 @@ import * as go from './go'; import { GradleVersionDatasource } from './gradle-version'; import { HelmDatasource } from './helm'; import { HexDatasource } from './hex'; -import * as jenkinsPlugins from './jenkins-plugins'; +import { JenkinsPluginsDatasource } from './jenkins-plugins'; import * as maven from './maven'; import { NodeDatasource } from './node'; import * as npm from './npm'; @@ -61,7 +61,7 @@ api.set('go', go); api.set('gradle-version', new GradleVersionDatasource()); api.set('helm', new HelmDatasource()); api.set('hex', new HexDatasource()); -api.set('jenkins-plugins', jenkinsPlugins); +api.set('jenkins-plugins', new JenkinsPluginsDatasource()); api.set('maven', maven); api.set('npm', npm); api.set(NodeDatasource.id, new NodeDatasource()); diff --git a/lib/datasource/jenkins-plugins/common.ts b/lib/datasource/jenkins-plugins/common.ts deleted file mode 100644 index 22acbbb5ab..0000000000 --- a/lib/datasource/jenkins-plugins/common.ts +++ /dev/null @@ -1 +0,0 @@ -export const id = 'jenkins-plugins'; diff --git a/lib/datasource/jenkins-plugins/get.ts b/lib/datasource/jenkins-plugins/get.ts deleted file mode 100644 index a80c96ef2c..0000000000 --- a/lib/datasource/jenkins-plugins/get.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { logger } from '../../logger'; -import { ExternalHostError } from '../../types/errors/external-host-error'; -import { clone } from '../../util/clone'; -import { getElapsedMinutes } from '../../util/date'; -import { Http } from '../../util/http'; -import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; -import { id } from './common'; -import type { - JenkinsCache, - JenkinsCacheTypes, - JenkinsPluginsInfoResponse, - JenkinsPluginsVersionsResponse, -} from './types'; - -const http = new Http(id); - -const packageInfoUrl = - 'https://updates.jenkins.io/current/update-center.actual.json'; -const packageVersionsUrl = - 'https://updates.jenkins.io/current/plugin-versions.json'; - -function hasCacheExpired(cache: JenkinsCache<JenkinsCacheTypes>): boolean { - return getElapsedMinutes(cache.lastSync) >= cache.cacheTimeMin; -} - -async function updateJenkinsCache( - cache: JenkinsCache<JenkinsCacheTypes>, - updateHandler: () => Promise<void> -): Promise<void> { - if (hasCacheExpired(cache)) { - cache.updatePromise = - // eslint-disable-next-line @typescript-eslint/no-misused-promises - cache.updatePromise || updateHandler(); - await cache.updatePromise; - - cache.updatePromise = null; - } -} - -function updateJenkinsPluginInfoCacheCallback( - response: JenkinsPluginsInfoResponse, - cache: JenkinsCache<ReleaseResult> -): void { - for (const name of Object.keys(response.plugins || [])) { - cache.cache[name] = { - releases: [], // releases are stored in another cache - sourceUrl: response.plugins[name]?.scm, - }; - } -} - -function updateJenkinsPluginVersionsCacheCallback( - response: JenkinsPluginsVersionsResponse, - cache: JenkinsCache<Release[]> -): void { - const plugins = response.plugins; - for (const name of Object.keys(plugins || [])) { - cache.cache[name] = Object.keys(plugins[name]).map((version) => ({ - version, - downloadUrl: plugins[name][version]?.url, - releaseTimestamp: plugins[name][version]?.buildDate - ? new Date(plugins[name][version].buildDate + ' UTC') - : null, - })); - } -} - -async function getJenkinsUpdateCenterResponse<T>( - cache: JenkinsCache<JenkinsCacheTypes> -): Promise<T> { - let response: T; - - const options = { - headers: { - 'Accept-Encoding': 'gzip, deflate, br', - }, - }; - - try { - logger.debug(`jenkins-plugins: Fetching Jenkins plugins ${cache.name}`); - const startTime = Date.now(); - response = (await http.getJson<T>(cache.dataUrl, options)).body; - const durationMs = Math.round(Date.now() - startTime); - logger.debug( - { durationMs }, - `jenkins-plugins: Fetched Jenkins plugins ${cache.name}` - ); - } catch (err) /* istanbul ignore next */ { - cache.cache = Object.create(null); - throw new ExternalHostError( - new Error(`jenkins-plugins: Fetch plugins ${cache.name} error`) - ); - } - - return response; -} - -async function updateJenkinsPluginCache<T>( - cache: JenkinsCache<JenkinsCacheTypes>, - - callback: (resp: T, cache: JenkinsCache<any>) => void -): Promise<void> { - const response = await getJenkinsUpdateCenterResponse<T>(cache); - if (response) { - callback(response, cache); - } - cache.lastSync = new Date(); -} - -const pluginInfoCache: JenkinsCache<ReleaseResult> = { - name: 'info', - dataUrl: packageInfoUrl, - lastSync: new Date('2000-01-01'), - cacheTimeMin: 1440, - cache: Object.create(null), -}; - -const pluginVersionsCache: JenkinsCache<Release[]> = { - name: 'versions', - dataUrl: packageVersionsUrl, - lastSync: new Date('2000-01-01'), - cacheTimeMin: 60, - cache: Object.create(null), -}; - -async function updateJenkinsPluginInfoCache(): Promise<void> { - await updateJenkinsPluginCache<JenkinsPluginsInfoResponse>( - pluginInfoCache, - updateJenkinsPluginInfoCacheCallback - ); -} - -async function updateJenkinsPluginVersionsCache(): Promise<void> { - await updateJenkinsPluginCache<JenkinsPluginsVersionsResponse>( - pluginVersionsCache, - updateJenkinsPluginVersionsCacheCallback - ); -} - -export async function getJenkinsPluginDependency( - lookupName: string -): Promise<ReleaseResult | null> { - logger.debug(`getJenkinsDependency(${lookupName})`); - await updateJenkinsCache(pluginInfoCache, updateJenkinsPluginInfoCache); - await updateJenkinsCache( - pluginVersionsCache, - updateJenkinsPluginVersionsCache - ); - - const plugin = pluginInfoCache.cache[lookupName]; - if (!plugin) { - return null; - } - - const result = clone(plugin); - const releases = pluginVersionsCache.cache[lookupName]; - result.releases = releases ? clone(releases) : []; - return result; -} - -export function getReleases({ - lookupName, -}: GetReleasesConfig): Promise<ReleaseResult | null> { - return getJenkinsPluginDependency(lookupName); -} - -function resetJenkinsCache(cache: JenkinsCache<JenkinsCacheTypes>): void { - cache.lastSync = new Date('2000-01-01'); - cache.cache = Object.create(null); -} - -// Note: use only for tests -export function resetCache(): void { - resetJenkinsCache(pluginInfoCache); - resetJenkinsCache(pluginVersionsCache); -} diff --git a/lib/datasource/jenkins-plugins/index.spec.ts b/lib/datasource/jenkins-plugins/index.spec.ts index 9057c64438..429bc6d440 100644 --- a/lib/datasource/jenkins-plugins/index.spec.ts +++ b/lib/datasource/jenkins-plugins/index.spec.ts @@ -2,34 +2,24 @@ import { getPkgReleases } from '..'; import * as httpMock from '../../../test/http-mock'; import { loadJsonFixture } from '../../../test/util'; import * as versioning from '../../versioning/docker'; -import { resetCache } from './get'; -import * as jenkins from '.'; +import { JenkinsPluginsDatasource } from '.'; const jenkinsPluginsVersions = loadJsonFixture('plugin-versions.json'); const jenkinsPluginsInfo = loadJsonFixture('update-center.actual.json'); describe('datasource/jenkins-plugins/index', () => { describe('getReleases', () => { - const SKIP_CACHE = process.env.RENOVATE_SKIP_CACHE; - const params = { versioning: versioning.id, - datasource: jenkins.id, + datasource: JenkinsPluginsDatasource.id, depName: 'email-ext', registryUrls: ['https://updates.jenkins.io/'], }; - beforeEach(() => { - resetCache(); - process.env.RENOVATE_SKIP_CACHE = 'true'; - jest.resetAllMocks(); - }); - afterEach(() => { if (!httpMock.allUsed()) { throw new Error('Not all http mocks have been used!'); } - process.env.RENOVATE_SKIP_CACHE = SKIP_CACHE; }); it('returns null for a package miss', async () => { @@ -41,11 +31,6 @@ describe('datasource/jenkins-plugins/index', () => { .get('/current/update-center.actual.json') .reply(200, jenkinsPluginsInfo); - httpMock - .scope('https://updates.jenkins.io') - .get('/current/plugin-versions.json') - .reply(200, jenkinsPluginsVersions); - expect(await getPkgReleases(newparams)).toBeNull(); }); @@ -60,7 +45,7 @@ describe('datasource/jenkins-plugins/index', () => { .get('/current/plugin-versions.json') .reply(200, jenkinsPluginsVersions); - let res = await getPkgReleases(params); + const res = await getPkgReleases(params); expect(res.releases).toHaveLength(75); expect(res).toMatchSnapshot(); @@ -74,10 +59,6 @@ describe('datasource/jenkins-plugins/index', () => { expect( res.releases.find((release) => release.version === '12.98') ).toBeUndefined(); - - // check that caching is working and no http requests are done after the first call to getPkgReleases - res = await getPkgReleases(params); - expect(res.releases).toHaveLength(75); }); it('returns package releases for a hit for info and miss for releases', async () => { @@ -106,11 +87,6 @@ describe('datasource/jenkins-plugins/index', () => { .get('/current/update-center.actual.json') .reply(200, '{}'); - httpMock - .scope('https://updates.jenkins.io') - .get('/current/plugin-versions.json') - .reply(200, '{}'); - expect(await getPkgReleases(params)).toBeNull(); }); }); diff --git a/lib/datasource/jenkins-plugins/index.ts b/lib/datasource/jenkins-plugins/index.ts index 5790e0e57c..dfafe85cb8 100644 --- a/lib/datasource/jenkins-plugins/index.ts +++ b/lib/datasource/jenkins-plugins/index.ts @@ -1,5 +1,102 @@ -export { id } from './common'; -export { getReleases } from './get'; -export const customRegistrySupport = true; -export const defaultRegistryUrls = ['https://updates.jenkins.io']; -export const registryStrategy = 'hunt'; +import { logger } from '../../logger'; +import { cache } from '../../util/cache/package/decorator'; +import { clone } from '../../util/clone'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; +import type { + JenkinsPluginsInfoResponse, + JenkinsPluginsVersionsResponse, +} from './types'; + +export class JenkinsPluginsDatasource extends Datasource { + static readonly id = 'jenkins-plugins'; + + constructor() { + super(JenkinsPluginsDatasource.id); + } + + override readonly defaultRegistryUrls = ['https://updates.jenkins.io']; + + override readonly registryStrategy = 'hunt'; + + private static readonly packageInfoUrl = + 'https://updates.jenkins.io/current/update-center.actual.json'; + private static readonly packageVersionsUrl = + 'https://updates.jenkins.io/current/plugin-versions.json'; + + async getReleases({ + lookupName, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + const plugins = await this.getJenkinsPluginInfo(); + const plugin = plugins[lookupName]; + if (!plugin) { + return null; + } + + const result = clone(plugin); + const versions = await this.getJenkinsPluginVersions(); + const releases = versions[lookupName]; + result.releases = releases ? clone(releases) : []; + return result; + } + + @cache({ + namespace: JenkinsPluginsDatasource.id, + key: 'info', + ttlMinutes: 1440, + }) + async getJenkinsPluginInfo(): Promise<Record<string, ReleaseResult>> { + const { plugins } = + await this.getJenkinsUpdateCenterResponse<JenkinsPluginsInfoResponse>( + JenkinsPluginsDatasource.packageInfoUrl + ); + + const info: Record<string, ReleaseResult> = {}; + for (const name of Object.keys(plugins ?? [])) { + info[name] = { + releases: [], // releases + sourceUrl: plugins[name]?.scm, + }; + } + return info; + } + + @cache({ namespace: JenkinsPluginsDatasource.id, key: 'versions' }) + async getJenkinsPluginVersions(): Promise<Record<string, Release[]>> { + const { plugins } = + await this.getJenkinsUpdateCenterResponse<JenkinsPluginsVersionsResponse>( + JenkinsPluginsDatasource.packageVersionsUrl + ); + + const versions: Record<string, Release[]> = {}; + for (const name of Object.keys(plugins ?? [])) { + versions[name] = Object.keys(plugins[name]).map((version) => ({ + version, + downloadUrl: plugins[name][version]?.url, + releaseTimestamp: plugins[name][version]?.buildDate + ? new Date(`${plugins[name][version].buildDate} UTC`) + : null, + })); + } + return versions; + } + + private async getJenkinsUpdateCenterResponse<T>(url: string): Promise<T> { + let response: T; + + try { + logger.debug(`jenkins-plugins: Fetching Jenkins plugins from ${url}`); + const startTime = Date.now(); + response = (await this.http.getJson<T>(url)).body; + const durationMs = Math.round(Date.now() - startTime); + logger.debug( + { durationMs }, + `jenkins-plugins: Fetched Jenkins plugins from ${url}` + ); + } catch (err) /* istanbul ignore next */ { + this.handleGenericErrors(err); + } + + return response; + } +} diff --git a/lib/datasource/jenkins-plugins/types.ts b/lib/datasource/jenkins-plugins/types.ts index 6c67a9bca1..16e730a309 100644 --- a/lib/datasource/jenkins-plugins/types.ts +++ b/lib/datasource/jenkins-plugins/types.ts @@ -1,16 +1,3 @@ -import type { Release, ReleaseResult } from '../types'; - -export type JenkinsCacheTypes = ReleaseResult | Release[]; - -export interface JenkinsCache<T> { - name: string; - dataUrl: string; - lastSync: Date; - cacheTimeMin: number; - cache: Record<string, T>; - updatePromise?: Promise<void> | undefined; -} - export interface JenkinsPluginInfo { name: string; scm?: string; diff --git a/lib/manager/jenkins/extract.ts b/lib/manager/jenkins/extract.ts index c5e9d013eb..3dcc58b0ce 100644 --- a/lib/manager/jenkins/extract.ts +++ b/lib/manager/jenkins/extract.ts @@ -1,5 +1,5 @@ import { load } from 'js-yaml'; -import * as datasourceJenkins from '../../datasource/jenkins-plugins'; +import { JenkinsPluginsDatasource } from '../../datasource/jenkins-plugins'; import { logger } from '../../logger'; import { SkipReason } from '../../types'; import { isSkipComment } from '../../util/ignore'; @@ -12,7 +12,7 @@ const YamlExtension = regEx(/\.ya?ml$/); function getDependency(plugin: JenkinsPlugin): PackageDependency { const dep: PackageDependency = { - datasource: datasourceJenkins.id, + datasource: JenkinsPluginsDatasource.id, versioning: dockerVersioning.id, depName: plugin.artifactId, }; diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 50bda12748..dbadb581e3 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -31,7 +31,6 @@ "./lib/datasource/gitlab-tags/types.ts", "./lib/datasource/gradle-version/types.ts", "./lib/datasource/hex/types.ts", - "./lib/datasource/jenkins-plugins/common.ts", "./lib/datasource/maven/common.ts", "./lib/datasource/node/common.ts", "./lib/datasource/node/types.ts", -- GitLab