From 208a316c39f645bf8bbb6037878c61aaee4c1239 Mon Sep 17 00:00:00 2001 From: Marcel <34819524+MarcelCoding@users.noreply.github.com> Date: Wed, 7 Sep 2022 07:10:27 +0200 Subject: [PATCH] feat: woodpecker manager (#17297) Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> Co-authored-by: Rhys Arkins <rhys@arkins.net> --- lib/config/options/index.ts | 1 + lib/modules/manager/api.ts | 2 + .../woodpecker/__fixtures__/.woodpecker.yml | 24 +++ .../manager/woodpecker/extract.spec.ts | 188 ++++++++++++++++++ lib/modules/manager/woodpecker/extract.ts | 48 +++++ lib/modules/manager/woodpecker/index.ts | 13 ++ lib/modules/manager/woodpecker/readme.md | 7 + lib/modules/manager/woodpecker/types.ts | 7 + 8 files changed, 290 insertions(+) create mode 100644 lib/modules/manager/woodpecker/__fixtures__/.woodpecker.yml create mode 100644 lib/modules/manager/woodpecker/extract.spec.ts create mode 100644 lib/modules/manager/woodpecker/extract.ts create mode 100644 lib/modules/manager/woodpecker/index.ts create mode 100644 lib/modules/manager/woodpecker/readme.md create mode 100644 lib/modules/manager/woodpecker/types.ts diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index a486d67625..2dac6716d2 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -849,6 +849,7 @@ const options: RenovateOptions[] = [ 'kubernetes', 'ansible', 'droneci', + 'woodpecker', ], }, { diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 4a3fe2bf5c..84b74fe6a2 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -74,6 +74,7 @@ import * as terragruntVersion from './terragrunt-version'; import * as travis from './travis'; import type { ManagerApi } from './types'; import * as velaci from './velaci'; +import * as woodpecker from './woodpecker'; const api = new Map<string, ManagerApi>(); export default api; @@ -153,3 +154,4 @@ api.set('terragrunt', terragrunt); api.set('terragrunt-version', terragruntVersion); api.set('travis', travis); api.set('velaci', velaci); +api.set('woodpecker', woodpecker); diff --git a/lib/modules/manager/woodpecker/__fixtures__/.woodpecker.yml b/lib/modules/manager/woodpecker/__fixtures__/.woodpecker.yml new file mode 100644 index 0000000000..aaa5c46759 --- /dev/null +++ b/lib/modules/manager/woodpecker/__fixtures__/.woodpecker.yml @@ -0,0 +1,24 @@ +pipeline: + redis: + image: quay.io/something/redis:alpine + + worker: + image: "node:10.0.0" + + db: + image: "postgres:9.4.0" + + vote: + image: dockersamples/examplevotingapp_vote:before + + result: + image: 'dockersamples/examplevotingapp_result:before' + + votingworker: + image: dockersamples/examplevotingapp_worker + + visualizer: + image: dockersamples/visualizer:stable + + debugapp: + image: app-local-debug diff --git a/lib/modules/manager/woodpecker/extract.spec.ts b/lib/modules/manager/woodpecker/extract.spec.ts new file mode 100644 index 0000000000..7a6faf476b --- /dev/null +++ b/lib/modules/manager/woodpecker/extract.spec.ts @@ -0,0 +1,188 @@ +import { Fixtures } from '../../../../test/fixtures'; +import { extractPackageFile } from '.'; + +const yamlFile = Fixtures.get('.woodpecker.yml'); + +describe('modules/manager/woodpecker/extract', () => { + describe('extractPackageFile()', () => { + it('returns null for empty', () => { + expect(extractPackageFile('', '', {})).toBeNull(); + }); + + it('returns null for non-object YAML', () => { + expect(extractPackageFile('nothing here', '', {})).toBeNull(); + }); + + it('returns null for malformed YAML', () => { + expect(extractPackageFile('nothing here\n:::::::', '', {})).toBeNull(); + }); + + it('extracts multiple image lines', () => { + const res = extractPackageFile(yamlFile, '', {}); + expect(res).toEqual({ + deps: [ + { + depName: 'quay.io/something/redis', + currentValue: 'alpine', + currentDigest: undefined, + replaceString: 'quay.io/something/redis:alpine', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + { + depName: 'node', + currentValue: '10.0.0', + currentDigest: undefined, + replaceString: 'node:10.0.0', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + { + depName: 'postgres', + currentValue: '9.4.0', + currentDigest: undefined, + replaceString: 'postgres:9.4.0', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + { + depName: 'dockersamples/examplevotingapp_vote', + currentValue: 'before', + currentDigest: undefined, + replaceString: 'dockersamples/examplevotingapp_vote:before', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + { + depName: 'dockersamples/examplevotingapp_result', + currentValue: 'before', + currentDigest: undefined, + replaceString: 'dockersamples/examplevotingapp_result:before', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + { + depName: 'dockersamples/examplevotingapp_worker', + currentValue: undefined, + currentDigest: undefined, + replaceString: 'dockersamples/examplevotingapp_worker', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + { + depName: 'dockersamples/visualizer', + currentValue: 'stable', + currentDigest: undefined, + replaceString: 'dockersamples/visualizer:stable', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + { + depName: 'app-local-debug', + currentValue: undefined, + currentDigest: undefined, + replaceString: 'app-local-debug', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + ], + }); + }); + + it('extracts image and replaces registry', () => { + const res = extractPackageFile( + ` + pipeline: + nginx: + image: quay.io/nginx:0.0.1 + `, + '', + { + registryAliases: { + 'quay.io': 'my-quay-mirror.registry.com', + }, + } + ); + expect(res).toEqual({ + deps: [ + { + autoReplaceStringTemplate: + 'quay.io/nginx:{{#if newValue}}{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentDigest: undefined, + currentValue: '0.0.1', + datasource: 'docker', + depName: 'my-quay-mirror.registry.com/nginx', + replaceString: 'quay.io/nginx:0.0.1', + }, + ], + }); + }); + + it('extracts image but no replacement', () => { + const res = extractPackageFile( + ` + pipeline: + nginx: + image: quay.io/nginx:0.0.1 + `, + '', + { + registryAliases: { + 'index.docker.io': 'my-docker-mirror.registry.com', + }, + } + ); + expect(res).toEqual({ + deps: [ + { + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentDigest: undefined, + currentValue: '0.0.1', + datasource: 'docker', + depName: 'quay.io/nginx', + replaceString: 'quay.io/nginx:0.0.1', + }, + ], + }); + }); + + it('extracts image and no double replacement', () => { + const res = extractPackageFile( + ` + pipeline: + nginx: + image: quay.io/nginx:0.0.1 + `, + '', + { + registryAliases: { + 'quay.io': 'my-quay-mirror.registry.com', + 'my-quay-mirror.registry.com': 'quay.io', + }, + } + ); + expect(res).toEqual({ + deps: [ + { + autoReplaceStringTemplate: + 'quay.io/nginx:{{#if newValue}}{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentDigest: undefined, + currentValue: '0.0.1', + datasource: 'docker', + depName: 'my-quay-mirror.registry.com/nginx', + replaceString: 'quay.io/nginx:0.0.1', + }, + ], + }); + }); + }); +}); diff --git a/lib/modules/manager/woodpecker/extract.ts b/lib/modules/manager/woodpecker/extract.ts new file mode 100644 index 0000000000..f41691ff73 --- /dev/null +++ b/lib/modules/manager/woodpecker/extract.ts @@ -0,0 +1,48 @@ +import is from '@sindresorhus/is'; +import { load } from 'js-yaml'; +import { logger } from '../../../logger'; +import { getDep } from '../dockerfile/extract'; +import type { ExtractConfig, PackageFile } from '../types'; +import type { WoodpeckerConfig } from './types'; + +export function extractPackageFile( + content: string, + fileName: string, + extractConfig: ExtractConfig +): PackageFile | null { + logger.debug('woodpecker.extractPackageFile()'); + let config: WoodpeckerConfig; + try { + // TODO: fix me (#9610) + config = load(content, { json: true }) as WoodpeckerConfig; + if (!config) { + logger.debug( + { fileName }, + 'Null config when parsing Woodpecker Configuration content' + ); + return null; + } + if (typeof config !== 'object') { + logger.debug( + { fileName, type: typeof config }, + 'Unexpected type for Woodpecker Configuration content' + ); + return null; + } + } catch (err) { + logger.debug( + { fileName, err }, + 'Error parsing Woodpecker Configuration config YAML' + ); + return null; + } + + // Image name/tags for services are only eligible for update if they don't + // use variables and if the image is not built locally + const deps = Object.values(config.pipeline ?? {}) + .filter((step) => is.string(step?.image)) + .map((step) => getDep(step.image, true, extractConfig.registryAliases)); + + logger.trace({ deps }, 'Woodpecker Configuration image'); + return deps.length ? { deps } : null; +} diff --git a/lib/modules/manager/woodpecker/index.ts b/lib/modules/manager/woodpecker/index.ts new file mode 100644 index 0000000000..12b959e5de --- /dev/null +++ b/lib/modules/manager/woodpecker/index.ts @@ -0,0 +1,13 @@ +import { ProgrammingLanguage } from '../../../constants'; +import { DockerDatasource } from '../../datasource/docker'; +import { extractPackageFile } from './extract'; + +export const language = ProgrammingLanguage.Docker; + +export { extractPackageFile }; + +export const defaultConfig = { + fileMatch: ['(^|\\/)\\.woodpecker[^/]*\\.ya?ml$'], +}; + +export const supportedDatasources = [DockerDatasource.id]; diff --git a/lib/modules/manager/woodpecker/readme.md b/lib/modules/manager/woodpecker/readme.md new file mode 100644 index 0000000000..b7d0aa6e82 --- /dev/null +++ b/lib/modules/manager/woodpecker/readme.md @@ -0,0 +1,7 @@ +Extracts all Docker images from Woodpecker Pipeline YAML files. + +- [Woodpecker homepage](https://woodpecker-ci.org/) +- [Woodpecker Docs: Pipeline Syntax](https://woodpecker-ci.org/docs/usage/pipeline-syntax) ([section with dependencies](https://woodpecker-ci.org/docs/usage/pipeline-syntax#image)) +- [`woodpecker-ci` JSON schema](https://raw.githubusercontent.com/woodpecker-ci/woodpecker/master/pipeline/schema/schema.json) + +If you need to change the versioning format, read the [versioning](https://docs.renovatebot.com/modules/versioning/) documentation to learn more. diff --git a/lib/modules/manager/woodpecker/types.ts b/lib/modules/manager/woodpecker/types.ts new file mode 100644 index 0000000000..aced9644f6 --- /dev/null +++ b/lib/modules/manager/woodpecker/types.ts @@ -0,0 +1,7 @@ +export type WoodpeckerConfig = { + pipeline?: Record<string, WoodpeckerStep>; +}; + +export interface WoodpeckerStep { + image?: string; +} -- GitLab