Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 78 additions & 19 deletions pkg/build/build_phase.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ func (phase *BuildPhase) AfterImages(ctx context.Context) error {
return err
}

if err := phase.propagateArtifacts(ctx); err != nil {
return err
}

telemetry.GetTelemetryWerfIO().BuildFinished(ctx, true)

return phase.createReport(ctx, imagesPairs)
Expand Down Expand Up @@ -350,17 +354,16 @@ func (phase *BuildPhase) convergeImageSbom(ctx context.Context, name string, ima
signerIdentity = phase.SbomSigningOptions.Signer().Fingerprint()
}

finalStageDesc := phase.finalStageDescForImage(name, images)
for _, img := range images {
if err := phase.convergePlatformImageSbom(ctx, name, img, finalStageDesc, signer, signerIdentity, breaker); err != nil {
if err := phase.convergePlatformImageSbom(ctx, name, img, signer, signerIdentity, breaker); err != nil {
return err
}
}

return nil
}

func (phase *BuildPhase) convergePlatformImageSbom(ctx context.Context, name string, img *image.Image, finalStageDesc *imagePkg.StageDesc, signer signature.Signer, signerIdentity string, breaker *externalref.ResolverBreaker) error {
func (phase *BuildPhase) convergePlatformImageSbom(ctx context.Context, name string, img *image.Image, signer signature.Signer, signerIdentity string, breaker *externalref.ResolverBreaker) error {
stageDesc := img.GetLastNonEmptyStageDesc()
if stageDesc == nil {
return fmt.Errorf("unable to converge sbom for image %q: stage descriptor is unavailable", name)
Expand Down Expand Up @@ -424,25 +427,81 @@ func (phase *BuildPhase) convergePlatformImageSbom(ctx context.Context, name str
return fmt.Errorf("unable to converge sbom for image %q: %w", name, err)
}

if err := phase.sbomStep.PropagateArtifacts(ctx, name, stageDesc, finalStageDesc, phase.Conveyor.StorageManager.GetCacheStagesStorageList()); err != nil {
return fmt.Errorf("unable to propagate sbom for image %q: %w", name, err)
}

return nil
}

// finalStageDescForImage returns the final repo descriptor to copy the SBOM artifacts into, or nil
// when there is nothing to copy. A single-platform image never has one: publishFinalImage stores the
// final repo descriptor in the content tag desc, which convergeImageSbom already uses as the SBOM
// target. Reaching for the last non-empty stage here instead panics, because an image resolved from
// the cache short-circuits in BeforeImageStages and never gets one.
func (phase *BuildPhase) finalStageDescForImage(name string, images []*image.Image) *imagePkg.StageDesc {
if len(images) == 1 {
return nil
}
if multiImg := phase.Conveyor.imagesTree.GetMultiplatformImage(name); multiImg != nil {
return multiImg.GetFinalStageDesc()
// propagateArtifacts copies the artifacts attached to every image in the stages repo
// into the final repo and the cache repos. It runs after both SBOM and VEX
// convergence so it carries every attached artifact kind, and it runs on every build,
// so a destination holding the image without its artifacts is repaired by the next
// run. For a multi-platform image the per-platform artifacts are copied onto the
// platform manifest digests — preserved by the registry-level index copy — and the
// image-level artifacts (e.g. VEX) onto the index digest.
func (phase *BuildPhase) propagateArtifacts(ctx context.Context) error {
cacheStagesStorageList := phase.Conveyor.StorageManager.GetCacheStagesStorageList()

for _, pair := range phase.Conveyor.imagesTree.GetImagesByName(false) {
name, images := pair.Unpair()

if multiImg := phase.Conveyor.imagesTree.GetMultiplatformImage(name); multiImg != nil {
var finalRepo string
finalDesc := multiImg.GetFinalStageDesc()
if finalDesc != nil {
finalRepo = finalDesc.Info.Repository
}

for _, img := range images {
stageDesc := img.GetContentTagDesc()
if stageDesc == nil {
continue
}
if err := phase.sbomStep.PropagateArtifacts(ctx, name,
stageDesc.Info.Repository, stageDesc.Info.GetDigest(),
finalRepo, stageDesc.Info.GetDigest(),
cacheStagesStorageList,
); err != nil {
return fmt.Errorf("unable to propagate artifacts for image %q (platform %s): %w", name, img.TargetPlatform, err)
}
}

if idxDesc := multiImg.GetStageDesc(); idxDesc != nil {
var finalDigest string
if finalDesc != nil {
finalDigest = finalDesc.Info.GetDigest()
}
if err := phase.sbomStep.PropagateArtifacts(ctx, name,
idxDesc.Info.Repository, idxDesc.Info.GetDigest(),
finalRepo, finalDigest,
cacheStagesStorageList,
); err != nil {
return fmt.Errorf("unable to propagate artifacts for image %q: %w", name, err)
}
}

continue
}

img := images[0]
stageDesc := img.GetContentTagDesc()
if stageDesc == nil {
continue
}

var finalRepo, finalDigest string
if finalDesc := img.GetFinalContentTagDesc(); finalDesc != nil {
finalRepo = finalDesc.Info.Repository
finalDigest = finalDesc.Info.GetDigest()
}

if err := phase.sbomStep.PropagateArtifacts(ctx, name,
stageDesc.Info.Repository, stageDesc.Info.GetDigest(),
finalRepo, finalDigest,
cacheStagesStorageList,
); err != nil {
return fmt.Errorf("unable to propagate artifacts for image %q: %w", name, err)
}
}

return nil
}

Expand Down Expand Up @@ -527,7 +586,7 @@ func (phase *BuildPhase) publishFinalImage(ctx context.Context, name string, img
if err != nil {
return fmt.Errorf("unable to copy image into final repo: %w", err)
}
img.SetContentTagDesc(desc)
img.SetFinalContentTagDesc(desc)

return nil
}
Expand Down
8 changes: 0 additions & 8 deletions pkg/build/build_phase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,12 +277,4 @@ var _ = Describe("BuildPhase", func() {
Expect(digestDisabledAgain).To(Equal(digestBaseline))
})
})

Describe("finalStageDescForImage", func() {
It("returns nil for a single-platform image resolved from the cache, without a built stage image", func() {
phase := &BuildPhase{}

Expect(phase.finalStageDescForImage("app", []*image.Image{{}})).To(BeNil())
})
})
})
2 changes: 1 addition & 1 deletion pkg/build/build_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func createBuildReport(ctx context.Context, phase *BuildPhase, imagePairs []util
targetPlatforms := util.MapFuncToSlice(images, func(img *image.Image) string { return img.TargetPlatform })

for _, img := range images {
imageDesc := img.GetContentTagDesc()
imageDesc := img.GetPublishedContentTagDesc()
var stages []ReportStageRecord
if !img.AnchorReused {
stages = getStagesReport(img, false)
Expand Down
27 changes: 25 additions & 2 deletions pkg/build/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,11 @@ type Image struct {
stageDurations map[stage.StageName]time.Duration
lastNonEmptyStage stage.Interface
contentTagDesc *image.StageDesc
rebuilt bool
useCustomTag bool
// finalContentTagDesc is the content tag descriptor of the image as copied into
// the final repo; contentTagDesc keeps describing the image in the stages repo.
finalContentTagDesc *image.StageDesc
rebuilt bool
useCustomTag bool

baseImageType BaseImageType
baseImageReference string
Expand Down Expand Up @@ -298,6 +301,26 @@ func (i *Image) GetContentTagDesc() *image.StageDesc {
return i.contentTagDesc
}

func (i *Image) SetFinalContentTagDesc(desc *image.StageDesc) {
i.finalContentTagDesc = desc
}

func (i *Image) GetFinalContentTagDesc() *image.StageDesc {
return i.finalContentTagDesc
}

// GetPublishedContentTagDesc returns the descriptor of the image as published for
// consumers: the final repo one when the image was copied there, the stages repo
// one otherwise. Build-internal consumers — artifact convergence, base and import
// SBOM lookups — must keep using GetContentTagDesc, which always describes the
// image in the repository it was built in.
func (i *Image) GetPublishedContentTagDesc() *image.StageDesc {
if i.finalContentTagDesc != nil {
return i.finalContentTagDesc
}
return i.contentTagDesc
}

func (i *Image) GetStage(name stage.StageName) stage.Interface {
for _, s := range i.stages {
if s.Name() == name {
Expand Down
24 changes: 12 additions & 12 deletions pkg/build/sbom_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,26 +199,26 @@ func (step *sbomStep) calculateStableChecksum(scanOpts scanner.ScanOptions, merg
)
}

// PropagateArtifacts copies the artifacts attached to the image stage (e.g. its SBOM)
// into the final repo and the cache repos. Stages themselves are copied there before
// SBOM generation runs, so the artifacts have to catch up separately.
func (step *sbomStep) PropagateArtifacts(ctx context.Context, werfImgName string, stageDesc, finalStageDesc *image.StageDesc, cacheStagesStorageList []storage.StagesStorage) error {
srcRepo := stageDesc.Info.Repository
srcDigest := stageDesc.Info.GetDigest()

if finalStageDesc != nil && finalStageDesc.Info.Repository != srcRepo {
if err := logboek.Context(ctx).Default().LogProcess("image %s: Copy SBOM artifacts into the final repo %s", werfImgName, finalStageDesc.Info.Repository).DoError(func() error {
return artifact.CopyAttachedArtifacts(ctx, srcRepo, srcDigest, finalStageDesc.Info.Repository, finalStageDesc.Info.GetDigest())
// PropagateArtifacts copies the artifacts attached to the image in the repository it
// was built in — its SBOM, VEX and any other attached kind — into the final repo and
// the cache repos. Stages are copied there before the artifacts exist, so the
// artifacts have to catch up separately. The copy runs on every build and is
// idempotent, so a destination holding the image without its artifacts is repaired
// by the next run.
func (step *sbomStep) PropagateArtifacts(ctx context.Context, werfImgName, srcRepo, srcDigest, finalRepo, finalDigest string, cacheStagesStorageList []storage.StagesStorage) error {
if finalRepo != "" && finalRepo != srcRepo {
if err := logboek.Context(ctx).Default().LogProcess("image %s: Copy attached artifacts into the final repo %s", werfImgName, finalRepo).DoError(func() error {
return artifact.CopyAttachedArtifacts(ctx, srcRepo, srcDigest, finalRepo, finalDigest)
}); err != nil {
return fmt.Errorf("copy attached artifacts into final repo %s: %w", finalStageDesc.Info.Repository, err)
return fmt.Errorf("copy attached artifacts into final repo %s: %w", finalRepo, err)
}
}

for _, cache := range cacheStagesStorageList {
if cache.Address() == storage.LocalStorageAddress || cache.Address() == srcRepo {
continue
}
if err := logboek.Context(ctx).Info().LogProcess("image %s: Copy SBOM artifacts into cache %s", werfImgName, cache.String()).DoError(func() error {
if err := logboek.Context(ctx).Info().LogProcess("image %s: Copy attached artifacts into cache %s", werfImgName, cache.String()).DoError(func() error {
return artifact.CopyAttachedArtifacts(ctx, srcRepo, srcDigest, cache.Address(), srcDigest)
}); err != nil {
logboek.Context(ctx).Warn().LogF("Warning: unable to copy attached artifacts into cache stages storage %s: %s\n", cache.String(), err)
Expand Down
23 changes: 6 additions & 17 deletions pkg/build/sbom_step_propagate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (

"github.com/werf/werf/v2/pkg/attestation"
"github.com/werf/werf/v2/pkg/docker_registry"
werfImage "github.com/werf/werf/v2/pkg/image"
"github.com/werf/werf/v2/pkg/oci/artifact"
"github.com/werf/werf/v2/pkg/storage"
"github.com/werf/werf/v2/test/mock"
Expand Down Expand Up @@ -55,16 +54,6 @@ var _ = Describe("SbomStep PropagateArtifacts", func() {
Expect(remote.Write(toRef, img, append([]remote.Option{remote.WithContext(ctx)}, remoteOpts...)...)).To(Succeed())
}

stageDescFor := func(repo, digest string) *werfImage.StageDesc {
return &werfImage.StageDesc{
StageID: &werfImage.StageID{},
Info: &werfImage.Info{
Repository: repo,
RepoDigest: repo + "@" + digest,
},
}
}

cacheStorage := func(address string) storage.StagesStorage {
s := mock.NewMockStagesStorage(gomock.NewController(GinkgoT()))
s.EXPECT().Address().Return(address).AnyTimes()
Expand Down Expand Up @@ -96,7 +85,7 @@ var _ = Describe("SbomStep PropagateArtifacts", func() {
copyImageByDigest(ctx, srcRepo, finalRepo, srcDigest)

step := &sbomStep{}
Expect(step.PropagateArtifacts(ctx, "app", stageDescFor(srcRepo, srcDigest), stageDescFor(finalRepo, srcDigest), nil)).To(Succeed())
Expect(step.PropagateArtifacts(ctx, "app", srcRepo, srcDigest, finalRepo, srcDigest, nil)).To(Succeed())

finalStore := artifact.NewOCIStore(finalRepo, "app", remoteOpts...)
content, err := finalStore.GetAttachedContent(ctx, srcDigest, attestation.DSSEMediaType, nil)
Expand All @@ -113,7 +102,7 @@ var _ = Describe("SbomStep PropagateArtifacts", func() {
cacheStorage(srcRepo),
cacheStorage(cacheRepo),
}
Expect(step.PropagateArtifacts(ctx, "app", stageDescFor(srcRepo, srcDigest), nil, caches)).To(Succeed())
Expect(step.PropagateArtifacts(ctx, "app", srcRepo, srcDigest, "", "", caches)).To(Succeed())

cacheStore := artifact.NewOCIStore(cacheRepo, "app", remoteOpts...)
content, err := cacheStore.GetAttachedContent(ctx, srcDigest, attestation.DSSEMediaType, nil)
Expand All @@ -123,23 +112,23 @@ var _ = Describe("SbomStep PropagateArtifacts", func() {

It("should do nothing without a final repo and caches", func(ctx SpecContext) {
step := &sbomStep{}
Expect(step.PropagateArtifacts(ctx, "app", stageDescFor(srcRepo, srcDigest), nil, nil)).To(Succeed())
Expect(step.PropagateArtifacts(ctx, "app", srcRepo, srcDigest, "", "", nil)).To(Succeed())
})

It("should skip the final repo when it matches the stages repo", func(ctx SpecContext) {
step := &sbomStep{}
Expect(step.PropagateArtifacts(ctx, "app", stageDescFor(srcRepo, srcDigest), stageDescFor(srcRepo, srcDigest), nil)).To(Succeed())
Expect(step.PropagateArtifacts(ctx, "app", srcRepo, srcDigest, srcRepo, srcDigest, nil)).To(Succeed())
})

It("should not fail when a cache repo is unreachable", func(ctx SpecContext) {
step := &sbomStep{}
caches := []storage.StagesStorage{cacheStorage("127.0.0.1:1/unreachable/cache")}
Expect(step.PropagateArtifacts(ctx, "app", stageDescFor(srcRepo, srcDigest), nil, caches)).To(Succeed())
Expect(step.PropagateArtifacts(ctx, "app", srcRepo, srcDigest, "", "", caches)).To(Succeed())
})

It("should fail when the final repo copy fails", func(ctx SpecContext) {
step := &sbomStep{}
err := step.PropagateArtifacts(ctx, "app", stageDescFor(srcRepo, srcDigest), stageDescFor("127.0.0.1:1/unreachable/final", srcDigest), nil)
err := step.PropagateArtifacts(ctx, "app", srcRepo, srcDigest, "127.0.0.1:1/unreachable/final", srcDigest, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("copy attached artifacts into final repo"))
})
Expand Down
11 changes: 11 additions & 0 deletions pkg/build/stages/remote_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/werf/werf/v2/pkg/build"
"github.com/werf/werf/v2/pkg/docker_registry"
"github.com/werf/werf/v2/pkg/image"
"github.com/werf/werf/v2/pkg/oci/artifact"
"github.com/werf/werf/v2/pkg/ref"
"github.com/werf/werf/v2/pkg/storage/manager"
)
Expand Down Expand Up @@ -83,6 +84,12 @@ func (s *RemoteStorage) copyCurrentBuildStagesFromRemote(ctx context.Context, fr
if err = fromRemote.RegistryClient.CopyImage(ctx, infoGetterName, reference.FullName(), docker_registry.CopyImageOptions{}); err != nil {
return fmt.Errorf("error copying stage %s into %s: %w", infoGetterName, reference.FullName(), err)
}

if infoGetter.Digest != "" {
if err := artifact.CopyAllAttachedArtifacts(ctx, infoGetter.Repo, infoGetter.Digest, reference.Repo, infoGetter.Digest); err != nil {
return fmt.Errorf("error copying artifacts attached to stage %s into %s: %w", infoGetterName, reference.Repo, err)
}
}
}

return nil
Expand Down Expand Up @@ -116,6 +123,10 @@ func (s *RemoteStorage) copyAllFromRemote(ctx context.Context, fromRemote *Remot
if err = fromRemote.RegistryClient.CopyImage(ctx, stageName, reference.FullName(), docker_registry.CopyImageOptions{}); err != nil {
return fmt.Errorf("error copying stage %s into %s: %w", stageName, reference.FullName(), err)
}

if err := artifact.CopyAllAttachedArtifacts(ctx, stageDesc.Info.Repository, stageDesc.Info.GetDigest(), reference.Repo, stageDesc.Info.GetDigest()); err != nil {
return fmt.Errorf("error copying artifacts attached to stage %s into %s: %w", stageName, reference.Repo, err)
}
}

return nil
Expand Down
6 changes: 3 additions & 3 deletions pkg/cleaning/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -1082,17 +1082,17 @@ func deleteOrphanedArtifacts(ctx context.Context, stagesStorage storage.StagesSt
}

func (m *cleanupManager) cleanupFinalStages(ctx context.Context) error {
// Skip stages from the final repo that are not exist in the repo.
// The final repo mirrors the stages repo: a final stage whose stage ID is still
// present in the stages repo after cleanup is kept, the rest are deleted.
// Note: we cannot make difference between repo and final because they have different stage descriptions.
FilterOutFinalStages:
for finalStageDesc := range m.stageManager.GetFinalStageDescSet().Iter() {
for stageDesc := range m.stageManager.GetStageDescSet().Iter() {
if stageDesc.StageID.IsEqual(*finalStageDesc.StageID) {
m.stageManager.MarkFinalStageDescAsProtected(finalStageDesc, stage_manager.ProtectionReasonFoundInRepo, false)
continue FilterOutFinalStages
}
}

m.stageManager.MarkFinalStageDescAsProtected(finalStageDesc, stage_manager.ProtectionReasonNotFoundInRepo, false)
}

for reason, finalStageDescSetToKeep := range m.stageManager.GetFinalProtectedStageDescSetByReason() {
Expand Down
Loading