@@ -18,7 +18,9 @@ package imagepromoter
1818
1919import (
2020 "context"
21+ "errors"
2122 "fmt"
23+ "net/http"
2224 "os"
2325 "strings"
2426
@@ -27,6 +29,7 @@ import (
2729 "github.com/google/go-containerregistry/pkg/crane"
2830 "github.com/google/go-containerregistry/pkg/gcrane"
2931 "github.com/google/go-containerregistry/pkg/name"
32+ "github.com/google/go-containerregistry/pkg/v1/remote/transport"
3033 "github.com/nozzle/throttler"
3134 "github.com/sigstore/sigstore/pkg/tuf"
3235 "github.com/sirupsen/logrus"
@@ -44,6 +47,7 @@ import (
4447const (
4548 oidcTokenAudience = "sigstore"
4649 signatureTagSuffix = ".sig"
50+ sbomTagSuffix = ".sbom"
4751
4852 TestSigningAccount = "k8s-infra-promoter-test-signer@k8s-cip-test-prod.iam.gserviceaccount.com"
4953)
@@ -60,7 +64,7 @@ func (di *DefaultPromoterImplementation) ValidateStagingSignatures(
6064 refsToEdges [ref ] = edge
6165 }
6266
63- refs := []string {}
67+ refs := make ( []string , 0 , len ( refsToEdges ))
6468 for ref := range refsToEdges {
6569 refs = append (refs , ref )
6670 }
@@ -258,7 +262,8 @@ func (di *DefaultPromoterImplementation) copyAttachedObjects(edge *reg.Promotion
258262 if err := crane .Copy (srcRef .String (), dstRef .String (), craneOpts ... ); err != nil {
259263 // If the signature layer does not exist it means that the src image
260264 // is not signed, so we catch the error and return nil
261- if strings .Contains (err .Error (), "MANIFEST_UNKNOWN" ) {
265+ var terr * transport.Error
266+ if errors .As (err , & terr ) && terr .StatusCode == http .StatusNotFound {
262267 logrus .Debugf ("Reference %s is not signed, not copying" , srcRef .String ())
263268 return nil
264269 }
@@ -327,14 +332,71 @@ func (di *DefaultPromoterImplementation) replicateSignatures(
327332 return nil
328333}
329334
330- // WriteSBOMs writes SBOMs to each of the newly promoted images and stores
331- // them along the signatures in the registry.
335+ // WriteSBOMs copies pre-generated SBOMs from the staging registry to each
336+ // production registry for the promoted images. SBOMs are expected to be
337+ // attached in staging (e.g., by the build system) and are identified by
338+ // the cosign SBOM tag convention (sha256-<hash>.sbom).
332339func (di * DefaultPromoterImplementation ) WriteSBOMs (
333- _ * options.Options , _ * reg.SyncContext , _ map [reg.PromotionEdge ]interface {},
340+ _ * options.Options , _ * reg.SyncContext , edges map [reg.PromotionEdge ]interface {},
334341) error {
342+ if len (edges ) == 0 {
343+ logrus .Info ("No images were promoted. No SBOMs to copy." )
344+ return nil
345+ }
346+
347+ for edge := range edges {
348+ // Skip signature and attestation layers
349+ if strings .HasSuffix (string (edge .DstImageTag .Tag ), ".sig" ) ||
350+ strings .HasSuffix (string (edge .DstImageTag .Tag ), ".att" ) ||
351+ strings .HasSuffix (string (edge .DstImageTag .Tag ), ".sbom" ) ||
352+ edge .DstImageTag .Tag == "" {
353+ continue
354+ }
355+
356+ if err := di .copySBOM (& edge ); err != nil {
357+ return fmt .Errorf ("copying SBOM for %s: %w" , edge .DstReference (), err )
358+ }
359+ }
360+
335361 return nil
336362}
337363
364+ // copySBOM copies an SBOM from the staging registry to the production registry
365+ // for a single promotion edge. If no SBOM exists in staging, this is not an error.
366+ func (di * DefaultPromoterImplementation ) copySBOM (edge * reg.PromotionEdge ) error {
367+ sbomTag := digestToSBOMTag (edge .Digest )
368+ srcRefString := fmt .Sprintf (
369+ "%s/%s:%s" , edge .SrcRegistry .Name , edge .SrcImageTag .Name , sbomTag ,
370+ )
371+ dstRefString := fmt .Sprintf (
372+ "%s/%s:%s" , edge .DstRegistry .Name , edge .DstImageTag .Name , sbomTag ,
373+ )
374+
375+ craneOpts := []crane.Option {
376+ crane .WithAuthFromKeychain (gcrane .Keychain ),
377+ crane .WithUserAgent (image .UserAgent ),
378+ crane .WithTransport (di .getSigningTransport ()),
379+ }
380+
381+ logrus .Infof ("SBOM copy: %s to %s" , srcRefString , dstRefString )
382+ if err := crane .Copy (srcRefString , dstRefString , craneOpts ... ); err != nil {
383+ // If the SBOM does not exist in staging, skip silently
384+ var terr * transport.Error
385+ if errors .As (err , & terr ) && terr .StatusCode == http .StatusNotFound {
386+ logrus .Debugf ("No SBOM found for %s, skipping" , srcRefString )
387+ return nil
388+ }
389+ return fmt .Errorf ("copying SBOM %s to %s: %w" , srcRefString , dstRefString , err )
390+ }
391+ return nil
392+ }
393+
394+ // digestToSBOMTag takes a digest and infers the tag name where
395+ // its SBOM can be found.
396+ func digestToSBOMTag (dg image.Digest ) string {
397+ return strings .ReplaceAll (string (dg ), "sha256:" , "sha256-" ) + sbomTagSuffix
398+ }
399+
338400// GetIdentityToken returns an identity token for the selected service account
339401// in order for this function to work, an account has to be already logged. This
340402// can be achieved using the.
0 commit comments