Skip to content
Snippets Groups Projects
Unverified Commit 30f0c423 authored by Gabriel Melillo's avatar Gabriel Melillo Committed by GitHub
Browse files

feat: helmfile manager (#5257)

parent cd8dbd4c
Branches
No related tags found
No related merge requests found
Showing
with 655 additions and 1 deletion
...@@ -483,6 +483,8 @@ Note: you shouldn't usually need to configure this unless you really care about ...@@ -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. 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 ## homebrew
## hostRules ## hostRules
......
...@@ -1714,6 +1714,21 @@ const options: RenovateOptions[] = [ ...@@ -1714,6 +1714,21 @@ const options: RenovateOptions[] = [
mergeable: true, mergeable: true,
cli: false, 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', name: 'circleci',
description: description:
......
...@@ -18,6 +18,7 @@ export const MANAGER_GO_MOD = 'gomod'; ...@@ -18,6 +18,7 @@ export const MANAGER_GO_MOD = 'gomod';
export const MANAGER_GRADLE = 'gradle'; export const MANAGER_GRADLE = 'gradle';
export const MANAGER_GRADLE_WRAPPER = 'gradle-wrapper'; export const MANAGER_GRADLE_WRAPPER = 'gradle-wrapper';
export const MANAGER_HELM_REQUIREMENTS = 'helm-requirements'; export const MANAGER_HELM_REQUIREMENTS = 'helm-requirements';
export const MANAGER_HELMFILE = 'helmfile';
export const MANAGER_HOMEBREW = 'homebrew'; export const MANAGER_HOMEBREW = 'homebrew';
export const MANAGER_KUBERNETES = 'kubernetes'; export const MANAGER_KUBERNETES = 'kubernetes';
export const MANAGER_LEININGEN = 'leiningen'; export const MANAGER_LEININGEN = 'leiningen';
......
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;
}
export { extractPackageFile } from './extract';
export { updateDependency } from './update';
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;
}
...@@ -42,6 +42,7 @@ import { ...@@ -42,6 +42,7 @@ import {
MANAGER_GRADLE, MANAGER_GRADLE,
MANAGER_GRADLE_WRAPPER, MANAGER_GRADLE_WRAPPER,
MANAGER_HELM_REQUIREMENTS, MANAGER_HELM_REQUIREMENTS,
MANAGER_HELMFILE,
MANAGER_HOMEBREW, MANAGER_HOMEBREW,
MANAGER_KUBERNETES, MANAGER_KUBERNETES,
MANAGER_LEININGEN, MANAGER_LEININGEN,
...@@ -84,6 +85,7 @@ const managerList = [ ...@@ -84,6 +85,7 @@ const managerList = [
MANAGER_GRADLE, MANAGER_GRADLE,
MANAGER_GRADLE_WRAPPER, MANAGER_GRADLE_WRAPPER,
MANAGER_HELM_REQUIREMENTS, MANAGER_HELM_REQUIREMENTS,
MANAGER_HELMFILE,
MANAGER_HOMEBREW, MANAGER_HOMEBREW,
MANAGER_KUBERNETES, MANAGER_KUBERNETES,
MANAGER_LEININGEN, MANAGER_LEININGEN,
......
...@@ -1123,6 +1123,18 @@ ...@@ -1123,6 +1123,18 @@
}, },
"$ref": "#" "$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": { "circleci": {
"description": "Configuration object for CircleCI yml renovation. Also inherits settings from `docker` object.", "description": "Configuration object for CircleCI yml renovation. Also inherits settings from `docker` object.",
"type": "object", "type": "object",
......
...@@ -96,7 +96,7 @@ Array [ ...@@ -96,7 +96,7 @@ Array [
"depName": "Configuration Error", "depName": "Configuration Error",
"message": "packageRules: "message": "packageRules:
You have included an unsupported manager in a package rule. Your list: foo. 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).",
}, },
] ]
`; `;
......
// 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/",
],
},
],
}
`;
// 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
"
`;
import { extractPackageFile } from '../../../lib/manager/helmfile/extract';
describe('lib/manager/helmfile/extract', () => {
describe('extractPackageFile()', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('returns null if no releases', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
});
expect(result).toBeNull();
});
it('do not crash on invalid helmfile.yaml', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases: [
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
});
expect(result).toBeNull();
});
it('skip if repository details are not specified', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: experimental/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
});
expect(result).not.toBeNull();
expect(result).toMatchSnapshot();
expect(result.deps.every(dep => dep.skipReason));
});
it('skip templetized release with invalid characters', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: stable/{{\`{{ .Release.Name }}\`}}
- name: example-internal
version: 1.0.0
chart: stable/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
});
expect(result).not.toBeNull();
expect(result).toMatchSnapshot();
});
it('skip local charts', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: ./charts/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
});
expect(result).not.toBeNull();
expect(result).toMatchSnapshot();
expect(result.deps.every(dep => dep.skipReason));
});
it('skip chart with unknown repository', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
});
expect(result).not.toBeNull();
expect(result).toMatchSnapshot();
expect(result.deps.every(dep => dep.skipReason));
});
it('skip chart with special character in the name', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
version: 1.0.0
chart: kiwigrid/example/example
- name: example2
version: 1.0.0
chart: kiwigrid/example?example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
});
expect(result).not.toBeNull();
expect(result).toMatchSnapshot();
expect(result.deps.every(dep => dep.skipReason));
});
it('skip chart that does not have specified version', async () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: example
chart: stable/example
`;
const fileName = 'helmfile.yaml';
const result = await extractPackageFile(content, fileName, {
aliases: {
stable: 'https://kubernetes-charts.storage.googleapis.com/',
},
});
expect(result).not.toBeNull();
expect(result).toMatchSnapshot();
expect(result.deps.every(dep => dep.skipReason));
});
});
});
import { updateDependency } from '../../../lib/manager/helmfile/update';
describe('lib/manager/helmfile/extract', () => {
describe('updateDependency()', () => {
it('returns the same fileContent for undefined upgrade', () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: fluentd-elasticsearch
version: 5.3.0
chart: kiwigrid/fluentd-elasticsearch
`;
const upgrade = undefined;
expect(updateDependency(content, upgrade)).toBe(content);
});
it('returns the same fileContent for invalid helmfile.yaml file', () => {
const content = `
Invalid helmfile.yaml content.
`;
const upgrade = {
depName: 'fluentd-elasticsearch',
newValue: '5.3.0',
repository: 'https://kiwigrid.github.io',
};
expect(updateDependency(content, upgrade)).toBe(content);
});
it('returns the same fileContent for empty upgrade', () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: fluentd-elasticsearch
version: 5.3.0
chart: kiwigrid/fluentd-elasticsearch
`;
const upgrade = {};
expect(updateDependency(content, upgrade)).toBe(content);
});
it('upgrades dependency if valid upgrade', () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: fluentd-elasticsearch
version: 5.3.0
chart: kiwigrid/fluentd-elasticsearch
`;
const upgrade = {
depName: 'fluentd-elasticsearch',
newValue: '5.3.1',
repository: 'https://kiwigrid.github.io',
};
expect(updateDependency(content, upgrade)).not.toBe(content);
expect(updateDependency(content, upgrade)).toMatchSnapshot();
});
it('upgrades dependency if version field comes before name field', () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- version: 5.3.0
name: fluentd-elasticsearch
chart: kiwigrid/fluentd-elasticsearch
`;
const upgrade = {
depName: 'fluentd-elasticsearch',
newValue: '5.3.1',
repository: 'https://kiwigrid.github.io',
};
expect(updateDependency(content, upgrade)).not.toBe(content);
expect(updateDependency(content, upgrade)).toMatchSnapshot();
});
it('upgrades dependency if chart is repeated', () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: fluentd-elasticsearch-internal
version: 5.3.0
chart: kiwigrid/fluentd-elasticsearch
- name: nginx-ingress
version: 1.3.0
chart: stable/nginx-ingress
- name: fluentd-elasticsearch-external
version: 5.3.0
chart: kiwigrid/fluentd-elasticsearch
`;
const upgrade = {
depName: 'fluentd-elasticsearch',
newValue: '5.3.1',
repository: 'https://kiwigrid.github.io',
};
expect(updateDependency(content, upgrade)).not.toBe(content);
expect(updateDependency(content, upgrade)).toMatchSnapshot();
});
it('Not fail if same version in multiple package', () => {
const content = `
repositories:
- name: kiwigrid
url: https://kiwigrid.github.io
releases:
- name: fluentd-elasticsearch-internal
version: 5.3.0
chart: kiwigrid/fluentd-elasticsearch
- name: nginx-ingress
version: 5.3.0
chart: stable/nginx-ingress
- name: fluentd-elasticsearch-external
version: 5.3.0
chart: kiwigrid/fluentd-elasticsearch
`;
const upgrade = {
depName: 'fluentd-elasticsearch',
newValue: '5.3.1',
repository: 'https://kiwigrid.github.io',
};
expect(updateDependency(content, upgrade)).toBe(content);
});
});
});
...@@ -62,6 +62,9 @@ Object { ...@@ -62,6 +62,9 @@ Object {
"helm-requirements": Array [ "helm-requirements": Array [
Object {}, Object {},
], ],
"helmfile": Array [
Object {},
],
"homebrew": Array [ "homebrew": Array [
Object {}, Object {},
], ],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment