diff --git a/lib/modules/manager/docker-compose/__fixtures__/docker-compose.4.yml.j2 b/lib/modules/manager/docker-compose/__fixtures__/docker-compose.4.yml.j2 new file mode 100644 index 00000000000000..8cb81b431a622b --- /dev/null +++ b/lib/modules/manager/docker-compose/__fixtures__/docker-compose.4.yml.j2 @@ -0,0 +1,26 @@ +--- +version: "3" + +networks: + {{ network_name }}: + external: true + frontend: + external: true + +services: + web: + image: "node:42.0.0" + networks: + - {{ network_name }} + - frontend + ports: + - "80:8000" + deploy: + replicas: 2 + update_config: + parallelism: 2 + delay: 10s + placement: + {{ placement | indent(8) }} + restart_policy: + condition: on-failure diff --git a/lib/modules/manager/docker-compose/extract.spec.ts b/lib/modules/manager/docker-compose/extract.spec.ts index a089c8b465a612..1c3a7ab0272886 100644 --- a/lib/modules/manager/docker-compose/extract.spec.ts +++ b/lib/modules/manager/docker-compose/extract.spec.ts @@ -6,6 +6,7 @@ const yamlFile1 = Fixtures.get('docker-compose.1.yml'); const yamlFile3 = Fixtures.get('docker-compose.3.yml'); const yamlFile3NoVersion = Fixtures.get('docker-compose.3-no-version.yml'); const yamlFile3DefaultValue = Fixtures.get('docker-compose.3-default-val.yml'); +const yamlFile4Templated = Fixtures.get('docker-compose.4.yml.j2'); describe('modules/manager/docker-compose/extract', () => { describe('extractPackageFile()', () => { @@ -56,6 +57,23 @@ describe('modules/manager/docker-compose/extract', () => { expect(res?.deps).toHaveLength(1); }); + it('extracts can parse a templated yaml file', () => { + const res = extractPackageFile(yamlFile4Templated, '', {}); + expect(res).toEqual({ + deps: [ + { + depName: 'node', + currentValue: '42.0.0', + currentDigest: undefined, + replaceString: 'node:42.0.0', + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + datasource: 'docker', + }, + ], + }); + }); + it('extracts can parse yaml tags for version 3', () => { const compose = codeBlock` web: diff --git a/lib/util/yaml.spec.ts b/lib/util/yaml.spec.ts index d45ab5d3d613e0..0edd6f4122756e 100644 --- a/lib/util/yaml.spec.ts +++ b/lib/util/yaml.spec.ts @@ -181,11 +181,11 @@ describe('util/yaml', () => { ).toEqual([ { myObject: { - aString: null, + aString: 'd1ddada', }, }, { - foo: null, + foo: '06de5ba', }, ]); }); @@ -273,25 +273,94 @@ services: ).toThrow(); }); - it('should parse content with template', () => { + it('should parse content with basic templates', () => { expect( parseSingleYaml( codeBlock` - myObject: - aString: {{value}} - {% if test.enabled %} - myNestedObject: - aNestedString: {{value}} - {% endif %} - `, + object: + string: "string" + number: 42 + stringTemplated: "{{value}}" + numberTemplated: {{42}} + list: + {% if test.enabled %} + - {{value}} + {% endif %} + - listEntry + `, { removeTemplates: true }, ), ).toEqual({ - myObject: { - aString: null, - myNestedObject: { - aNestedString: null, + object: { + // Hash for `{{value}}` + string: 'string', + number: 42, + // Hash for `{{value}}` + stringTemplated: 'f00c233', + // Hash for `{{42}}` + numberTemplated: 'df35179', + list: ['f00c233', 'listEntry'], + }, + }); + }); + + it('should parse content with templated yaml keys', () => { + expect( + parseSingleYaml( + codeBlock` + object: + string: "string" + {{aKey}}: + string: "string" + number: 12 + {{anotherKey}}: + string: "another string" + number: 30 + number: 42 + `, + { removeTemplates: true }, + ), + ).toEqual({ + object: { + string: 'string', + // Hash for `{{aKey}}` + '94fdf85': { + string: 'string', + number: 12, + }, + // Hash for `{{anotherKey}}` + '8904e7f': { + string: 'another string', + number: 30, + }, + number: 42, + }, + }); + }); + + it('should parse content with templated value in objects', () => { + expect( + parseSingleYaml( + codeBlock` + object: + string: "string" + childObject: + {{value}} + key: value + anotherChildObject: + {{value}} + number: 42 + `, + { removeTemplates: true }, + ), + ).toEqual({ + object: { + string: 'string', + childObject: { + key: 'value', }, + anotherChildObject: null, + number: 42, }, }); }); diff --git a/lib/util/yaml.ts b/lib/util/yaml.ts index e8d5ef50ae2eae..8c0d226d96fe92 100644 --- a/lib/util/yaml.ts +++ b/lib/util/yaml.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto'; import type { CreateNodeOptions, DocumentOptions, @@ -140,14 +141,25 @@ export function dump(obj: any, opts?: DumpOptions): string { return stringify(obj, opts); } +function getShortHash(data: any): string { + return crypto.createHash('sha256').update(data).digest('hex').substring(0, 7); +} + function massageContent(content: string, options?: YamlOptions): string { if (options?.removeTemplates) { - return content - .replace(regEx(/\s+{{.+?}}:.+/gs), '') - .replace(regEx(/{{`.+?`}}/gs), '') - .replace(regEx(/{{.+?}}/gs), '') - .replace(regEx(/{%`.+?`%}/gs), '') - .replace(regEx(/{%.+?%}/g), ''); + return ( + content + // NOTE: It seems safe to empty a line that only + // contains a Jinja2 tag entry. + .replace(regEx(/^(\s*)?{{.+?}}$/gm), '') + .replace(regEx(/{{`.+?`}}/gs), '') + // NOTE: In order to keep a proper Yaml syntax before + // the parsing, we're remplacing each of the remaining + // Jinja2 by a hash of the whole matched tag. + .replace(regEx(/{{.+?}}/g), getShortHash) + .replace(regEx(/{%`.+?`%}/gs), '') + .replace(regEx(/{%.+?%}/g), '') + ); } return content;