Skip to content

Commit

Permalink
feat: helm-values manager (#5134)
Browse files Browse the repository at this point in the history
  • Loading branch information
dominik-horb-umg committed Feb 4, 2020
1 parent 515a70b commit edf85d4
Show file tree
Hide file tree
Showing 18 changed files with 582 additions and 9 deletions.
13 changes: 13 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions lib/config/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
1 change: 1 addition & 0 deletions lib/manager/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
60 changes: 60 additions & 0 deletions lib/manager/helm-values/extract.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions lib/manager/helm-values/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { extractPackageFile } from './extract';
export { updateDependency } from './update';
122 changes: 122 additions & 0 deletions lib/manager/helm-values/update.ts
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 41 additions & 0 deletions lib/manager/helm-values/util.ts
Original file line number Diff line number Diff line change
@@ -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)
);
}
9 changes: 9 additions & 0 deletions renovate-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 [
Expand All @@ -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 [
Expand All @@ -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 [
Expand Down Expand Up @@ -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 [
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/manager/helm-requirements/extract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion test/manager/helm-requirements/update.spec.ts
Original file line number Diff line number Diff line change
@@ -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 = `
Expand Down

0 comments on commit edf85d4

Please sign in to comment.