From fe482f91c5d6c92695c16fbae0deb28c0d3a3bca Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@arkins.net> Date: Thu, 5 Mar 2020 12:49:54 +0100 Subject: [PATCH] feat(manager): support kustomize (#5484) --- .../kustomize/__fixtures__/gitImages.yaml | 17 ++ .../kustomize/__fixtures__/gitSshBase.yaml | 10 + .../kustomize/__fixtures__/gitSubdir.yaml | 10 + .../__fixtures__/kustomizeEmpty.yaml | 5 + .../kustomize/__fixtures__/kustomizeHttp.yaml | 10 + .../__fixtures__/kustomizeWithLocal.yaml | 12 + .../__fixtures__/service-1/kustomization.yaml | 8 + .../kustomize/__fixtures__/service.yaml | 10 + .../__snapshots__/extract.spec.ts.snap | 80 +++++++ lib/manager/kustomize/extract.spec.ts | 213 ++++++++++++++++++ lib/manager/kustomize/extract.ts | 112 +++++++++ lib/manager/kustomize/index.ts | 7 + lib/manager/kustomize/readme.md | 28 +++ 13 files changed, 522 insertions(+) create mode 100644 lib/manager/kustomize/__fixtures__/gitImages.yaml create mode 100644 lib/manager/kustomize/__fixtures__/gitSshBase.yaml create mode 100644 lib/manager/kustomize/__fixtures__/gitSubdir.yaml create mode 100644 lib/manager/kustomize/__fixtures__/kustomizeEmpty.yaml create mode 100644 lib/manager/kustomize/__fixtures__/kustomizeHttp.yaml create mode 100644 lib/manager/kustomize/__fixtures__/kustomizeWithLocal.yaml create mode 100644 lib/manager/kustomize/__fixtures__/service-1/kustomization.yaml create mode 100644 lib/manager/kustomize/__fixtures__/service.yaml create mode 100644 lib/manager/kustomize/__snapshots__/extract.spec.ts.snap create mode 100644 lib/manager/kustomize/extract.spec.ts create mode 100644 lib/manager/kustomize/extract.ts create mode 100644 lib/manager/kustomize/index.ts create mode 100644 lib/manager/kustomize/readme.md diff --git a/lib/manager/kustomize/__fixtures__/gitImages.yaml b/lib/manager/kustomize/__fixtures__/gitImages.yaml new file mode 100644 index 0000000000..1b04dc9975 --- /dev/null +++ b/lib/manager/kustomize/__fixtures__/gitImages.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: testing-namespace + +resources: +- deployment.yaml + +images: +- name: node + newTag: v0.1.0 +- newTag: v0.0.1 + name: group/instance +- name: quay.io/test/repo + newTag: v0.0.2 +- name: gitlab.com/org/suborg/image + newTag: v0.0.3 diff --git a/lib/manager/kustomize/__fixtures__/gitSshBase.yaml b/lib/manager/kustomize/__fixtures__/gitSshBase.yaml new file mode 100644 index 0000000000..9f4194050c --- /dev/null +++ b/lib/manager/kustomize/__fixtures__/gitSshBase.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: + - git@github.com:moredhel/remote-kustomize.git?ref=v0.0.1 + +namespace: testing-namespace + +resources: + - deployment.yaml diff --git a/lib/manager/kustomize/__fixtures__/gitSubdir.yaml b/lib/manager/kustomize/__fixtures__/gitSubdir.yaml new file mode 100644 index 0000000000..23d42adae1 --- /dev/null +++ b/lib/manager/kustomize/__fixtures__/gitSubdir.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: +- git@github.com:kubernetes-sigs/kustomize.git//examples/helloWorld?ref=v2.0.0 + +namespace: testing-namespace + +resources: +- deployment.yaml diff --git a/lib/manager/kustomize/__fixtures__/kustomizeEmpty.yaml b/lib/manager/kustomize/__fixtures__/kustomizeEmpty.yaml new file mode 100644 index 0000000000..0c00d6fb77 --- /dev/null +++ b/lib/manager/kustomize/__fixtures__/kustomizeEmpty.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- deployment.yaml diff --git a/lib/manager/kustomize/__fixtures__/kustomizeHttp.yaml b/lib/manager/kustomize/__fixtures__/kustomizeHttp.yaml new file mode 100644 index 0000000000..cea2d09d13 --- /dev/null +++ b/lib/manager/kustomize/__fixtures__/kustomizeHttp.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: +- github.com/user/repo//deploy?ref=v0.0.1 + +namespace: testing-namespace + +resources: +- deployment.yaml diff --git a/lib/manager/kustomize/__fixtures__/kustomizeWithLocal.yaml b/lib/manager/kustomize/__fixtures__/kustomizeWithLocal.yaml new file mode 100644 index 0000000000..62c35fd2c7 --- /dev/null +++ b/lib/manager/kustomize/__fixtures__/kustomizeWithLocal.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: +- service-1 +- https://moredhel/remote-kustomize.git?ref=v0.0.1 +- https://moredhel/remote-kustomize.git//deploy?ref=v0.0.1 + +namespace: testing-namespace + +resources: +- deployment.yaml diff --git a/lib/manager/kustomize/__fixtures__/service-1/kustomization.yaml b/lib/manager/kustomize/__fixtures__/service-1/kustomization.yaml new file mode 100644 index 0000000000..c8aa2ddfc0 --- /dev/null +++ b/lib/manager/kustomize/__fixtures__/service-1/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: testing-namespace + +images: +- name: moredhel/renovate-test-1 + newTag: v0.0.1 diff --git a/lib/manager/kustomize/__fixtures__/service.yaml b/lib/manager/kustomize/__fixtures__/service.yaml new file mode 100644 index 0000000000..1bc1afdc32 --- /dev/null +++ b/lib/manager/kustomize/__fixtures__/service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: sample-service +spec: + ports: + - port: 80 + protocol: TCP + targetPort: http + name: http diff --git a/lib/manager/kustomize/__snapshots__/extract.spec.ts.snap b/lib/manager/kustomize/__snapshots__/extract.spec.ts.snap new file mode 100644 index 0000000000..746ff1bb98 --- /dev/null +++ b/lib/manager/kustomize/__snapshots__/extract.spec.ts.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`manager/kustomize/extract extractPackageFile() extracts http dependency 1`] = ` +Array [ + Object { + "currentValue": "v0.0.1", + "datasource": "git-tags", + "depName": "github.com/user/repo//deploy", + "lookupName": "github.com/user/repo", + }, +] +`; + +exports[`manager/kustomize/extract extractPackageFile() extracts multiple image lines 1`] = ` +Array [ + Object { + "currentValue": "v0.0.1", + "datasource": "git-tags", + "depName": "https://moredhel/remote-kustomize.git", + "lookupName": "https://moredhel/remote-kustomize.git", + }, + Object { + "currentValue": "v0.0.1", + "datasource": "git-tags", + "depName": "https://moredhel/remote-kustomize.git//deploy", + "lookupName": "https://moredhel/remote-kustomize.git//deploy", + }, +] +`; + +exports[`manager/kustomize/extract extractPackageFile() extracts ssh dependency 1`] = ` +Array [ + Object { + "currentValue": "v0.0.1", + "datasource": "git-tags", + "depName": "git@github.com:moredhel/remote-kustomize.git", + "lookupName": "git@github.com:moredhel/remote-kustomize.git", + }, +] +`; + +exports[`manager/kustomize/extract extractPackageFile() extracts ssh dependency with a subdir 1`] = ` +Array [ + Object { + "currentValue": "v2.0.0", + "datasource": "git-tags", + "depName": "git@github.com:kubernetes-sigs/kustomize.git//examples/helloWorld", + "lookupName": "git@github.com:kubernetes-sigs/kustomize.git", + }, +] +`; + +exports[`manager/kustomize/extract extractPackageFile() should extract out image versions 1`] = ` +Array [ + Object { + "currentValue": "v0.1.0", + "datasource": "docker", + "depName": "node", + "lookupName": "node", + }, + Object { + "currentValue": "v0.0.1", + "datasource": "docker", + "depName": "group/instance", + "lookupName": "group/instance", + }, + Object { + "currentValue": "v0.0.2", + "datasource": "docker", + "depName": "quay.io/test/repo", + "lookupName": "quay.io/test/repo", + }, + Object { + "currentValue": "v0.0.3", + "datasource": "docker", + "depName": "gitlab.com/org/suborg/image", + "lookupName": "gitlab.com/org/suborg/image", + }, +] +`; diff --git a/lib/manager/kustomize/extract.spec.ts b/lib/manager/kustomize/extract.spec.ts new file mode 100644 index 0000000000..9ba1f06b2a --- /dev/null +++ b/lib/manager/kustomize/extract.spec.ts @@ -0,0 +1,213 @@ +import { readFileSync } from 'fs'; +import { + extractBase, + extractImage, + parseKustomize, + extractPackageFile, +} from './extract'; +import * as datasourceDocker from '../../datasource/docker'; +import * as datasourceGitTags from '../../datasource/git-tags'; + +const kustomizeGitSSHBase = readFileSync( + 'lib/manager/kustomize/__fixtures__/gitSshBase.yaml', + 'utf8' +); + +const kustomizeEmpty = readFileSync( + 'lib/manager/kustomize/__fixtures__/kustomizeEmpty.yaml', + 'utf8' +); + +const kustomizeGitSSHSubdir = readFileSync( + 'lib/manager/kustomize/__fixtures__/gitSubdir.yaml', + 'utf8' +); + +const kustomizeHTTP = readFileSync( + 'lib/manager/kustomize/__fixtures__/kustomizeHttp.yaml', + 'utf8' +); + +const kustomizeWithLocal = readFileSync( + 'lib/manager/kustomize/__fixtures__/kustomizeWithLocal.yaml', + 'utf8' +); + +const nonKustomize = readFileSync( + 'lib/manager/kustomize/__fixtures__/service.yaml', + 'utf8' +); + +const gitImages = readFileSync( + 'lib/manager/kustomize/__fixtures__/gitImages.yaml', + 'utf8' +); + +describe('manager/kustomize/extract', () => { + it('should successfully parse a valid kustomize file', () => { + const file = parseKustomize(kustomizeGitSSHBase); + expect(file).not.toBeNull(); + }); + it('return null on an invalid file', () => { + const file = parseKustomize(''); + expect(file).toBeNull(); + }); + describe('extractBase', () => { + it('should return null for a local base ', () => { + const res = extractBase('./service-1'); + expect(res).toBeNull(); + }); + it('should extract out the version of an http base', () => { + const base = 'https://github.com/user/test-repo.git'; + const version = 'v1.0.0'; + const sample = { + currentValue: version, + datasource: datasourceGitTags.id, + depName: base, + lookupName: base, + }; + + const pkg = extractBase(`${base}?ref=${version}`); + expect(pkg).toEqual(sample); + }); + it('should extract out the version of a git base', () => { + const base = 'git@github.com:user/repo.git'; + const version = 'v1.0.0'; + const sample = { + currentValue: version, + datasource: datasourceGitTags.id, + depName: base, + lookupName: base, + }; + + const pkg = extractBase(`${base}?ref=${version}`); + expect(pkg).toEqual(sample); + }); + it('should extract out the version of a git base with subdir', () => { + const base = 'git@github.com:user/repo.git'; + const version = 'v1.0.0'; + const sample = { + currentValue: version, + datasource: datasourceGitTags.id, + depName: `${base}//subdir`, + lookupName: base, + }; + + const pkg = extractBase(`${sample.depName}?ref=${version}`); + expect(pkg).toEqual(sample); + }); + }); + describe('image extraction', () => { + it('should return null on a null input', () => { + const pkg = extractImage({ + name: null, + newTag: null, + }); + expect(pkg).toEqual(null); + }); + it('should correctly extract a default image', () => { + const sample = { + currentValue: 'v1.0.0', + datasource: datasourceDocker.id, + depName: 'node', + lookupName: 'node', + }; + const pkg = extractImage({ + name: sample.lookupName, + newTag: sample.currentValue, + }); + expect(pkg).toEqual(sample); + }); + it('should correctly extract an image in a repo', () => { + const sample = { + currentValue: 'v1.0.0', + datasource: datasourceDocker.id, + depName: 'test/node', + lookupName: 'test/node', + }; + const pkg = extractImage({ + name: sample.lookupName, + newTag: sample.currentValue, + }); + expect(pkg).toEqual(sample); + }); + it('should correctly extract from a different registry', () => { + const sample = { + currentValue: 'v1.0.0', + datasource: datasourceDocker.id, + depName: 'quay.io/repo/image', + lookupName: 'quay.io/repo/image', + }; + const pkg = extractImage({ + name: sample.lookupName, + newTag: sample.currentValue, + }); + expect(pkg).toEqual(sample); + }); + it('should correctly extract from a different port', () => { + const sample = { + currentValue: 'v1.0.0', + datasource: datasourceDocker.id, + depName: 'localhost:5000/repo/image', + lookupName: 'localhost:5000/repo/image', + }; + const pkg = extractImage({ + name: sample.lookupName, + newTag: sample.currentValue, + }); + expect(pkg).toEqual(sample); + }); + it('should correctly extract from a multi-depth registry', () => { + const sample = { + currentValue: 'v1.0.0', + datasource: datasourceDocker.id, + depName: 'localhost:5000/repo/image/service', + lookupName: 'localhost:5000/repo/image/service', + }; + const pkg = extractImage({ + name: sample.lookupName, + newTag: sample.currentValue, + }); + expect(pkg).toEqual(sample); + }); + }); + describe('extractPackageFile()', () => { + it('returns null for non kustomize kubernetes files', () => { + expect(extractPackageFile(nonKustomize)).toBeNull(); + }); + it('extracts multiple image lines', () => { + const res = extractPackageFile(kustomizeWithLocal); + expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(2); + }); + it('extracts ssh dependency', () => { + const res = extractPackageFile(kustomizeGitSSHBase); + expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(1); + }); + it('extracts ssh dependency with a subdir', () => { + const res = extractPackageFile(kustomizeGitSSHSubdir); + expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(1); + }); + it('extracts http dependency', () => { + const res = extractPackageFile(kustomizeHTTP); + expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(1); + expect(res.deps[0].currentValue).toEqual('v0.0.1'); + }); + it('should extract out image versions', () => { + const res = extractPackageFile(gitImages); + expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(4); + expect(res.deps[0].currentValue).toEqual('v0.1.0'); + expect(res.deps[1].currentValue).toEqual('v0.0.1'); + }); + it('ignores non-Kubernetes empty files', () => { + expect(extractPackageFile('')).toBeNull(); + }); + it('does nothing with kustomize empty kustomize files', () => { + expect(extractPackageFile(kustomizeEmpty)).toBeNull(); + }); + }); +}); diff --git a/lib/manager/kustomize/extract.ts b/lib/manager/kustomize/extract.ts new file mode 100644 index 0000000000..69f3f5a3b4 --- /dev/null +++ b/lib/manager/kustomize/extract.ts @@ -0,0 +1,112 @@ +import { safeLoad } from 'js-yaml'; +import { PackageFile, PackageDependency } from '../common'; +import { logger } from '../../logger'; +import * as datasourceDocker from '../../datasource/docker'; +import * as datasourceGitTags from '../../datasource/git-tags'; + +interface Image { + name: string; + newTag: string; +} + +interface Kustomize { + kind: string; + bases: string[]; + images: Image[]; +} + +// extract the version from the url +const versionMatch = /(?<basename>.*)\?ref=(?<version>.*)\s*$/; + +// extract the url from the base of a url with a subdir +const extractUrl = /^(?<url>.*)(?:\/\/.*)$/; + +export function extractBase(base: string): PackageDependency | null { + const basenameVersion = versionMatch.exec(base); + if (basenameVersion) { + const currentValue = basenameVersion.groups.version; + const root = basenameVersion.groups.basename; + + const urlResult = extractUrl.exec(root); + let url = root; + // if a match, then there was a subdir, update + if (urlResult && !url.startsWith('http')) { + url = urlResult.groups.url; + } + + return { + datasource: datasourceGitTags.id, + depName: root, + lookupName: url, + currentValue, + }; + } + + return null; +} + +export function extractImage(image: Image): PackageDependency | null { + if (image && image.name && image.newTag) { + return { + datasource: datasourceDocker.id, + depName: image.name, + lookupName: image.name, + currentValue: image.newTag, + }; + } + + return null; +} + +export function parseKustomize(content: string): Kustomize | null { + let pkg = null; + try { + pkg = safeLoad(content); + } catch (e) /* istanbul ignore next */ { + return null; + } + + if (!pkg) { + return null; + } + + if (pkg.kind !== 'Kustomization') { + return null; + } + + pkg.bases = pkg.bases || []; + pkg.images = pkg.images || []; + + return pkg; +} + +export function extractPackageFile(content: string): PackageFile | null { + logger.trace('kustomize.extractPackageFile()'); + const deps: PackageDependency[] = []; + + const pkg = parseKustomize(content); + if (!pkg) { + return null; + } + + // grab the remote bases + for (const base of pkg.bases) { + const dep = extractBase(base); + if (dep) { + deps.push(dep); + } + } + + // grab the image tags + for (const image of pkg.images) { + const dep = extractImage(image); + if (dep) { + deps.push(dep); + } + } + + if (!deps.length) { + return null; + } + return { deps }; +} diff --git a/lib/manager/kustomize/index.ts b/lib/manager/kustomize/index.ts new file mode 100644 index 0000000000..ce5bdde2f0 --- /dev/null +++ b/lib/manager/kustomize/index.ts @@ -0,0 +1,7 @@ +export { extractPackageFile } from './extract'; + +export const autoReplace = true; + +export const defaultConfig = { + fileMatch: ['(^|/)kustomization\\.yaml'], +}; diff --git a/lib/manager/kustomize/readme.md b/lib/manager/kustomize/readme.md new file mode 100644 index 0000000000..33fef75469 --- /dev/null +++ b/lib/manager/kustomize/readme.md @@ -0,0 +1,28 @@ +This package will manage two parts of the `kustomization.yaml` file: + +1. [remote bases](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md) +2. [image tags](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/image.md) + +**How It Works** + +1. Renovate will search each repository for any `kustomization.yaml` files. +2. Existing dependencies will be extracted from remote bases & image tags +3. Renovate will resolve the dependency's source repository and check for semver tags if found. +4. If an update was found, Renovate will update `kustomization.yaml` + +**Limitations** + +- Currently this hasn't been tested using https to fetch the repos +- the image tags are limited to the following formats: + +``` +- name: image/name + newTag: v0.0.1 +``` + +or + +``` +- newTag: v0.0.1 + name: image/name +``` -- GitLab