feat: helm-values manager (#5134)

This commit is contained in:
Dominik Horb 2020-02-04 15:11:08 +01:00 committed by GitHub
parent 515a70b206
commit edf85d42bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 582 additions and 9 deletions

View file

@ -483,6 +483,19 @@ Note: you shouldn't usually need to configure this unless you really care about
Renovate supports updating Helm Chart references within `requirements.yaml` files. If your Helm charts make use of Aliases then you will need to configure an `aliases` object in your config to tell Renovate where to look for them.
## helm-values
Renovate supports updating of Docker dependencies within Helm Chart `values.yaml` files or other YAML
files that use the same format (via `fileMatch` configuration). Updates are performed if the files
follow the conventional format used in most of the `stable` Helm charts:
```yaml
image:
repository: 'some-docker/dependency'
tag: v1.0.0
registry: registry.example.com # optional key, will default to "docker.io"
```
## helmfile
## homebrew

View file

@ -1753,6 +1753,18 @@ const options: RenovateOptions[] = [
mergeable: true,
cli: false,
},
{
name: 'helm-values',
description: 'Configuration object for helm values.yaml files.',
stage: 'package',
type: 'object',
default: {
commitMessageTopic: 'helm values {{depName}}',
fileMatch: ['(^|/)values.yaml$'],
},
mergeable: true,
cli: false,
},
{
name: 'helmfile',
description: 'Configuration object for helmfile helmfile.yaml files.',

View file

@ -148,6 +148,7 @@ export interface Upgrade<T = Record<string, any>>
checksumUrl?: string;
currentVersion?: string;
depGroup?: string;
dockerRepository?: string;
downloadUrl?: string;
localDir?: string;
name?: string;

View file

@ -0,0 +1,60 @@
import yaml from 'js-yaml';
import { logger } from '../../logger';
import { getDep } from '../dockerfile/extract';
import { PackageFile, PackageDependency } from '../common';
import {
matchesHelmValuesDockerHeuristic,
HelmDockerImageDependency,
} from './util';
/**
* Recursively find all supported dependencies in the yaml object.
*
* @param parsedContent
*/
function findDependencies(
parsedContent: object | HelmDockerImageDependency,
packageDependencies: Array<PackageDependency>
): Array<PackageDependency> {
if (!parsedContent || typeof parsedContent !== 'object') {
return packageDependencies;
}
Object.keys(parsedContent).forEach(key => {
if (matchesHelmValuesDockerHeuristic(key, parsedContent[key])) {
const currentItem = parsedContent[key];
const registry = currentItem.registry ? `${currentItem.registry}/` : '';
packageDependencies.push(
getDep(`${registry}${currentItem.repository}:${currentItem.tag}`)
);
} else {
findDependencies(parsedContent[key], packageDependencies);
}
});
return packageDependencies;
}
export function extractPackageFile(content: string): PackageFile {
try {
// a parser that allows extracting line numbers would be preferable, with
// the current approach we need to match anything we find again during the update
const parsedContent = yaml.safeLoad(content);
logger.debug(
{ parsedContent },
'Trying to find dependencies in helm-values'
);
const deps = findDependencies(parsedContent, []);
if (deps.length) {
logger.debug({ deps }, 'Found dependencies in helm-values');
return { deps };
}
} catch (err) {
logger.error({ err }, 'Failed to parse helm-values file');
}
return null;
}

View file

@ -0,0 +1,2 @@
export { extractPackageFile } from './extract';
export { updateDependency } from './update';

View file

@ -0,0 +1,122 @@
import YAWN from 'yawn-yaml/cjs';
import { logger } from '../../logger';
import { Upgrade } from '../common';
import {
matchesHelmValuesDockerHeuristic,
HelmDockerImageDependency,
} from './util';
function shouldUpdate(
parentKey: string,
data: unknown | HelmDockerImageDependency,
dockerRepository: string,
currentValue: string,
originalRegistryValue: string
): boolean {
return (
matchesHelmValuesDockerHeuristic(parentKey, data) &&
data.repository === dockerRepository &&
data.tag === currentValue &&
((!data.registry && !originalRegistryValue) ||
data.registry === originalRegistryValue)
);
}
/**
* Extract the originally set registry value if it is included in the depName.
*/
function getOriginalRegistryValue(
depName: string,
dockerRepository: string
): string {
if (depName.length > dockerRepository.length) {
return depName.substring(0, depName.lastIndexOf(dockerRepository) - 1);
}
return '';
}
/**
* Recursive function that walks the yaml strucuture
* and updates the first match of an 'image' key it finds,
* if it adheres to the supported structure.
*
* @param parsedContent The part of the yaml tree we should look at.
* @param dockerRepository The docker repository that should be updated.
* @param currentValue The current version that should be updated.
* @param newValue The update version that should be set instead of currentValue.
* @returns True if the parsedContent was updated, false otherwise.
*/
function updateDoc(
parsedContent: object | HelmDockerImageDependency,
dockerRepository: string,
currentValue: string,
newValue: string,
originalRegistryValue: string
): boolean {
for (const key of Object.keys(parsedContent)) {
if (
shouldUpdate(
key,
parsedContent[key],
dockerRepository,
currentValue,
originalRegistryValue
)
) {
// the next statement intentionally updates the passed in parameter
// with the updated dependency value
// eslint-disable-next-line no-param-reassign
parsedContent[key].tag = newValue;
return true;
}
if (typeof parsedContent[key] === 'object') {
const foundMatch = updateDoc(
parsedContent[key],
dockerRepository,
currentValue,
newValue,
originalRegistryValue
);
if (foundMatch) {
return true;
}
}
}
return false;
}
export function updateDependency(
fileContent: string,
upgrade: Upgrade
): string | null {
if (
!upgrade ||
!upgrade.depName ||
!upgrade.newValue ||
!upgrade.currentValue ||
!upgrade.dockerRepository
) {
logger.debug('Failed to update dependency, invalid upgrade');
return fileContent;
}
const yawn = new YAWN(fileContent);
const doc = yawn.json;
const originalRegistryValue = getOriginalRegistryValue(
upgrade.depName,
upgrade.dockerRepository
);
updateDoc(
doc,
upgrade.dockerRepository,
upgrade.currentValue,
upgrade.newValue,
originalRegistryValue
);
yawn.json = doc;
return yawn.yaml;
}

View file

@ -0,0 +1,41 @@
export type HelmDockerImageDependency = {
registry?: string;
repository: string;
tag: string;
};
/**
* This is a workaround helper to allow the usage of 'unknown' in
* a type-guard function while checking that keys exist.
*
* @see https://github.com/microsoft/TypeScript/issues/21732
* @see https://stackoverflow.com/a/58630274
*/
function hasKey<K extends string>(k: K, o: {}): o is { [_ in K]: {} } {
return typeof o === 'object' && k in o;
}
/**
* Type guard to determine whether a given partial Helm values.yaml object potentially
* defines a Helm Docker dependency.
*
* There is no exact standard of how Docker dependencies are defined in Helm
* values.yaml files (as of January 1st 2020), this function defines a
* heuristic based on the most commonly used format in the stable Helm charts:
*
* image:
* repository: 'something'
* tag: v1.0.0
*/
export function matchesHelmValuesDockerHeuristic(
parentKey: string,
data: unknown
): data is HelmDockerImageDependency {
return (
parentKey === 'image' &&
data &&
typeof data === 'object' &&
hasKey('repository', data) &&
hasKey('tag', data)
);
}

View file

@ -1165,6 +1165,15 @@
},
"$ref": "#"
},
"helm-values": {
"description": "Configuration object for helm values.yaml files.",
"type": "object",
"default": {
"commitMessageTopic": "helm values {{depName}}",
"fileMatch": ["(^|/)values.yaml$"]
},
"$ref": "#"
},
"helmfile": {
"description": "Configuration object for helmfile helmfile.yaml files.",
"type": "object",

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/manager/helm/extract extractPackageFile() parses simple requirements.yaml correctly 1`] = `
exports[`lib/manager/helm-requirements/extract extractPackageFile() parses simple requirements.yaml correctly 1`] = `
Object {
"datasource": "helm",
"deps": Array [
@ -22,7 +22,7 @@ Object {
}
`;
exports[`lib/manager/helm/extract extractPackageFile() resolves aliased registry urls 1`] = `
exports[`lib/manager/helm-requirements/extract extractPackageFile() resolves aliased registry urls 1`] = `
Object {
"datasource": "helm",
"deps": Array [
@ -37,7 +37,7 @@ Object {
}
`;
exports[`lib/manager/helm/extract extractPackageFile() skips invalid registry urls 1`] = `
exports[`lib/manager/helm-requirements/extract extractPackageFile() skips invalid registry urls 1`] = `
Object {
"datasource": "helm",
"deps": Array [
@ -66,7 +66,7 @@ Object {
}
`;
exports[`lib/manager/helm/extract extractPackageFile() skips local dependencies 1`] = `
exports[`lib/manager/helm-requirements/extract extractPackageFile() skips local dependencies 1`] = `
Object {
"datasource": "helm",
"deps": Array [

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/manager/helm/extract updateDependency() upgrades dependency if newValue version value is repeated 1`] = `
exports[`lib/manager/helm-requirements/update updateDependency() upgrades dependency if newValue version value is repeated 1`] = `
"
dependencies:
- version: 0.9.0
@ -12,7 +12,7 @@ exports[`lib/manager/helm/extract updateDependency() upgrades dependency if newV
"
`;
exports[`lib/manager/helm/extract updateDependency() upgrades dependency if valid upgrade 1`] = `
exports[`lib/manager/helm-requirements/update updateDependency() upgrades dependency if valid upgrade 1`] = `
"
dependencies:
- name: redis
@ -24,7 +24,7 @@ exports[`lib/manager/helm/extract updateDependency() upgrades dependency if vali
"
`;
exports[`lib/manager/helm/extract updateDependency() upgrades dependency if version field comes before name field 1`] = `
exports[`lib/manager/helm-requirements/update updateDependency() upgrades dependency if version field comes before name field 1`] = `
"
dependencies:
- version: 0.11.0

View file

@ -3,7 +3,7 @@ import { platform as _platform } from '../../../lib/platform';
const platform: any = _platform;
describe('lib/manager/helm/extract', () => {
describe('lib/manager/helm-requirements/extract', () => {
describe('extractPackageFile()', () => {
beforeEach(() => {
jest.resetAllMocks();

View file

@ -1,6 +1,6 @@
import { updateDependency } from '../../../lib/manager/helm-requirements/update';
describe('lib/manager/helm/extract', () => {
describe('lib/manager/helm-requirements/update', () => {
describe('updateDependency()', () => {
it('returns the same fileContent for undefined upgrade', () => {
const content = `

View file

@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/manager/helm-values/extract extractPackageFile() extracts from complex values file correctly" 1`] = `
Object {
"deps": Array [
Object {
"currentDigest": undefined,
"currentValue": "11.6.0-debian-9-r0",
"datasource": "docker",
"depName": "bitnami/postgresql",
},
Object {
"currentDigest": undefined,
"currentValue": "0.7.0-debian-9-r12",
"datasource": "docker",
"depName": "docker.io/bitnami/postgres-exporter",
},
Object {
"currentDigest": undefined,
"currentValue": "11.5.0-debian-9-r0",
"datasource": "docker",
"depName": "docker.io/bitnami/postgresql",
},
],
}
`;
exports[`lib/manager/helm-values/extract extractPackageFile() extracts from values.yaml correctly with same structure as "helm create" 1`] = `
Object {
"deps": Array [
Object {
"currentDigest": undefined,
"currentValue": "1.16.1",
"datasource": "docker",
"depName": "nginx",
},
],
}
`;

View file

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/manager/helm-values/update updateDependency() upgrades correct dependency if registry included 1`] = `
"
db:
image:
image:
repository: bitnami/postgresql
tag: 11.6.0-debian-9-r0
some-non-image-related-key: 'with-some-value'
warehouse:
image:
registry: docker.io
repository: bitnami/postgresql
tag: 12.5.0
some-non-image-related-key: 'with-some-value'
"
`;
exports[`lib/manager/helm-values/update updateDependency() upgrades dependency if newValue version value is repeated 1`] = `
"
db:
image:
image:
registry: docker.io
repository: bitnami/postgresql
tag: 12.5.0
some-non-image-related-key: 'with-some-value'
warehouse:
image:
registry: docker.io
repository: bitnami/postgresql
tag: 11.6.0-debian-9-r0
some-non-image-related-key: 'with-some-value'
"
`;
exports[`lib/manager/helm-values/update updateDependency() upgrades dependency if valid upgrade 1`] = `
"
image:
repository: bitnami/postgres-exporter
tag: 0.8.0
"
`;

View file

@ -0,0 +1,68 @@
# Default values for test-chart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: 1.16.1
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 80
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths: []
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

View file

@ -0,0 +1,21 @@
---
api:
image:
image:
repository: bitnami/postgresql
tag: 11.6.0-debian-9-r0
some-non-image-related-key: 'with-some-value'
# https://github.com/helm/charts/blob/c5838636973a5546196db6e48ae46f99a55900c4/stable/postgresql/values.yaml#L426
metrics:
image:
registry: docker.io
repository: bitnami/postgres-exporter
tag: 0.7.0-debian-9-r12
pullPolicy: IfNotPresent
someOtherKey:
- image:
registry: docker.io
repository: bitnami/postgresql
tag: 11.5.0-debian-9-r0
some-non-image-related-key: 'with-some-value'

View file

@ -0,0 +1,41 @@
import { readFileSync } from 'fs';
import { extractPackageFile } from '../../../lib/manager/helm-values/extract';
const helmDefaultChartInitValues = readFileSync(
'test/manager/helm-values/_fixtures/default_chart_init_values.yaml',
'utf8'
);
const helmMultiAndNestedImageValues = readFileSync(
'test/manager/helm-values/_fixtures/multi_and_nested_image_values.yaml',
'utf8'
);
describe('lib/manager/helm-values/extract', () => {
describe('extractPackageFile()', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('returns null for invalid yaml file content', () => {
const result = extractPackageFile('nothing here: [');
expect(result).toBeNull();
});
it('returns null for empty yaml file content', () => {
const result = extractPackageFile('');
expect(result).toBeNull();
});
it('returns null for no file content', () => {
const result = extractPackageFile(null);
expect(result).toBeNull();
});
it('extracts from values.yaml correctly with same structure as "helm create"', () => {
const result = extractPackageFile(helmDefaultChartInitValues);
expect(result).toMatchSnapshot();
});
it('extracts from complex values file correctly"', () => {
const result = extractPackageFile(helmMultiAndNestedImageValues);
expect(result).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,100 @@
import { updateDependency } from '../../../lib/manager/helm-values/update';
describe('lib/manager/helm-values/update', () => {
describe('updateDependency()', () => {
it('returns the same fileContent for undefined upgrade', () => {
const content = 'someKey: "someValue"';
const upgrade = undefined;
expect(updateDependency(content, upgrade)).toBe(content);
});
it('returns the same fileContent for invalid values.yaml file', () => {
const content = `
Invalid values.yaml content.
`;
const upgrade = {
depName: 'bitnami/postgres-exporter',
currentValue: '0.7.0-debian-9-r12',
datasource: 'docker',
newValue: '0.8.0',
dockerRepository: 'bitnami/postgres-exporter',
};
expect(updateDependency(content, upgrade)).toBe(content);
});
it('returns the same fileContent for empty upgrade', () => {
const content = 'someKey: "someValue"';
const upgrade = {};
expect(updateDependency(content, upgrade)).toBe(content);
});
it('returns the same fileContent for null content', () => {
const content = null;
const upgrade = {};
expect(updateDependency(content, upgrade)).toBe(content);
});
it('upgrades dependency if valid upgrade', () => {
const content = `
image:
repository: bitnami/postgres-exporter
tag: 0.7.0-debian-9-r12
`;
const upgrade = {
depName: 'bitnami/postgres-exporter',
currentValue: '0.7.0-debian-9-r12',
newValue: '0.8.0',
dockerRepository: 'bitnami/postgres-exporter',
};
expect(updateDependency(content, upgrade)).not.toBe(content);
expect(updateDependency(content, upgrade)).toMatchSnapshot();
});
it('upgrades dependency if newValue version value is repeated', () => {
const content = `
db:
image:
image:
registry: docker.io
repository: bitnami/postgresql
tag: 11.6.0-debian-9-r0
some-non-image-related-key: 'with-some-value'
warehouse:
image:
registry: docker.io
repository: bitnami/postgresql
tag: 11.6.0-debian-9-r0
some-non-image-related-key: 'with-some-value'
`;
const upgrade = {
depName: 'docker.io/bitnami/postgresql',
currentValue: '11.6.0-debian-9-r0',
newValue: '12.5.0',
dockerRepository: 'bitnami/postgresql',
};
expect(updateDependency(content, upgrade)).not.toBe(content);
expect(updateDependency(content, upgrade)).toMatchSnapshot();
});
it('upgrades correct dependency if registry included', () => {
const content = `
db:
image:
image:
repository: bitnami/postgresql
tag: 11.6.0-debian-9-r0
some-non-image-related-key: 'with-some-value'
warehouse:
image:
registry: docker.io
repository: bitnami/postgresql
tag: 11.6.0-debian-9-r0
some-non-image-related-key: 'with-some-value'
`;
const upgrade = {
depName: 'docker.io/bitnami/postgresql',
currentValue: '11.6.0-debian-9-r0',
newValue: '12.5.0',
dockerRepository: 'bitnami/postgresql',
};
expect(updateDependency(content, upgrade)).not.toBe(content);
expect(updateDependency(content, upgrade)).toMatchSnapshot();
});
});
});