diff --git a/lib/manager/dockerfile/__fixtures__/Dockerfile2 b/lib/manager/dockerfile/__fixtures__/Dockerfile2 new file mode 100644 index 0000000000000000000000000000000000000000..2c2320d27793d5a52dda80f2bfcbc510dc721934 --- /dev/null +++ b/lib/manager/dockerfile/__fixtures__/Dockerfile2 @@ -0,0 +1,49 @@ +# different FROM syntaxes + FROM image1 as name1 + +FROM image2:1.0.0@sha256:abcdef \ + as name2 + +# FROM image3 + +FROM\ + --platform=linux \ + # comment1 + image4 + + FROM \ + \ + image5 \ + #comment5 + as name3 + +# different COPY --from syntaxes +COPY --from=image6 /path/1 /path/2 + + COPY \ + --from=image7:1.0.0@sha256:abcdef \ + /path/1 \ + /path/2 + +# COPY --from=image8 + +COPY --from=image9 + +COPY\ + --chown=root \ + # comment1 +# + #comment2 + --from=image10 + # comment2 + path1 path2 + +COPY --chown=root --from=image11 / ./ + + COPY \ + \ + --from=image12 a \ + #comment5 + b + +COPY --from=image13 --chown=root: a b diff --git a/lib/manager/dockerfile/__snapshots__/extract.spec.ts.snap b/lib/manager/dockerfile/__snapshots__/extract.spec.ts.snap index 19960ee8cc275f3983b805ff326a9ebd9d29d71f..7c0f2969618d241186a335be729f9497ef19e121 100644 --- a/lib/manager/dockerfile/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/dockerfile/__snapshots__/extract.spec.ts.snap @@ -33,6 +33,110 @@ Array [ ] `; +exports[`lib/manager/dockerfile/extract extractPackageFile() extracts images from all sorts of (maybe multiline) FROM and COPY --from statements 1`] = ` +Array [ + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image1", + "depType": "stage", + "replaceString": "image1", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": "sha256:abcdef", + "currentValue": "1.0.0", + "datasource": "docker", + "depName": "image2", + "depType": "stage", + "replaceString": "image2:1.0.0@sha256:abcdef", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image4", + "depType": "stage", + "replaceString": "image4", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image5", + "depType": "stage", + "replaceString": "image5", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image6", + "depType": "stage", + "replaceString": "image6", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": "sha256:abcdef", + "currentValue": "1.0.0", + "datasource": "docker", + "depName": "image7", + "depType": "stage", + "replaceString": "image7:1.0.0@sha256:abcdef", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image9", + "depType": "stage", + "replaceString": "image9", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image10", + "depType": "stage", + "replaceString": "image10", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image11", + "depType": "stage", + "replaceString": "image11", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image12", + "depType": "stage", + "replaceString": "image12", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": undefined, + "datasource": "docker", + "depName": "image13", + "depType": "final", + "replaceString": "image13", + }, +] +`; + exports[`lib/manager/dockerfile/extract extractPackageFile() extracts images on adjacent lines 1`] = ` Array [ Object { diff --git a/lib/manager/dockerfile/extract.spec.ts b/lib/manager/dockerfile/extract.spec.ts index 93d0af0dacf2aca7207ab0e6c344083173ff60ec..347664ebde61e6fedadf1a483964a382cfcb1c84 100644 --- a/lib/manager/dockerfile/extract.spec.ts +++ b/lib/manager/dockerfile/extract.spec.ts @@ -6,6 +6,11 @@ const d1 = readFileSync( 'utf8' ); +const d2 = readFileSync( + 'lib/manager/dockerfile/__fixtures__/Dockerfile2', + 'utf8' +); + describe('lib/manager/dockerfile/extract', () => { describe('extractPackageFile()', () => { it('handles no FROM', () => { @@ -142,6 +147,11 @@ describe('lib/manager/dockerfile/extract', () => { expect(res).toMatchSnapshot(); expect(res).toHaveLength(2); }); + it('extracts images from all sorts of (maybe multiline) FROM and COPY --from statements', () => { + const res = extractPackageFile(d2).deps; + expect(res).toMatchSnapshot(); + expect(res).toHaveLength(11); + }); it('handles calico/node', () => { const res = extractPackageFile('FROM calico/node\n').deps; expect(res).toMatchSnapshot(); diff --git a/lib/manager/dockerfile/extract.ts b/lib/manager/dockerfile/extract.ts index aaba5097229e5d09b53dba6edb84a2cf2d986aad..6705b9ea2aed8d3d9d55106c6ee821577b527146 100644 --- a/lib/manager/dockerfile/extract.ts +++ b/lib/manager/dockerfile/extract.ts @@ -65,56 +65,61 @@ export function getDep( export function extractPackageFile(content: string): PackageFile | null { const deps: PackageDependency[] = []; const stageNames: string[] = []; - let lineNumber = 0; - for (const fromLine of content.split('\n')) { - const fromMatch = /^FROM /i.test(fromLine); - if (fromMatch) { - logger.trace({ lineNumber, fromLine }, 'FROM line'); - const [, currentFrom, ...fromRest] = fromLine.match(/\S+/g); - if (fromRest.length === 2 && fromRest[0].toLowerCase() === 'as') { - logger.debug('Found a multistage build stage name'); - stageNames.push(fromRest[1]); - } - if (currentFrom === 'scratch') { - logger.debug('Skipping scratch'); - } else if (stageNames.includes(currentFrom)) { - logger.debug({ currentFrom }, 'Skipping alias FROM'); - } else { - const dep = getDep(currentFrom); - logger.trace( - { - depName: dep.depName, - currentValue: dep.currentValue, - currentDigest: dep.currentDigest, - }, - 'Dockerfile FROM' - ); - deps.push(dep); - } + + const fromMatches = content.matchAll( + /^[ \t]*FROM(?:\\\r?\n| |\t|#.*\r?\n|-\S+)+(?<image>\S+)(?:(?:\\\r?\n| |\t|#.*\r?\n)+as[ \t]+(?<name>\S+))?/gim + ); + + for (const fromMatch of fromMatches) { + if (fromMatch.groups.name) { + logger.debug('Found a multistage build stage name'); + stageNames.push(fromMatch.groups.name); + } + if (fromMatch.groups.image === 'scratch') { + logger.debug('Skipping scratch'); + } else if (stageNames.includes(fromMatch.groups.image)) { + logger.debug({ image: fromMatch.groups.image }, 'Skipping alias FROM'); + } else { + const dep = getDep(fromMatch.groups.image); + logger.trace( + { + depName: dep.depName, + currentValue: dep.currentValue, + currentDigest: dep.currentDigest, + }, + 'Dockerfile FROM' + ); + deps.push(dep); } + } + + const copyFromMatches = content.matchAll( + /^[ \t]*COPY(?:\\\r?\n| |\t|#.*\r?\n|-\S+)+--from=(?<image>\S+)/gim + ); - const copyFromMatch = /^(COPY --from=)([^\s]+)\s+(.*)$/i.exec(fromLine); - if (copyFromMatch) { - const [, , currentFrom] = copyFromMatch; - logger.trace({ lineNumber, fromLine }, 'COPY --from line'); - if (stageNames.includes(currentFrom)) { - logger.debug({ currentFrom }, 'Skipping alias COPY --from'); - } else if (!Number.isNaN(Number(currentFrom))) { - logger.debug({ currentFrom }, 'Skipping index reference COPY --from'); - } else { - const dep = getDep(currentFrom); - logger.debug( - { - depName: dep.depName, - currentValue: dep.currentValue, - currentDigest: dep.currentDigest, - }, - 'Dockerfile COPY --from' - ); - deps.push(dep); - } + for (const copyFromMatch of copyFromMatches) { + if (stageNames.includes(copyFromMatch.groups.image)) { + logger.debug( + { image: copyFromMatch.groups.image }, + 'Skipping alias COPY --from' + ); + } else if (!Number.isNaN(Number(copyFromMatch.groups.image))) { + logger.debug( + { image: copyFromMatch.groups.image }, + 'Skipping index reference COPY --from' + ); + } else { + const dep = getDep(copyFromMatch.groups.image); + logger.debug( + { + depName: dep.depName, + currentValue: dep.currentValue, + currentDigest: dep.currentDigest, + }, + 'Dockerfile COPY --from' + ); + deps.push(dep); } - lineNumber += 1; } if (!deps.length) { return null;