Skip to content

Commit

Permalink
feat: helmfile manager (#5257)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmelillo authored Feb 2, 2020
1 parent cd8dbd4 commit 30f0c42
Show file tree
Hide file tree
Showing 14 changed files with 655 additions and 1 deletion.
2 changes: 2 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,8 @@ 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.

## helmfile

## homebrew

## hostRules
Expand Down
15 changes: 15 additions & 0 deletions lib/config/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,21 @@ const options: RenovateOptions[] = [
mergeable: true,
cli: false,
},
{
name: 'helmfile',
description: 'Configuration object for helmfile helmfile.yaml files.',
stage: 'package',
type: 'object',
default: {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
commitMessageTopic: 'helm chart {{depName}}',
fileMatch: ['(^|/)helmfile.yaml$'],
},
mergeable: true,
cli: false,
},
{
name: 'circleci',
description:
Expand Down
1 change: 1 addition & 0 deletions lib/constants/managers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const MANAGER_GO_MOD = 'gomod';
export const MANAGER_GRADLE = 'gradle';
export const MANAGER_GRADLE_WRAPPER = 'gradle-wrapper';
export const MANAGER_HELM_REQUIREMENTS = 'helm-requirements';
export const MANAGER_HELMFILE = 'helmfile';
export const MANAGER_HOMEBREW = 'homebrew';
export const MANAGER_KUBERNETES = 'kubernetes';
export const MANAGER_LEININGEN = 'leiningen';
Expand Down
85 changes: 85 additions & 0 deletions lib/manager/helmfile/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import is from '@sindresorhus/is';
import yaml from 'js-yaml';

import { logger } from '../../logger';
import { PackageFile, PackageDependency, ExtractConfig } from '../common';

const isValidChartName = (name: string): boolean => {
return name.match(/[!@#$%^&*(),.?":{}/|<>A-Z]/) === null;
};

export function extractPackageFile(
content: string,
fileName: string,
config: ExtractConfig
): PackageFile {
let deps = [];
let doc;
const aliases: Record<string, string> = {};
try {
doc = yaml.safeLoad(content, { json: true });
} catch (err) {
logger.debug({ err, fileName }, 'Failed to parse helmfile helmfile.yaml');
return null;
}
if (!(doc && is.array(doc.releases))) {
logger.debug({ fileName }, 'helmfile.yaml has no releases');
return null;
}

if (doc.repositories) {
for (let i = 0; i < doc.repositories.length; i += 1) {
aliases[doc.repositories[i].name] = doc.repositories[i].url;
}
}
logger.debug({ aliases }, 'repositories discovered.');

deps = doc.releases.map(dep => {
let depName = dep.chart;
let repoName = null;

// If starts with ./ is for sure a local path
if (dep.chart.startsWith('./')) {
return {
depName,
skipReason: 'local-chart',
} as PackageDependency;
}

if (dep.chart.includes('/')) {
const v = dep.chart.split('/');
repoName = v.shift();
depName = v.join('/');
} else {
repoName = dep.chart;
}

const res: PackageDependency = {
depName,
currentValue: dep.version,
registryUrls: [aliases[repoName]]
.concat([config.aliases[repoName]])
.filter(Boolean),
};

// If version is null is probably a local chart
if (!res.currentValue) {
res.skipReason = 'local-chart';
}

// By definition on helm the chart name should be lowecase letter + number + -
// However helmfile support templating of that field
if (!isValidChartName(res.depName)) {
res.skipReason = 'unsupported-chart-type';
}

// Skip in case we cannot locate the registry
if (is.emptyArray(res.registryUrls)) {
res.skipReason = 'unknown-registry';
}

return res;
});

return { deps, datasource: 'helm' } as PackageFile;
}
2 changes: 2 additions & 0 deletions lib/manager/helmfile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { extractPackageFile } from './extract';
export { updateDependency } from './update';
74 changes: 74 additions & 0 deletions lib/manager/helmfile/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import _ from 'lodash';
import yaml from 'js-yaml';
import is from '@sindresorhus/is';

import { logger } from '../../logger';
import { Upgrade } from '../common';

// Return true if the match string is found at index in content
function matchAt(content: string, index: number, match: string): boolean {
return content.substring(index, index + match.length) === match;
}

// Replace oldString with newString at location index of content
function replaceAt(
content: string,
index: number,
oldString: string,
newString: string
): string {
logger.debug(`Replacing ${oldString} with ${newString} at index ${index}`);
return (
content.substr(0, index) +
newString +
content.substr(index + oldString.length)
);
}

export function updateDependency(
fileContent: string,
upgrade: Upgrade
): string | null {
logger.trace({ config: upgrade }, 'updateDependency()');
if (!upgrade || !upgrade.depName || !upgrade.newValue) {
logger.debug('Failed to update dependency, invalid upgrade');
return fileContent;
}
const doc = yaml.safeLoad(fileContent, { json: true });
if (!doc || !is.array(doc.releases)) {
logger.debug('Failed to update dependency, invalid helmfile.yaml file');
return fileContent;
}
const { depName, newValue } = upgrade;
const oldVersion = doc.releases.filter(
dep => dep.chart.split('/')[1] === depName
)[0].version;
doc.releases = doc.releases.map(dep =>
dep.chart.split('/')[1] === depName ? { ...dep, version: newValue } : dep
);
const searchString = `${oldVersion}`;
const newString = `${newValue}`;
let newFileContent = fileContent;

let searchIndex = newFileContent.indexOf('releases') + 'releases'.length;
for (; searchIndex < newFileContent.length; searchIndex += 1) {
// First check if we have a hit for the old version
if (matchAt(newFileContent, searchIndex, searchString)) {
logger.trace(`Found match at index ${searchIndex}`);
// Now test if the result matches
newFileContent = replaceAt(
newFileContent,
searchIndex,
searchString,
newString
);
}
}
// Compare the parsed yaml structure of old and new
if (!_.isEqual(doc, yaml.safeLoad(newFileContent, { json: true }))) {
logger.trace(`Mismatched replace: ${newFileContent}`);
newFileContent = fileContent;
}

return newFileContent;
}
2 changes: 2 additions & 0 deletions lib/manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
MANAGER_GRADLE,
MANAGER_GRADLE_WRAPPER,
MANAGER_HELM_REQUIREMENTS,
MANAGER_HELMFILE,
MANAGER_HOMEBREW,
MANAGER_KUBERNETES,
MANAGER_LEININGEN,
Expand Down Expand Up @@ -84,6 +85,7 @@ const managerList = [
MANAGER_GRADLE,
MANAGER_GRADLE_WRAPPER,
MANAGER_HELM_REQUIREMENTS,
MANAGER_HELMFILE,
MANAGER_HOMEBREW,
MANAGER_KUBERNETES,
MANAGER_LEININGEN,
Expand Down
12 changes: 12 additions & 0 deletions renovate-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,18 @@
},
"$ref": "#"
},
"helmfile": {
"description": "Configuration object for helmfile helmfile.yaml files.",
"type": "object",
"default": {
"aliases": {
"stable": "https://kubernetes-charts.storage.googleapis.com/"
},
"commitMessageTopic": "helm chart {{depName}}",
"fileMatch": ["(^|/)helmfile.yaml$"]
},
"$ref": "#"
},
"circleci": {
"description": "Configuration object for CircleCI yml renovation. Also inherits settings from `docker` object.",
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion test/config/__snapshots__/validation.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Array [
"depName": "Configuration Error",
"message": "packageRules:
You have included an unsupported manager in a package rule. Your list: foo.
Supported managers are: (ansible, bazel, buildkite, bundler, cargo, cdnurl, circleci, composer, deps-edn, docker-compose, dockerfile, droneci, git-submodules, github-actions, gitlabci, gitlabci-include, gomod, gradle, gradle-wrapper, helm-requirements, homebrew, kubernetes, leiningen, maven, meteor, mix, npm, nuget, nvm, pip_requirements, pip_setup, pipenv, poetry, pub, sbt, swift, terraform, travis, ruby-version).",
Supported managers are: (ansible, bazel, buildkite, bundler, cargo, cdnurl, circleci, composer, deps-edn, docker-compose, dockerfile, droneci, git-submodules, github-actions, gitlabci, gitlabci-include, gomod, gradle, gradle-wrapper, helm-requirements, helmfile, homebrew, kubernetes, leiningen, maven, meteor, mix, npm, nuget, nvm, pip_requirements, pip_setup, pipenv, poetry, pub, sbt, swift, terraform, travis, ruby-version).",
},
]
`;
Expand Down
104 changes: 104 additions & 0 deletions test/manager/helmfile/__snapshots__/extract.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`lib/manager/helmfile/extract extractPackageFile() skip chart that does not have specified version 1`] = `
Object {
"datasource": "helm",
"deps": Array [
Object {
"currentValue": undefined,
"depName": "example",
"registryUrls": Array [
"https://kubernetes-charts.storage.googleapis.com/",
],
"skipReason": "local-chart",
},
],
}
`;

exports[`lib/manager/helmfile/extract extractPackageFile() skip chart with special character in the name 1`] = `
Object {
"datasource": "helm",
"deps": Array [
Object {
"currentValue": "1.0.0",
"depName": "example/example",
"registryUrls": Array [
"https://kiwigrid.github.io",
],
"skipReason": "unsupported-chart-type",
},
Object {
"currentValue": "1.0.0",
"depName": "example?example",
"registryUrls": Array [
"https://kiwigrid.github.io",
],
"skipReason": "unsupported-chart-type",
},
],
}
`;

exports[`lib/manager/helmfile/extract extractPackageFile() skip chart with unknown repository 1`] = `
Object {
"datasource": "helm",
"deps": Array [
Object {
"currentValue": "1.0.0",
"depName": "example",
"registryUrls": Array [],
"skipReason": "unknown-registry",
},
],
}
`;

exports[`lib/manager/helmfile/extract extractPackageFile() skip if repository details are not specified 1`] = `
Object {
"datasource": "helm",
"deps": Array [
Object {
"currentValue": "1.0.0",
"depName": "example",
"registryUrls": Array [],
"skipReason": "unknown-registry",
},
],
}
`;

exports[`lib/manager/helmfile/extract extractPackageFile() skip local charts 1`] = `
Object {
"datasource": "helm",
"deps": Array [
Object {
"depName": "./charts/example",
"skipReason": "local-chart",
},
],
}
`;

exports[`lib/manager/helmfile/extract extractPackageFile() skip templetized release with invalid characters 1`] = `
Object {
"datasource": "helm",
"deps": Array [
Object {
"currentValue": "1.0.0",
"depName": "{{\`{{ .Release.Name }}\`}}",
"registryUrls": Array [
"https://kubernetes-charts.storage.googleapis.com/",
],
"skipReason": "unsupported-chart-type",
},
Object {
"currentValue": "1.0.0",
"depName": "example",
"registryUrls": Array [
"https://kubernetes-charts.storage.googleapis.com/",
],
},
],
}
`;
46 changes: 46 additions & 0 deletions test/manager/helmfile/__snapshots__/update.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`lib/manager/helmfile/extract updateDependency() upgrades dependency if chart is repeated 1`] = `
"
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: fluentd-elasticsearch-internal
version: 5.3.1
chart: kiwigrid/fluentd-elasticsearch
- name: nginx-ingress
version: 1.3.0
chart: stable/nginx-ingress
- name: fluentd-elasticsearch-external
version: 5.3.1
chart: kiwigrid/fluentd-elasticsearch
"
`;

exports[`lib/manager/helmfile/extract updateDependency() upgrades dependency if valid upgrade 1`] = `
"
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: fluentd-elasticsearch
version: 5.3.1
chart: kiwigrid/fluentd-elasticsearch
"
`;

exports[`lib/manager/helmfile/extract updateDependency() upgrades dependency if version field comes before name field 1`] = `
"
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- version: 5.3.1
name: fluentd-elasticsearch
chart: kiwigrid/fluentd-elasticsearch
"
`;
Loading

0 comments on commit 30f0c42

Please sign in to comment.