import is from '@sindresorhus/is' ; import * as datasourceDocker from '../../datasource/docker' ; import { logger } from '../../logger' ; import { SkipReason } from '../../types' ; import { regEx } from '../../util/regex' ; import * as ubuntuVersioning from '../../versioning/ubuntu' ; import type { PackageDependency , PackageFile } from '../types' ; const variableMarker = '$' ; const variableOpen = '${' ; const variableClose = '}' ; const variableDefaultValueSplit = ':-' ; export function splitImageParts ( currentFrom ) { let isVariable = false ; let cleanedCurrentFrom = currentFrom ; if ( currentFrom . startsWith ( variableOpen ) && currentFrom . endsWith ( variableClose ) ) { isVariable = true ; 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 ] = cleanedCurrentFrom . split ( '@' ) ; const depTagSplit = currentDepTag . split ( ':' ) ; let depName ; let currentValue ; if ( depTagSplit . length === 1 || depTagSplit [ depTagSplit . length - 1 ] . includes ( '/' ) ) { depName = currentDepTag ; } else { currentValue = depTagSplit . pop ( ) ; depName = depTagSplit . join ( ':' ) ; } if ( currentValue && currentValue . indexOf ( variableMarker ) !== - 1 ) { return { skipReason : SkipReason . ContainsVariable , } ; } if ( isVariable ) { const dep = { depName , currentValue , currentDigest , replaceString : cleanedCurrentFrom , } ; if ( ! dep . currentValue ) { delete dep . currentValue ; } if ( ! dep . currentDigest ) { delete dep . currentDigest ; } return dep ; } const dep = { depName , currentValue , currentDigest , } ; return dep ; } const quayRegex = regEx ( /^quay\.io(?::[1-9][0-9]{0,4})?/i ) ; export function getDep ( currentFrom , specifyReplaceString = true ) { if ( ! is . string ( currentFrom ) ) { return { skipReason : SkipReason . InvalidValue , } ; } const dep = splitImageParts ( currentFrom ) ; if ( specifyReplaceString ) { if ( ! dep . replaceString ) { dep . replaceString = currentFrom ; } dep . autoReplaceStringTemplate = '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}' ; } dep . datasource = datasourceDocker . id ; if ( dep . depName ) { const specialPrefixes = [ 'amd64' , 'arm64' , 'library' ] ; for ( const prefix of specialPrefixes ) { if ( dep . depName . startsWith ( `${ prefix }/` ) ) { dep . lookupName = dep . depName ; dep . depName = dep . depName . replace ( `${ prefix }/` , '' ) ; if ( specifyReplaceString ) { dep . autoReplaceStringTemplate = '{{lookupName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}' ; } } } } if ( dep . depName === 'ubuntu' ) { dep . versioning = ubuntuVersioning . id ; } if ( quayRegex . test ( dep . depName ) ) { const depName = dep . depName . replace ( quayRegex , 'quay.io' ) ; if ( depName !== dep . depName ) { dep . lookupName = dep . depName ; dep . depName = depName ; dep . autoReplaceStringTemplate = '{{lookupName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}' ; } } return dep ; } export function extractPackageFile ( content ) { const deps = [ ] ; const stageNames = [ ] ; const fromMatches = content . matchAll ( /^[ \t]*FROM(?:\\\r?\n| |\t|#.*?\r?\n|[ \t]--[a-z]+=\S+?)*[ \t](?<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|[ \t]--[a-z]+=\w+?)*[ \t]--from=(?<image>\S+)/gim ) ; 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 ) ) ) { const dep = getDep ( copyFromMatch . groups . image ) ; logger . debug ( { depName : dep . depName , currentValue : dep . currentValue , currentDigest : dep . currentDigest , } , 'Dockerfile COPY --from' ) ; deps . push ( dep ) ; } else { logger . debug ( { image : copyFromMatch . groups . image } , 'Skipping index reference COPY --from' ) ; } } if ( ! deps . length ) { return null ; } for ( const d of deps ) { d . depType = 'stage' ; } deps [ deps . length - 1 ] . depType = 'final' ; return { deps } ; }