diff --git a/lib/manager/kustomize/__fixtures__/gitImages.yaml b/lib/manager/kustomize/__fixtures__/gitImages.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1b04dc9975a4f28b0c2943c95a546c0136a9c493 --- /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 0000000000000000000000000000000000000000..9f4194050cab7fd86f9c99a7191f6f012cb6d37f --- /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 0000000000000000000000000000000000000000..23d42adae1c5a766ed316858039784a4d40ff266 --- /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 0000000000000000000000000000000000000000..0c00d6fb774c78d8c175400d017a7255525b327d --- /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 0000000000000000000000000000000000000000..cea2d09d13b466ffd7190f873e5a5e2dc6c044a7 --- /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 0000000000000000000000000000000000000000..62c35fd2c73e7c81f6b8195058f3ddc357272503 --- /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 0000000000000000000000000000000000000000..c8aa2ddfc02aa05e6d0de4b39b82d8400bcf28cb --- /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 0000000000000000000000000000000000000000..1bc1afdc3223b122a592f6220e3f19f6866f88f8 --- /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 0000000000000000000000000000000000000000..746ff1bb9890220c9a13db5cd191d74b15a8c4b1 --- /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 0000000000000000000000000000000000000000..9ba1f06b2a559957c57231d978ea031f528c6adf --- /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 0000000000000000000000000000000000000000..69f3f5a3b4861c38fd3dfdf9ba167b51e006c122 --- /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 0000000000000000000000000000000000000000..ce5bdde2f02366fd82c355698b184f7f85ca540c --- /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 0000000000000000000000000000000000000000..33fef75469d6ea608db5b7caa59f7d492863c217 --- /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 +```