From 039fce8dc6082e2268049939c13dc67585bb1708 Mon Sep 17 00:00:00 2001 From: Gabriel-Ladzaretti <97394622+Gabriel-Ladzaretti@users.noreply.github.com> Date: Sun, 21 Aug 2022 15:53:20 +0300 Subject: [PATCH] refactor(repository/cache): add support for adding various cache clients (#17146) Co-authored-by: Nabeel Saabna <48175656+nabeelsaabna@users.noreply.github.com> Co-authored-by: Rhys Arkins <rhys@arkins.net> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- docs/usage/self-hosted-configuration.md | 2 + lib/config/options/index.ts | 10 +++ lib/config/types.ts | 2 + lib/util/cache/repository/impl/base.ts | 72 ++++++++++++++-- .../cache/repository/impl/cache-factory.ts | 22 +++++ lib/util/cache/repository/impl/local.spec.ts | 34 +++++--- lib/util/cache/repository/impl/local.ts | 86 ++++--------------- lib/util/cache/repository/impl/null.ts | 19 ++++ lib/util/cache/repository/index.ts | 8 +- lib/util/cache/repository/init.ts | 22 ++--- 10 files changed, 175 insertions(+), 102 deletions(-) create mode 100644 lib/util/cache/repository/impl/cache-factory.ts create mode 100644 lib/util/cache/repository/impl/null.ts diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 423019b9f2..650c681856 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -626,6 +626,8 @@ Set this to `"enabled"` to have Renovate maintain a JSON file cache per-reposito Set to `"reset"` if you ever need to bypass the cache and have it overwritten. JSON files will be stored inside the `cacheDir` beside the existing file-based package cache. +## repositoryCacheType + ## requireConfig By default, Renovate needs a Renovate config file in each repository where it runs before it will propose any dependency updates. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index c791195515..a8736faccc 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -229,6 +229,16 @@ const options: RenovateOptions[] = [ default: 'disabled', experimental: true, }, + { + name: 'repositoryCacheType', + description: + 'Set the type of renovate repository cache if repositoryCache is not disabled.', + globalOnly: true, + type: 'string', + stage: 'repository', + default: 'local', + experimental: true, + }, { name: 'force', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index 4e11fa6e81..65ccc58323 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -11,6 +11,7 @@ export type RenovateConfigStage = | 'pr'; export type RepositoryCacheConfig = 'disabled' | 'enabled' | 'reset'; +export type RepositoryCacheType = 'local' | string; export type DryRunConfig = 'extract' | 'lookup' | 'full'; export type RequiredConfig = 'required' | 'optional' | 'ignored'; @@ -65,6 +66,7 @@ export interface RenovateSharedConfig { recreateClosed?: boolean; repository?: string; repositoryCache?: RepositoryCacheConfig; + repositoryCacheType?: RepositoryCacheType; schedule?: string[]; automergeSchedule?: string[]; semanticCommits?: 'auto' | 'enabled' | 'disabled'; diff --git a/lib/util/cache/repository/impl/base.ts b/lib/util/cache/repository/impl/base.ts index 449a0801b2..f2950a3286 100644 --- a/lib/util/cache/repository/impl/base.ts +++ b/lib/util/cache/repository/impl/base.ts @@ -1,16 +1,74 @@ -import type { RepoCache, RepoCacheData } from '../types'; +import { promisify } from 'util'; +import zlib from 'zlib'; +import hasha from 'hasha'; +import { GlobalConfig } from '../../../../config/global'; +import { logger } from '../../../../logger'; +import { + CACHE_REVISION, + isValidRev10, + isValidRev11, + isValidRev12, +} from '../common'; +import type { RepoCache, RepoCacheData, RepoCacheRecord } from '../types'; -export class RepoCacheBase implements RepoCache { - protected data: RepoCacheData = {}; +const compress = promisify(zlib.brotliCompress); +const decompress = promisify(zlib.brotliDecompress); + +export abstract class RepoCacheBase implements RepoCache { + protected platform = GlobalConfig.get('platform')!; + private oldHash: string | null = null; + private data: RepoCacheData = {}; + + protected constructor(protected readonly repository: string) {} + + protected abstract read(): Promise<string | undefined>; + + protected abstract write(data: RepoCacheRecord): Promise<void>; - // istanbul ignore next async load(): Promise<void> { - await Promise.resolve(); + try { + const oldCache = await this.read(); + + if (isValidRev12(oldCache, this.repository)) { + const compressed = Buffer.from(oldCache.payload, 'base64'); + const uncompressed = await decompress(compressed); + const jsonStr = uncompressed.toString('utf8'); + this.data = JSON.parse(jsonStr); + this.oldHash = oldCache.hash; + logger.debug('Repository cache is valid'); + return; + } + + if (isValidRev11(oldCache, this.repository)) { + this.data = oldCache.data; + logger.debug('Repository cache is migrated from 11 revision'); + return; + } + + if (isValidRev10(oldCache, this.repository)) { + delete oldCache.repository; + delete oldCache.revision; + this.data = oldCache; + logger.debug('Repository cache is migrated from 10 revision'); + return; + } + + logger.debug('Repository cache is invalid'); + } catch (err) { + logger.debug('Error reading repository cache'); + } } - // istanbul ignore next async save(): Promise<void> { - await Promise.resolve(); + const revision = CACHE_REVISION; + const repository = this.repository; + const jsonStr = JSON.stringify(this.data); + const hash = await hasha.async(jsonStr, { algorithm: 'sha256' }); + if (hash !== this.oldHash) { + const compressed = await compress(jsonStr); + const payload = compressed.toString('base64'); + await this.write({ revision, repository, payload, hash }); + } } getData(): RepoCacheData { diff --git a/lib/util/cache/repository/impl/cache-factory.ts b/lib/util/cache/repository/impl/cache-factory.ts new file mode 100644 index 0000000000..1ebfa5136b --- /dev/null +++ b/lib/util/cache/repository/impl/cache-factory.ts @@ -0,0 +1,22 @@ +import type { RepositoryCacheType } from '../../../../config/types'; +import { logger } from '../../../../logger'; +import type { RepoCache } from '../types'; +import { RepoCacheLocal } from './local'; + +export class CacheFactory { + static get( + repository: string, + cacheType: RepositoryCacheType = 'local' + ): RepoCache { + switch (cacheType) { + case 'local': + return new RepoCacheLocal(repository); + default: + logger.warn( + { cacheType }, + `Repository cache type not supported using type "local" instead` + ); + return new RepoCacheLocal(repository); + } + } +} diff --git a/lib/util/cache/repository/impl/local.spec.ts b/lib/util/cache/repository/impl/local.spec.ts index 38ec21c00a..a3ab46953c 100644 --- a/lib/util/cache/repository/impl/local.spec.ts +++ b/lib/util/cache/repository/impl/local.spec.ts @@ -3,9 +3,11 @@ import zlib from 'zlib'; import hasha from 'hasha'; import { fs } from '../../../../../test/util'; import { GlobalConfig } from '../../../../config/global'; +import { logger } from '../../../../logger'; import { CACHE_REVISION } from '../common'; import type { RepoCacheData, RepoCacheRecord } from '../types'; -import { LocalRepoCache } from './local'; +import { CacheFactory } from './cache-factory'; +import { RepoCacheLocal } from './local'; jest.mock('../../../fs'); @@ -20,17 +22,16 @@ async function createCacheRecord( const hash = hasha(jsonStr, { algorithm: 'sha256' }); const compressed = await compress(jsonStr); const payload = compressed.toString('base64'); - const record: RepoCacheRecord = { revision, repository, payload, hash }; - return record; + return { revision, repository, payload, hash }; } describe('util/cache/repository/impl/local', () => { beforeEach(() => { - GlobalConfig.set({ cacheDir: '/tmp/cache' }); + GlobalConfig.set({ cacheDir: '/tmp/cache', platform: 'github' }); }); it('returns empty object before any data load', () => { - const localRepoCache = new LocalRepoCache('github', 'some/repo'); + const localRepoCache = CacheFactory.get('some/repo', 'local'); expect(localRepoCache.getData()).toBeEmpty(); }); @@ -38,7 +39,7 @@ describe('util/cache/repository/impl/local', () => { const data: RepoCacheData = { semanticCommits: 'enabled' }; const cacheRecord = await createCacheRecord(data); fs.readCacheFile.mockResolvedValue(JSON.stringify(cacheRecord)); - const localRepoCache = new LocalRepoCache('github', 'some/repo'); + const localRepoCache = CacheFactory.get('some/repo', 'local'); await localRepoCache.load(); @@ -53,7 +54,7 @@ describe('util/cache/repository/impl/local', () => { semanticCommits: 'enabled', }) ); - const localRepoCache = new LocalRepoCache('github', 'some/repo'); + const localRepoCache = CacheFactory.get('some/repo', 'local'); await localRepoCache.load(); await localRepoCache.save(); @@ -73,7 +74,7 @@ describe('util/cache/repository/impl/local', () => { data: { semanticCommits: 'enabled' }, }) ); - const localRepoCache = new LocalRepoCache('github', 'some/repo'); + const localRepoCache = CacheFactory.get('some/repo', 'local'); await localRepoCache.load(); await localRepoCache.save(); @@ -94,7 +95,7 @@ describe('util/cache/repository/impl/local', () => { }) ); - const localRepoCache = new LocalRepoCache('github', 'some/repo'); + const localRepoCache = CacheFactory.get('some/repo', 'local'); await localRepoCache.load(); expect(localRepoCache.getData()).toBeEmpty(); @@ -102,7 +103,7 @@ describe('util/cache/repository/impl/local', () => { it('handles invalid data', async () => { fs.readCacheFile.mockResolvedValue(JSON.stringify({ foo: 'bar' })); - const localRepoCache = new LocalRepoCache('github', 'some/repo'); + const localRepoCache = CacheFactory.get('some/repo', 'local'); await localRepoCache.load(); @@ -111,7 +112,7 @@ describe('util/cache/repository/impl/local', () => { it('handles file read error', async () => { fs.readCacheFile.mockRejectedValue(new Error('unknown error')); - const localRepoCache = new LocalRepoCache('github', 'some/repo'); + const localRepoCache = CacheFactory.get('some/repo', 'local'); await localRepoCache.load(); @@ -123,7 +124,7 @@ describe('util/cache/repository/impl/local', () => { const cacheRecord = createCacheRecord({ semanticCommits: 'enabled' }); fs.readCacheFile.mockResolvedValueOnce(JSON.stringify(cacheRecord)); - const localRepoCache = new LocalRepoCache('github', 'some/repo'); + const localRepoCache = CacheFactory.get('some/repo', 'local'); await localRepoCache.load(); expect(localRepoCache.getData()).toEqual({}); @@ -131,9 +132,9 @@ describe('util/cache/repository/impl/local', () => { it('saves modified cache data to file', async () => { const oldCacheRecord = createCacheRecord({ semanticCommits: 'enabled' }); + const cacheType = 'protocol://domain/path'; fs.readCacheFile.mockResolvedValueOnce(JSON.stringify(oldCacheRecord)); - const localRepoCache = new LocalRepoCache('github', 'some/repo'); - + const localRepoCache = CacheFactory.get('some/repo', cacheType); await localRepoCache.load(); const data = localRepoCache.getData(); data.semanticCommits = 'disabled'; @@ -142,6 +143,11 @@ describe('util/cache/repository/impl/local', () => { const newCacheRecord = await createCacheRecord({ semanticCommits: 'disabled', }); + expect(localRepoCache instanceof RepoCacheLocal).toBeTrue(); + expect(logger.warn).toHaveBeenCalledWith( + { cacheType }, + `Repository cache type not supported using type "local" instead` + ); expect(fs.outputCacheFile).toHaveBeenCalledWith( '/tmp/cache/renovate/repository/github/some/repo.json', JSON.stringify(newCacheRecord) diff --git a/lib/util/cache/repository/impl/local.ts b/lib/util/cache/repository/impl/local.ts index c3edec3e56..6315cc78fb 100644 --- a/lib/util/cache/repository/impl/local.ts +++ b/lib/util/cache/repository/impl/local.ts @@ -1,86 +1,38 @@ -import { promisify } from 'util'; -import zlib from 'zlib'; -import hasha from 'hasha'; import upath from 'upath'; import { GlobalConfig } from '../../../../config/global'; import { logger } from '../../../../logger'; import { outputCacheFile, readCacheFile } from '../../../fs'; -import { - CACHE_REVISION, - isValidRev10, - isValidRev11, - isValidRev12, -} from '../common'; import type { RepoCacheRecord } from '../types'; import { RepoCacheBase } from './base'; -const compress = promisify(zlib.brotliCompress); -const decompress = promisify(zlib.brotliDecompress); - -export class LocalRepoCache extends RepoCacheBase { - private oldHash: string | null = null; - - constructor(private platform: string, private repository: string) { - super(); +export class RepoCacheLocal extends RepoCacheBase { + constructor(repository: string) { + super(repository); } - public getCacheFileName(): string { - const cacheDir = GlobalConfig.get('cacheDir'); - const repoCachePath = '/renovate/repository/'; - const platform = this.platform; - const fileName = `${this.repository}.json`; - return upath.join(cacheDir, repoCachePath, platform, fileName); - } - - override async load(): Promise<void> { + protected async read(): Promise<string | undefined> { const cacheFileName = this.getCacheFileName(); + let data: string | undefined; try { - const cacheFileName = this.getCacheFileName(); const rawCache = await readCacheFile(cacheFileName, 'utf8'); - const oldCache = JSON.parse(rawCache); - - if (isValidRev12(oldCache, this.repository)) { - const compressed = Buffer.from(oldCache.payload, 'base64'); - const uncompressed = await decompress(compressed); - const jsonStr = uncompressed.toString('utf8'); - this.data = JSON.parse(jsonStr); - this.oldHash = oldCache.hash; - logger.debug('Repository cache is valid'); - return; - } - - if (isValidRev11(oldCache, this.repository)) { - this.data = oldCache.data; - logger.debug('Repository cache is migrated from 11 revision'); - return; - } - - if (isValidRev10(oldCache, this.repository)) { - delete oldCache.repository; - delete oldCache.revision; - this.data = oldCache; - logger.debug('Repository cache is migrated from 10 revision'); - return; - } - - logger.debug('Repository cache is invalid'); + data = JSON.parse(rawCache); } catch (err) { - logger.debug({ cacheFileName }, 'Repository cache not found'); + logger.debug({ cacheFileName }, 'Repository local cache not found'); + throw err; } + return data; } - override async save(): Promise<void> { + protected async write(data: RepoCacheRecord): Promise<void> { const cacheFileName = this.getCacheFileName(); - const revision = CACHE_REVISION; - const repository = this.repository; - const data = this.getData(); - const jsonStr = JSON.stringify(data); - const hash = await hasha.async(jsonStr, { algorithm: 'sha256' }); - if (hash !== this.oldHash) { - const compressed = await compress(jsonStr); - const payload = compressed.toString('base64'); - const record: RepoCacheRecord = { revision, repository, payload, hash }; - await outputCacheFile(cacheFileName, JSON.stringify(record)); - } + await outputCacheFile(cacheFileName, JSON.stringify(data)); + } + + private getCacheFileName(): string { + const cacheDir = GlobalConfig.get('cacheDir'); + const repoCachePath = '/renovate/repository/'; + const platform = this.platform; + const fileName = `${this.repository}.json`; + return upath.join(cacheDir, repoCachePath, platform, fileName); } } diff --git a/lib/util/cache/repository/impl/null.ts b/lib/util/cache/repository/impl/null.ts new file mode 100644 index 0000000000..99e06623c8 --- /dev/null +++ b/lib/util/cache/repository/impl/null.ts @@ -0,0 +1,19 @@ +import type { RepoCache, RepoCacheData } from '../types'; + +export class RepoCacheNull implements RepoCache { + private data: RepoCacheData = {}; + + // istanbul ignore next + load(): Promise<void> { + return Promise.resolve(); + } + + // istanbul ignore next + save(): Promise<void> { + return Promise.resolve(); + } + + getData(): RepoCacheData { + return this.data; + } +} diff --git a/lib/util/cache/repository/index.ts b/lib/util/cache/repository/index.ts index 2a2ea539b5..1fc4553892 100644 --- a/lib/util/cache/repository/index.ts +++ b/lib/util/cache/repository/index.ts @@ -1,10 +1,12 @@ -import { RepoCacheBase } from './impl/base'; +import { RepoCacheNull } from './impl/null'; import type { RepoCache, RepoCacheData } from './types'; -let repoCache: RepoCache = new RepoCacheBase(); +// This will be overwritten with initRepoCache() +// Used primarily as a placeholder and for testing +let repoCache: RepoCache = new RepoCacheNull(); export function resetCache(): void { - setCache(new RepoCacheBase()); + setCache(new RepoCacheNull()); } export function setCache(cache: RepoCache): void { diff --git a/lib/util/cache/repository/init.ts b/lib/util/cache/repository/init.ts index dbef05bd91..be2999d0b3 100644 --- a/lib/util/cache/repository/init.ts +++ b/lib/util/cache/repository/init.ts @@ -1,6 +1,6 @@ -import { GlobalConfig } from '../../../config/global'; import type { RenovateConfig } from '../../../config/types'; -import { LocalRepoCache } from './impl/local'; +import { CacheFactory } from './impl/cache-factory'; +import { RepoCacheNull } from './impl/null'; import { resetCache, setCache } from '.'; /** @@ -9,24 +9,24 @@ import { resetCache, setCache } from '.'; export async function initRepoCache(config: RenovateConfig): Promise<void> { resetCache(); - const { platform } = GlobalConfig.get(); - const { repository, repositoryCache } = config; + const { repository, repositoryCache, repositoryCacheType: type } = config; - if (repositoryCache === 'disabled' || !platform || !repository) { + if (repositoryCache === 'disabled') { + setCache(new RepoCacheNull()); return; } if (repositoryCache === 'enabled') { - const localCache = new LocalRepoCache(platform, repository); - await localCache.load(); - setCache(localCache); + const cache = CacheFactory.get(repository!, type); + await cache.load(); + setCache(cache); return; } if (repositoryCache === 'reset') { - const localCache = new LocalRepoCache(platform, repository); - await localCache.save(); - setCache(localCache); + const cache = CacheFactory.get(repository!, type); + await cache.save(); + setCache(cache); return; } } -- GitLab