@@ -392,6 +392,8 @@ type buildOptions struct {
392392 UseFixedBuildDir bool
393393 DisableCoverage bool
394394 InFlightChecksums bool
395+ DockerExportToCache bool
396+ DockerExportSet bool // Track if explicitly set via CLI flag or env var
395397
396398 context * buildContext
397399}
@@ -515,6 +517,15 @@ func WithInFlightChecksums(enabled bool) BuildOption {
515517 }
516518}
517519
520+ // WithDockerExportToCache configures whether Docker images should be exported to cache
521+ func WithDockerExportToCache (exportToCache bool , explicitlySet bool ) BuildOption {
522+ return func (opts * buildOptions ) error {
523+ opts .DockerExportToCache = exportToCache
524+ opts .DockerExportSet = explicitlySet
525+ return nil
526+ }
527+ }
528+
518529func withBuildContext (ctx * buildContext ) BuildOption {
519530 return func (opts * buildOptions ) error {
520531 opts .context = ctx
@@ -1690,6 +1701,19 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p
16901701 return nil , err
16911702 }
16921703
1704+ // Apply global export mode setting from build options (CLI flag or env var)
1705+ // This overrides the package-level configuration in BOTH directions
1706+ if buildctx .DockerExportSet {
1707+ if cfg .ExportToCache != buildctx .DockerExportToCache {
1708+ log .WithField ("package" , p .FullName ()).
1709+ WithField ("package_config" , cfg .ExportToCache ).
1710+ WithField ("override_value" , buildctx .DockerExportToCache ).
1711+ Info ("Docker export mode overridden via CLI flag or environment variable" )
1712+ }
1713+ cfg .ExportToCache = buildctx .DockerExportToCache
1714+ }
1715+ // else: respect package config (no override)
1716+
16931717 var (
16941718 commands = make (map [PackageBuildPhase ][][]string )
16951719 imageDependencies = make (map [string ]string )
@@ -1850,9 +1874,9 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p
18501874 ))
18511875
18521876 commands [PackageBuildPhasePackage ] = pkgcmds
1853- } else if len (cfg .Image ) > 0 {
1877+ } else if len (cfg .Image ) > 0 && ! cfg . ExportToCache {
18541878 // Image push workflow
1855- log .WithField ("images" , cfg .Image ).Debug ("configuring image push" )
1879+ log .WithField ("images" , cfg .Image ).Debug ("configuring image push (legacy behavior) " )
18561880
18571881 for _ , img := range cfg .Image {
18581882 pkgCommands = append (pkgCommands , [][]string {
@@ -1905,75 +1929,74 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p
19051929 Commands : commands ,
19061930 }
19071931
1908- // Enhanced subjects function with better error handling and logging
1909- res .Subjects = func () ([]in_toto.Subject , error ) {
1910- subjectLogger := log .WithField ("operation" , "provenance-subjects" )
1911- subjectLogger .Debug ("Calculating provenance subjects for pushed images" )
1912-
1913- // Get image digest with improved error handling
1914- out , err := exec .Command ("docker" , "inspect" , version ).CombinedOutput ()
1915- if err != nil {
1916- return nil , xerrors .Errorf ("failed to inspect image %s: %w\n Output: %s" ,
1917- version , err , string (out ))
1918- }
1919-
1920- var inspectRes []struct {
1921- ID string `json:"Id"`
1922- RepoDigests []string `json:"RepoDigests"`
1923- }
1932+ // Add subjects function for provenance generation
1933+ res .Subjects = createDockerSubjectsFunction (version , cfg )
1934+ } else if len (cfg .Image ) > 0 && cfg .ExportToCache {
1935+ // Export to cache for signing
1936+ log .WithField ("package" , p .FullName ()).Debug ("Exporting Docker image to cache" )
19241937
1925- if err := json .Unmarshal (out , & inspectRes ); err != nil {
1926- return nil , xerrors .Errorf ("cannot unmarshal Docker inspect response: %w" , err )
1927- }
1938+ // Export the image to tar
1939+ imageTarPath := filepath .Join (wd , "image.tar" )
1940+ pkgCommands = append (pkgCommands ,
1941+ []string {"docker" , "save" , version , "-o" , imageTarPath },
1942+ )
19281943
1929- if len (inspectRes ) == 0 {
1930- return nil , xerrors .Errorf ("docker inspect returned empty result for image %s" , version )
1931- }
1944+ // Store image names for later use
1945+ for _ , img := range cfg .Image {
1946+ pkgCommands = append (pkgCommands ,
1947+ []string {"sh" , "-c" , fmt .Sprintf ("echo %s >> %s" , img , dockerImageNamesFiles )},
1948+ )
1949+ }
19321950
1933- // Try to get digest from ID first (most reliable)
1934- var digest common.DigestSet
1935- if inspectRes [0 ].ID != "" {
1936- segs := strings .Split (inspectRes [0 ].ID , ":" )
1937- if len (segs ) == 2 {
1938- digest = common.DigestSet {
1939- segs [0 ]: segs [1 ],
1940- }
1941- }
1951+ // Add metadata file
1952+ if len (cfg .Metadata ) > 0 {
1953+ metadataContent , err := yaml .Marshal (cfg .Metadata )
1954+ if err != nil {
1955+ return nil , xerrors .Errorf ("failed to marshal metadata: %w" , err )
19421956 }
1957+ encodedMetadata := base64 .StdEncoding .EncodeToString (metadataContent )
1958+ pkgCommands = append (pkgCommands ,
1959+ []string {"sh" , "-c" , fmt .Sprintf ("echo %s | base64 -d > %s" , encodedMetadata , dockerMetadataFile )},
1960+ )
1961+ }
19431962
1944- // If we couldn't get digest from ID, try RepoDigests as fallback
1945- if len ( digest ) == 0 && len ( inspectRes [ 0 ]. RepoDigests ) > 0 {
1946- for _ , repoDigest := range inspectRes [ 0 ]. RepoDigests {
1947- parts := strings . Split ( repoDigest , "@" )
1948- if len ( parts ) == 2 {
1949- digestParts := strings . Split ( parts [ 1 ], ":" )
1950- if len ( digestParts ) == 2 {
1951- digest = common. DigestSet {
1952- digestParts [ 0 ]: digestParts [ 1 ],
1953- }
1954- break
1955- }
1956- }
1957- }
1958- }
1963+ // Package everything into final tar.gz
1964+ sourcePaths := [] string { "./image.tar" , fmt . Sprintf ( "./%s" , dockerImageNamesFiles ), "./docker-export-metadata.json" }
1965+ if len ( cfg . Metadata ) > 0 {
1966+ sourcePaths = append ( sourcePaths , fmt . Sprintf ( "./%s" , dockerMetadataFile ) )
1967+ }
1968+ if p . C . W . Provenance . Enabled {
1969+ sourcePaths = append ( sourcePaths , fmt . Sprintf ( "./%s" , provenanceBundleFilename ))
1970+ }
1971+ if p . C . W . SBOM . Enabled {
1972+ sourcePaths = append ( sourcePaths ,
1973+ fmt . Sprintf ( "./%s" , sbomBaseFilename + sbomCycloneDXFileExtension ),
1974+ fmt . Sprintf ( "./%s" , sbomBaseFilename + sbomSPDXFileExtension ),
1975+ fmt . Sprintf ( "./%s" , sbomBaseFilename + sbomSyftFileExtension ),
1976+ )
1977+ }
19591978
1960- if len (digest ) == 0 {
1961- return nil , xerrors .Errorf ("could not determine digest for image %s" , version )
1962- }
1979+ archiveCmd := BuildTarCommand (
1980+ WithOutputFile (result ),
1981+ WithSourcePaths (sourcePaths ... ),
1982+ WithCompression (! buildctx .DontCompress ),
1983+ )
1984+ pkgCommands = append (pkgCommands , archiveCmd )
19631985
1964- subjectLogger . WithField ( "digest" , digest ). Debug ( "Found image digest" )
1986+ commands [ PackageBuildPhasePackage ] = pkgCommands
19651987
1966- // Create subjects for each image
1967- result := make ([]in_toto.Subject , 0 , len (cfg .Image ))
1968- for _ , tag := range cfg .Image {
1969- result = append (result , in_toto.Subject {
1970- Name : tag ,
1971- Digest : digest ,
1972- })
1973- }
1988+ // Initialize res with commands
1989+ res = & packageBuild {
1990+ Commands : commands ,
1991+ }
19741992
1975- return result , nil
1993+ // Add PostProcess to create structured metadata file
1994+ res .PostProcess = func (buildCtx * buildContext , pkg * Package , buildDir string ) error {
1995+ return createDockerExportMetadata (buildDir , version , cfg )
19761996 }
1997+
1998+ // Add subjects function for provenance generation
1999+ res .Subjects = createDockerSubjectsFunction (version , cfg )
19772000 }
19782001
19792002 return res , nil
@@ -2033,6 +2056,118 @@ func extractImageNameFromCache(pkgName, cacheBundleFN string) (imgname string, e
20332056 return "" , nil
20342057}
20352058
2059+ // createDockerSubjectsFunction creates a function that generates SLSA provenance subjects for Docker images
2060+ func createDockerSubjectsFunction (version string , cfg DockerPkgConfig ) func () ([]in_toto.Subject , error ) {
2061+ return func () ([]in_toto.Subject , error ) {
2062+ subjectLogger := log .WithField ("operation" , "provenance-subjects" )
2063+ subjectLogger .Debug ("Calculating provenance subjects for Docker images" )
2064+
2065+ // Get image digest with improved error handling
2066+ out , err := exec .Command ("docker" , "inspect" , version ).CombinedOutput ()
2067+ if err != nil {
2068+ return nil , xerrors .Errorf ("failed to inspect image %s: %w\n Output: %s" ,
2069+ version , err , string (out ))
2070+ }
2071+
2072+ var inspectRes []struct {
2073+ ID string `json:"Id"`
2074+ RepoDigests []string `json:"RepoDigests"`
2075+ }
2076+
2077+ if err := json .Unmarshal (out , & inspectRes ); err != nil {
2078+ return nil , xerrors .Errorf ("cannot unmarshal Docker inspect response: %w" , err )
2079+ }
2080+
2081+ if len (inspectRes ) == 0 {
2082+ return nil , xerrors .Errorf ("docker inspect returned empty result for image %s" , version )
2083+ }
2084+
2085+ // Try to get digest from ID first (most reliable)
2086+ var digest common.DigestSet
2087+ if inspectRes [0 ].ID != "" {
2088+ segs := strings .Split (inspectRes [0 ].ID , ":" )
2089+ if len (segs ) == 2 {
2090+ digest = common.DigestSet {
2091+ segs [0 ]: segs [1 ],
2092+ }
2093+ }
2094+ }
2095+
2096+ // If we couldn't get digest from ID, try RepoDigests as fallback
2097+ if len (digest ) == 0 && len (inspectRes [0 ].RepoDigests ) > 0 {
2098+ for _ , repoDigest := range inspectRes [0 ].RepoDigests {
2099+ parts := strings .Split (repoDigest , "@" )
2100+ if len (parts ) == 2 {
2101+ digestParts := strings .Split (parts [1 ], ":" )
2102+ if len (digestParts ) == 2 {
2103+ digest = common.DigestSet {
2104+ digestParts [0 ]: digestParts [1 ],
2105+ }
2106+ break
2107+ }
2108+ }
2109+ }
2110+ }
2111+
2112+ if len (digest ) == 0 {
2113+ return nil , xerrors .Errorf ("could not determine digest for image %s" , version )
2114+ }
2115+
2116+ subjectLogger .WithField ("digest" , digest ).Debug ("Found image digest" )
2117+
2118+ // Create subjects for each image
2119+ result := make ([]in_toto.Subject , 0 , len (cfg .Image ))
2120+ for _ , tag := range cfg .Image {
2121+ result = append (result , in_toto.Subject {
2122+ Name : tag ,
2123+ Digest : digest ,
2124+ })
2125+ }
2126+
2127+ return result , nil
2128+ }
2129+ }
2130+
2131+ // DockerImageMetadata holds metadata for exported Docker images
2132+ type DockerImageMetadata struct {
2133+ ImageNames []string `json:"image_names" yaml:"image_names"`
2134+ BuiltVersion string `json:"built_version" yaml:"built_version"`
2135+ Digest string `json:"digest,omitempty" yaml:"digest,omitempty"`
2136+ BuildTime time.Time `json:"build_time" yaml:"build_time"`
2137+ CustomMeta map [string ]string `json:"custom_metadata,omitempty" yaml:"custom_metadata,omitempty"`
2138+ }
2139+
2140+ // createDockerExportMetadata creates metadata file for exported Docker images
2141+ func createDockerExportMetadata (wd , version string , cfg DockerPkgConfig ) error {
2142+ metadata := DockerImageMetadata {
2143+ ImageNames : cfg .Image ,
2144+ BuiltVersion : version ,
2145+ BuildTime : time .Now (),
2146+ CustomMeta : cfg .Metadata ,
2147+ }
2148+
2149+ // Try to get image digest if available
2150+ inspectCmd := exec .Command ("docker" , "inspect" , "--format={{index .Id}}" , version )
2151+ if output , err := inspectCmd .Output (); err == nil {
2152+ metadata .Digest = strings .TrimSpace (string (output ))
2153+ }
2154+
2155+ // Write as JSON for easy parsing
2156+ metadataBytes , err := json .MarshalIndent (metadata , "" , " " )
2157+ if err != nil {
2158+ return fmt .Errorf ("failed to marshal Docker metadata: %w" , err )
2159+ }
2160+
2161+ metadataPath := filepath .Join (wd , "docker-export-metadata.json" )
2162+ if err := os .WriteFile (metadataPath , metadataBytes , 0644 ); err != nil {
2163+ return fmt .Errorf ("failed to write Docker metadata: %w" , err )
2164+ }
2165+
2166+ log .WithField ("path" , metadataPath ).Debug ("Created Docker export metadata" )
2167+ return nil
2168+ }
2169+
2170+
20362171// Update buildGeneric to use compression arg helper
20372172func (p * Package ) buildGeneric (buildctx * buildContext , wd , result string ) (res * packageBuild , err error ) {
20382173 cfg , ok := p .Config .(GenericPkgConfig )
0 commit comments