diff --git a/lib/manager/docker-compose/__fixtures__/docker-compose.3-default-val.yml b/lib/manager/docker-compose/__fixtures__/docker-compose.3-default-val.yml new file mode 100644 index 0000000000000000000000000000000000000000..0ff42a995d7c432c8e2ec27e23dd4777d68fcd41 --- /dev/null +++ b/lib/manager/docker-compose/__fixtures__/docker-compose.3-default-val.yml @@ -0,0 +1,4 @@ +version: "3.7" +services: + redis: + image: ${REDIS_IMAGE:-redis:5.0.0@sha256:abcd} \ No newline at end of file diff --git a/lib/manager/docker-compose/extract.spec.ts b/lib/manager/docker-compose/extract.spec.ts index e0b09bddf84c60cd14a7f1f8569a448e0432596e..5067db6164ecabe408cc1f26ff11a8938d6f1f2f 100644 --- a/lib/manager/docker-compose/extract.spec.ts +++ b/lib/manager/docker-compose/extract.spec.ts @@ -4,6 +4,7 @@ import { extractPackageFile } from './extract'; const yamlFile1 = loadFixture('docker-compose.1.yml'); const yamlFile3 = loadFixture('docker-compose.3.yml'); const yamlFile3NoVersion = loadFixture('docker-compose.3-no-version.yml'); +const yamlFile3DefaultValue = loadFixture('docker-compose.3-default-val.yml'); describe('manager/docker-compose/extract', () => { describe('extractPackageFile()', () => { @@ -31,5 +32,21 @@ describe('manager/docker-compose/extract', () => { expect(res.deps).toMatchSnapshot(); expect(res.deps).toHaveLength(8); }); + it('extracts default variable values for version 3', () => { + const res = extractPackageFile(yamlFile3DefaultValue); + expect(res.deps).toMatchInlineSnapshot(` + Array [ + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": "sha256:abcd", + "currentValue": "5.0.0", + "datasource": "docker", + "depName": "redis", + "replaceString": "redis:5.0.0@sha256:abcd", + }, + ] + `); + expect(res.deps).toHaveLength(1); + }); }); }); diff --git a/lib/manager/dockerfile/extract.spec.ts b/lib/manager/dockerfile/extract.spec.ts index ac320fe8035819210d1d8399bbcca1f17b54969a..f038205134248471ef913b20b741e4dd62e502bb 100644 --- a/lib/manager/dockerfile/extract.spec.ts +++ b/lib/manager/dockerfile/extract.spec.ts @@ -617,5 +617,44 @@ describe('manager/dockerfile/extract', () => { it('rejects null', () => { expect(getDep(null)).toEqual({ skipReason: 'invalid-value' }); }); + + it('handles default environment variable values', () => { + // eslint-disable-next-line no-template-curly-in-string + const res = getDep('${REDIS_IMAGE:-redis:5.0.0@sha256:abcd}'); + expect(res).toMatchInlineSnapshot(` +Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": "sha256:abcd", + "currentValue": "5.0.0", + "datasource": "docker", + "depName": "redis", + "replaceString": "redis:5.0.0@sha256:abcd", +} +`); + + // eslint-disable-next-line no-template-curly-in-string + const res2 = getDep('${REDIS_IMAGE:-redis:5.0.0}'); + expect(res2).toMatchInlineSnapshot(` +Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentValue": "5.0.0", + "datasource": "docker", + "depName": "redis", + "replaceString": "redis:5.0.0", +} +`); + + // eslint-disable-next-line no-template-curly-in-string + const res3 = getDep('${REDIS_IMAGE:-redis@sha256:abcd}'); + expect(res3).toMatchInlineSnapshot(` +Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": "sha256:abcd", + "datasource": "docker", + "depName": "redis", + "replaceString": "redis@sha256:abcd", +} +`); + }); }); }); diff --git a/lib/manager/dockerfile/extract.ts b/lib/manager/dockerfile/extract.ts index eb0bacfae270674e30391a39156e12598fc3aab5..0fe8d607ece0b71c6dd09397d9098b89aa4c09b2 100644 --- a/lib/manager/dockerfile/extract.ts +++ b/lib/manager/dockerfile/extract.ts @@ -6,13 +6,43 @@ import { regEx } from '../../util/regex'; import * as ubuntuVersioning from '../../versioning/ubuntu'; import type { PackageDependency, PackageFile } from '../types'; +const variableOpen = '${'; +const variableClose = '}'; +const variableDefaultValueSplit = ':-'; + export function splitImageParts(currentFrom: string): PackageDependency { - if (currentFrom.includes('$')) { - return { - skipReason: SkipReason.ContainsVariable, - }; + // Check if we have a variable in format of "${VARIABLE:-<image>:<defaultVal>@<digest>}" + // If so, remove everything except the image, defaultVal and digest. + let isVariable = false; + let cleanedCurrentFrom: string = currentFrom; + if ( + currentFrom.startsWith(variableOpen) && + currentFrom.endsWith(variableClose) + ) { + isVariable = true; + + // If the variable contains exactly one $ and has the default value, we consider it as a valid dependency; + // otherwise skip it. + if ( + currentFrom.split('$').length !== 2 || + currentFrom.indexOf(variableDefaultValueSplit) === -1 + ) { + return { + skipReason: SkipReason.ContainsVariable, + }; + } + + cleanedCurrentFrom = currentFrom.substr( + variableOpen.length, + currentFrom.length - (variableClose.length + 2) + ); + cleanedCurrentFrom = cleanedCurrentFrom.substr( + cleanedCurrentFrom.indexOf(variableDefaultValueSplit) + + variableDefaultValueSplit.length + ); } - const [currentDepTag, currentDigest] = currentFrom.split('@'); + + const [currentDepTag, currentDigest] = cleanedCurrentFrom.split('@'); const depTagSplit = currentDepTag.split(':'); let depName: string; let currentValue: string; @@ -25,6 +55,29 @@ export function splitImageParts(currentFrom: string): PackageDependency { currentValue = depTagSplit.pop(); depName = depTagSplit.join(':'); } + + if (isVariable) { + // If we have the variable and it contains the default value, we need to return + // it as a valid dependency. + + const dep = { + depName, + currentValue, + currentDigest, + replaceString: cleanedCurrentFrom, + }; + + if (!dep.currentValue) { + delete dep.currentValue; + } + + if (!dep.currentDigest) { + delete dep.currentDigest; + } + + return dep; + } + const dep: PackageDependency = { depName, currentValue, @@ -46,7 +99,9 @@ export function getDep( } const dep = splitImageParts(currentFrom); if (specifyReplaceString) { - dep.replaceString = currentFrom; + if (!dep.replaceString) { + dep.replaceString = currentFrom; + } dep.autoReplaceStringTemplate = '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}'; }