Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: helmfile manager #5257

Merged
merged 11 commits into from
Feb 2, 2020
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