From a763ff3fec3d0b9f9bceec24f88f7e377c8fbc7b Mon Sep 17 00:00:00 2001 From: secustor Date: Wed, 28 Apr 2021 15:07:55 +0200 Subject: [PATCH 1/5] feat(argocd): implement manager --- lib/manager/api.ts | 2 + .../__fixtures__/malformedApplications.yml | 7 ++ .../argocd/__fixtures__/randomManifest.yml | 21 ++++++ .../argocd/__fixtures__/validApplication.yml | 38 ++++++++++ .../argocd/__snapshots__/extract.spec.ts.snap | 27 +++++++ lib/manager/argocd/extract.spec.ts | 34 +++++++++ lib/manager/argocd/extract.ts | 71 +++++++++++++++++++ lib/manager/argocd/index.ts | 5 ++ lib/manager/argocd/readme.md | 33 +++++++++ lib/manager/argocd/util.ts | 3 + 10 files changed, 241 insertions(+) create mode 100644 lib/manager/argocd/__fixtures__/malformedApplications.yml create mode 100644 lib/manager/argocd/__fixtures__/randomManifest.yml create mode 100644 lib/manager/argocd/__fixtures__/validApplication.yml create mode 100644 lib/manager/argocd/__snapshots__/extract.spec.ts.snap create mode 100644 lib/manager/argocd/extract.spec.ts create mode 100644 lib/manager/argocd/extract.ts create mode 100644 lib/manager/argocd/index.ts create mode 100644 lib/manager/argocd/readme.md create mode 100644 lib/manager/argocd/util.ts diff --git a/lib/manager/api.ts b/lib/manager/api.ts index 0a1a2c49d2e5f6..27ddd92efaf2f0 100644 --- a/lib/manager/api.ts +++ b/lib/manager/api.ts @@ -1,5 +1,6 @@ import * as ansible from './ansible'; import * as ansibleGalaxy from './ansible-galaxy'; +import * as argoCD from './argocd'; import * as azurePipelines from './azure-pipelines'; import * as batect from './batect'; import * as batectWrapper from './batect-wrapper'; @@ -65,6 +66,7 @@ export default api; api.set('ansible', ansible); api.set('ansible-galaxy', ansibleGalaxy); +api.set('argocd', argoCD); api.set('azure-pipelines', azurePipelines); api.set('batect', batect); api.set('batect-wrapper', batectWrapper); diff --git a/lib/manager/argocd/__fixtures__/malformedApplications.yml b/lib/manager/argocd/__fixtures__/malformedApplications.yml new file mode 100644 index 00000000000000..3af60676acb2ce --- /dev/null +++ b/lib/manager/argocd/__fixtures__/malformedApplications.yml @@ -0,0 +1,7 @@ +--- +# malformed application as the source section is missing +apiVersion: argoproj.io/v1alpha1 +kind: Application +spec: + target: + namespace: testing diff --git a/lib/manager/argocd/__fixtures__/randomManifest.yml b/lib/manager/argocd/__fixtures__/randomManifest.yml new file mode 100644 index 00000000000000..007ecd317d2de6 --- /dev/null +++ b/lib/manager/argocd/__fixtures__/randomManifest.yml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/lib/manager/argocd/__fixtures__/validApplication.yml b/lib/manager/argocd/__fixtures__/validApplication.yml new file mode 100644 index 00000000000000..b0ba7cdd513592 --- /dev/null +++ b/lib/manager/argocd/__fixtures__/validApplication.yml @@ -0,0 +1,38 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: +... +spec: + source: + chart: kube-state-metrics + repoURL: https://kubernetes-charts.storage.googleapis.com + targetRevision: 2.4.1 +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +spec: + source: + chart: traefik + helm: + values: | + traefik: + service: + spec: + loadBalancerIP: 1.2.3.4 + repoURL: gs://helm-charts-internal + targetRevision: 0.0.2 +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +spec: + source: + repoURL: https://git.example.com/foo/bar.git + targetRevision: v1.2.0 +--- +# malformed application as the source section is missing +apiVersion: argoproj.io/v1alpha1 +kind: Application +spec: + target: + namespace: testing diff --git a/lib/manager/argocd/__snapshots__/extract.spec.ts.snap b/lib/manager/argocd/__snapshots__/extract.spec.ts.snap new file mode 100644 index 00000000000000..5691635a62572c --- /dev/null +++ b/lib/manager/argocd/__snapshots__/extract.spec.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`manager/argocd/extract extractPackageFile() full rest 1`] = ` +Array [ + Object { + "currentValue": "2.4.1", + "datasource": "helm", + "depName": "kube-state-metrics", + "registryUrls": Array [ + "https://kubernetes-charts.storage.googleapis.com", + ], + }, + Object { + "currentValue": "0.0.2", + "datasource": "helm", + "depName": "traefik", + "registryUrls": Array [ + "gs://helm-charts-internal", + ], + }, + Object { + "currentValue": "v1.2.0", + "datasource": "git-tags", + "depName": "https://git.example.com/foo/bar.git", + }, +] +`; diff --git a/lib/manager/argocd/extract.spec.ts b/lib/manager/argocd/extract.spec.ts new file mode 100644 index 00000000000000..422bab0a81a206 --- /dev/null +++ b/lib/manager/argocd/extract.spec.ts @@ -0,0 +1,34 @@ +import { getName, loadFixture } from '../../../test/util'; +import { extractPackageFile } from './extract'; + +const validApplication = loadFixture('validApplication.yml'); +const malformedApplication = loadFixture('malformedApplications.yml'); +const randomManifest = loadFixture('randomManifest.yml'); + +describe(getName(), () => { + describe('extractPackageFile()', () => { + it('returns null for empty', () => { + expect(extractPackageFile('nothing here', 'applications.yml')).toBeNull(); + }); + + it('return null for kubernetes manifest', () => { + const result = extractPackageFile(randomManifest, 'applications.yml'); + expect(result).toBeNull(); + }); + + it('return null if deps array would be empty', () => { + const result = extractPackageFile( + malformedApplication, + 'applications.yml' + ); + expect(result).toBeNull(); + }); + + it('full rest', () => { + const result = extractPackageFile(validApplication, 'applications.yml'); + expect(result).not.toBeNull(); + expect(result.deps).toBeArrayOfSize(3); + expect(result.deps).toMatchSnapshot(); + }); + }); +}); diff --git a/lib/manager/argocd/extract.ts b/lib/manager/argocd/extract.ts new file mode 100644 index 00000000000000..7d273c31557351 --- /dev/null +++ b/lib/manager/argocd/extract.ts @@ -0,0 +1,71 @@ +import * as gitTags from '../../datasource/git-tags'; +import * as helm from '../../datasource/helm'; +import type { ExtractConfig, PackageDependency, PackageFile } from '../types'; +import { fileTestRegex, keyValueExtractionRegex } from './util'; + +function createDependency( + attributes: Record +): PackageDependency { + let result: PackageDependency; + + if (attributes.repoURL == null || attributes.targetRevision == null) { + return null; + } + + // a chart variable is defined this is helm declaration + if (attributes.chart) { + result = { + depName: attributes.chart, + registryUrls: [attributes.repoURL], + currentValue: attributes.targetRevision, + datasource: helm.id, + }; + } else { + result = { + depName: attributes.repoURL, + currentValue: attributes.targetRevision, + datasource: gitTags.id, + }; + } + return result; +} + +export function extractPackageFile( + content: string, + fileName: string, + config?: ExtractConfig +): PackageFile | null { + // check for argo reference. API version for the kind attribute is used + if (fileTestRegex.test(content) === false) { + return null; + } + + const definitionStrings = content.split('---'); + + const deps = definitionStrings.map((definitionString) => { + // check if the block is an ArgoCD API object + if (fileTestRegex.test(content) === false) { + return null; + } + + const lines = definitionString.split('\n'); + + const attributes: Record = {}; + lines.forEach((line) => { + const regexResult = keyValueExtractionRegex.exec(line); + if (regexResult) { + attributes[regexResult.groups.key] = regexResult.groups.value; + } + }); + + return createDependency(attributes); + }); + + const filteredDeps = deps.filter((value) => value); + + if (filteredDeps.length === 0) { + return null; + } + + return { deps: filteredDeps }; +} diff --git a/lib/manager/argocd/index.ts b/lib/manager/argocd/index.ts new file mode 100644 index 00000000000000..1012c71468dbc4 --- /dev/null +++ b/lib/manager/argocd/index.ts @@ -0,0 +1,5 @@ +export { extractPackageFile } from './extract'; + +export const defaultConfig = { + fileMatch: [], +}; diff --git a/lib/manager/argocd/readme.md b/lib/manager/argocd/readme.md new file mode 100644 index 00000000000000..4bc8a70e7945c8 --- /dev/null +++ b/lib/manager/argocd/readme.md @@ -0,0 +1,33 @@ +The `argocd` manager has no `fileMatch` default patterns, so it won't match any files until you configure it with a pattern. This is because there is no commonly accepted file/directory naming convention for argocd YAML files and we don't want to check every single `*.yaml` file in repositories just in case any of them contain ArgoCD definitions. + +If most `.yaml` files in your repository are argocd ones, then you could add this to your config: + +```json +{ + "argocd": { + "fileMatch": ["\\.yaml$"] + } +} +``` + +If instead you have them all inside a `argocd/` directory, you would add this: + +```json +{ + "argocd": { + "fileMatch": ["argocd/.+\\.yaml$"] + } +} +``` + +Or if it's just a single file then something like this: + +```json +{ + "argocd": { + "fileMatch": ["^config/applications\\.yaml$"] + } +} +``` + +If you need to change the versioning format, read the [versioning](https://docs.renovatebot.com/modules/versioning/) documentation to learn more. diff --git a/lib/manager/argocd/util.ts b/lib/manager/argocd/util.ts new file mode 100644 index 00000000000000..0c0d1434820980 --- /dev/null +++ b/lib/manager/argocd/util.ts @@ -0,0 +1,3 @@ +export const keyValueExtractionRegex = /^\s*(?[^\s]+):\s+"?(?[^"\s]+)"?\s*$/; +// looks for `apiVersion: argoproj.io/ +export const fileTestRegex = /\s*apiVersion:\s*argoproj.io\/\s*/; From dfa9874a7e9929509ea6a8729c02cc1effca48c1 Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 29 Apr 2021 00:03:25 +0200 Subject: [PATCH 2/5] fix(argocd): use yaml parser and typing --- .../argocd/__fixtures__/validApplication.yml | 4 +- .../argocd/__snapshots__/extract.spec.ts.snap | 4 +- lib/manager/argocd/extract.spec.ts | 2 +- lib/manager/argocd/extract.ts | 48 +++++++++++-------- lib/manager/argocd/types.ts | 9 ++++ 5 files changed, 41 insertions(+), 26 deletions(-) create mode 100644 lib/manager/argocd/types.ts diff --git a/lib/manager/argocd/__fixtures__/validApplication.yml b/lib/manager/argocd/__fixtures__/validApplication.yml index b0ba7cdd513592..d8146bec1b02f5 100644 --- a/lib/manager/argocd/__fixtures__/validApplication.yml +++ b/lib/manager/argocd/__fixtures__/validApplication.yml @@ -1,12 +1,10 @@ --- apiVersion: argoproj.io/v1alpha1 kind: Application -metadata: -... spec: source: chart: kube-state-metrics - repoURL: https://kubernetes-charts.storage.googleapis.com + repoURL: https://prometheus-community.github.io/helm-charts targetRevision: 2.4.1 --- apiVersion: argoproj.io/v1alpha1 diff --git a/lib/manager/argocd/__snapshots__/extract.spec.ts.snap b/lib/manager/argocd/__snapshots__/extract.spec.ts.snap index 5691635a62572c..c5c2f9abf557f0 100644 --- a/lib/manager/argocd/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/argocd/__snapshots__/extract.spec.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`manager/argocd/extract extractPackageFile() full rest 1`] = ` +exports[`manager/argocd/extract extractPackageFile() full test 1`] = ` Array [ Object { "currentValue": "2.4.1", "datasource": "helm", "depName": "kube-state-metrics", "registryUrls": Array [ - "https://kubernetes-charts.storage.googleapis.com", + "https://prometheus-community.github.io/helm-charts", ], }, Object { diff --git a/lib/manager/argocd/extract.spec.ts b/lib/manager/argocd/extract.spec.ts index 422bab0a81a206..1160b6b95f4b44 100644 --- a/lib/manager/argocd/extract.spec.ts +++ b/lib/manager/argocd/extract.spec.ts @@ -24,7 +24,7 @@ describe(getName(), () => { expect(result).toBeNull(); }); - it('full rest', () => { + it('full test', () => { const result = extractPackageFile(validApplication, 'applications.yml'); expect(result).not.toBeNull(); expect(result.deps).toBeArrayOfSize(3); diff --git a/lib/manager/argocd/extract.ts b/lib/manager/argocd/extract.ts index 7d273c31557351..bcfb3fa8a4a765 100644 --- a/lib/manager/argocd/extract.ts +++ b/lib/manager/argocd/extract.ts @@ -1,29 +1,45 @@ +import { safeLoad } from 'js-yaml'; import * as gitTags from '../../datasource/git-tags'; import * as helm from '../../datasource/helm'; import type { ExtractConfig, PackageDependency, PackageFile } from '../types'; -import { fileTestRegex, keyValueExtractionRegex } from './util'; +import type { ApplicationDefinition } from './types'; +import { fileTestRegex } from './util'; +function loadYaml(content: string): ApplicationDefinition { + const config = safeLoad(content); + + if (typeof config !== 'object') { + /* istanbul ignore next */ + throw new Error( + `Configuration file does not contain a YAML object (it is ${typeof config}).` + ); + } + + return config as ApplicationDefinition; +} function createDependency( - attributes: Record + definition: ApplicationDefinition ): PackageDependency { let result: PackageDependency; - if (attributes.repoURL == null || attributes.targetRevision == null) { + const source = definition.spec.source; + + if (source == null) { return null; } // a chart variable is defined this is helm declaration - if (attributes.chart) { + if (source.chart) { result = { - depName: attributes.chart, - registryUrls: [attributes.repoURL], - currentValue: attributes.targetRevision, + depName: source.chart, + registryUrls: [source.repoURL], + currentValue: source.targetRevision, datasource: helm.id, }; } else { result = { - depName: attributes.repoURL, - currentValue: attributes.targetRevision, + depName: source.repoURL, + currentValue: source.targetRevision, datasource: gitTags.id, }; } @@ -44,21 +60,13 @@ export function extractPackageFile( const deps = definitionStrings.map((definitionString) => { // check if the block is an ArgoCD API object - if (fileTestRegex.test(content) === false) { + if (fileTestRegex.test(definitionString) === false) { return null; } - const lines = definitionString.split('\n'); - - const attributes: Record = {}; - lines.forEach((line) => { - const regexResult = keyValueExtractionRegex.exec(line); - if (regexResult) { - attributes[regexResult.groups.key] = regexResult.groups.value; - } - }); + const definition = loadYaml(definitionString); - return createDependency(attributes); + return createDependency(definition); }); const filteredDeps = deps.filter((value) => value); diff --git a/lib/manager/argocd/types.ts b/lib/manager/argocd/types.ts new file mode 100644 index 00000000000000..8a84291575d077 --- /dev/null +++ b/lib/manager/argocd/types.ts @@ -0,0 +1,9 @@ +export interface ApplicationDefinition { + spec: { + source: { + chart?: string; + repoURL: string; + targetRevision: string; + }; + }; +} From 9eb1ca98f45529726e7b81029c16088444266232 Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 29 Apr 2021 00:13:26 +0200 Subject: [PATCH 3/5] fix(argocd): use loadAll instead custom logic --- lib/manager/argocd/extract.ts | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/lib/manager/argocd/extract.ts b/lib/manager/argocd/extract.ts index bcfb3fa8a4a765..ea7468c842297f 100644 --- a/lib/manager/argocd/extract.ts +++ b/lib/manager/argocd/extract.ts @@ -1,21 +1,12 @@ -import { safeLoad } from 'js-yaml'; +import { safeLoadAll } from 'js-yaml'; import * as gitTags from '../../datasource/git-tags'; import * as helm from '../../datasource/helm'; import type { ExtractConfig, PackageDependency, PackageFile } from '../types'; import type { ApplicationDefinition } from './types'; import { fileTestRegex } from './util'; -function loadYaml(content: string): ApplicationDefinition { - const config = safeLoad(content); - - if (typeof config !== 'object') { - /* istanbul ignore next */ - throw new Error( - `Configuration file does not contain a YAML object (it is ${typeof config}).` - ); - } - - return config as ApplicationDefinition; +function loadYaml(content: string): ApplicationDefinition[] { + return safeLoadAll(content); } function createDependency( definition: ApplicationDefinition @@ -56,16 +47,9 @@ export function extractPackageFile( return null; } - const definitionStrings = content.split('---'); - - const deps = definitionStrings.map((definitionString) => { - // check if the block is an ArgoCD API object - if (fileTestRegex.test(definitionString) === false) { - return null; - } - - const definition = loadYaml(definitionString); + const definitions = loadYaml(content); + const deps = definitions.map((definition) => { return createDependency(definition); }); From f428bb63919a40bb676a18b4a933db06ed3ba54a Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 29 Apr 2021 00:15:32 +0200 Subject: [PATCH 4/5] fix(argocd): linting --- lib/manager/argocd/extract.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/manager/argocd/extract.ts b/lib/manager/argocd/extract.ts index ea7468c842297f..f917d051dbcf26 100644 --- a/lib/manager/argocd/extract.ts +++ b/lib/manager/argocd/extract.ts @@ -49,9 +49,7 @@ export function extractPackageFile( const definitions = loadYaml(content); - const deps = definitions.map((definition) => { - return createDependency(definition); - }); + const deps = definitions.map((definition) => createDependency(definition)); const filteredDeps = deps.filter((value) => value); From 7b3401b275cfdd621936b96756cd47fcec3be4c8 Mon Sep 17 00:00:00 2001 From: secustor Date: Thu, 29 Apr 2021 10:10:28 +0200 Subject: [PATCH 5/5] fix(argocd): apply requested changes --- .../__fixtures__/malformedApplications.yml | 4 +++ lib/manager/argocd/extract.ts | 35 +++++++------------ 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/lib/manager/argocd/__fixtures__/malformedApplications.yml b/lib/manager/argocd/__fixtures__/malformedApplications.yml index 3af60676acb2ce..574b683230cc89 100644 --- a/lib/manager/argocd/__fixtures__/malformedApplications.yml +++ b/lib/manager/argocd/__fixtures__/malformedApplications.yml @@ -5,3 +5,7 @@ kind: Application spec: target: namespace: testing +--- +# malformed application as the source section is missing +apiVersion: argoproj.io/v1alpha1 +kind: Application diff --git a/lib/manager/argocd/extract.ts b/lib/manager/argocd/extract.ts index f917d051dbcf26..3ff72772f8ce2a 100644 --- a/lib/manager/argocd/extract.ts +++ b/lib/manager/argocd/extract.ts @@ -5,15 +5,10 @@ import type { ExtractConfig, PackageDependency, PackageFile } from '../types'; import type { ApplicationDefinition } from './types'; import { fileTestRegex } from './util'; -function loadYaml(content: string): ApplicationDefinition[] { - return safeLoadAll(content); -} function createDependency( definition: ApplicationDefinition ): PackageDependency { - let result: PackageDependency; - - const source = definition.spec.source; + const source = definition.spec?.source; if (source == null) { return null; @@ -21,20 +16,18 @@ function createDependency( // a chart variable is defined this is helm declaration if (source.chart) { - result = { + return { depName: source.chart, registryUrls: [source.repoURL], currentValue: source.targetRevision, datasource: helm.id, }; - } else { - result = { - depName: source.repoURL, - currentValue: source.targetRevision, - datasource: gitTags.id, - }; } - return result; + return { + depName: source.repoURL, + currentValue: source.targetRevision, + datasource: gitTags.id, + }; } export function extractPackageFile( @@ -47,15 +40,11 @@ export function extractPackageFile( return null; } - const definitions = loadYaml(content); + const definitions: ApplicationDefinition[] = safeLoadAll(content); - const deps = definitions.map((definition) => createDependency(definition)); - - const filteredDeps = deps.filter((value) => value); - - if (filteredDeps.length === 0) { - return null; - } + const deps = definitions + .map((definition) => createDependency(definition)) + .filter(Boolean); - return { deps: filteredDeps }; + return deps.length ? { deps } : null; }