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
Binary file added pkg/build/build.test
Binary file not shown.
8 changes: 8 additions & 0 deletions pkg/build/build_phase.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/werf/werf/v2/pkg/sbom/externalref"
"github.com/werf/werf/v2/pkg/sbom/gomod"
sbomImage "github.com/werf/werf/v2/pkg/sbom/image"
osPm "github.com/werf/werf/v2/pkg/sbom/packages/os_pm"
"github.com/werf/werf/v2/pkg/sbom/scanner"
"github.com/werf/werf/v2/pkg/stapel"
"github.com/werf/werf/v2/pkg/storage"
Expand Down Expand Up @@ -331,9 +332,16 @@ func (phase *BuildPhase) convergeImageSbom(ctx context.Context, name string, ima

goModPatcher := gomod.NewBOMPatcher(gitRepo, commit, imageContext)

var hasOsPmPackages bool
if primaryImg.StapelImageConfig != nil && primaryImg.StapelImageConfig.ImageBaseConfig() != nil {
hasOsPmPackages = len(primaryImg.StapelImageConfig.ImageBaseConfig().Packages) > 0
}
osPmPatcher := osPm.NewBOMPatcher(stageDesc.Info.Name, hasOsPmPackages)

patchers := []BOMPatcherInterface{
externalRefPatcher,
goModPatcher,
osPmPatcher,
}

if err := phase.sbomStep.ConvergeWithMerge(ctx, name, stageDesc, scanner.DefaultSyftScanOptions(), mergeOpts, patchers, primaryImg.TargetPlatform); err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/build/image/stapel.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func initStages(ctx context.Context, image *Image, metaConfig *config.Meta, stap
stages = append(stages, stage.NewGitArchiveStage(gitArchiveStageOptions, baseStageOptions))
}

stages = appendIfExist(ctx, stages, stage.GeneratePackagesInstallStage(ctx, imageBaseConfig, baseStageOptions))
stages = appendIfExist(ctx, stages, stage.GenerateInstallStage(ctx, imageBaseConfig, gitPatchStageOptions, baseStageOptions))
stages = appendIfExist(ctx, stages, stage.GenerateDependenciesAfterInstallStage(imageBaseConfig, baseStageOptions))
stages = appendIfExist(ctx, stages, stage.GenerateBeforeSetupStage(ctx, imageBaseConfig, gitPatchStageOptions, baseStageOptions))
Expand Down
2 changes: 2 additions & 0 deletions pkg/build/stage/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
From StageName = "from"
BeforeInstall StageName = "beforeInstall"
DependenciesBeforeInstall StageName = "dependenciesBeforeInstall"
PackagesInstall StageName = "packagesInstall"
GitArchive StageName = "gitArchive"
Install StageName = "install"
DependenciesAfterInstall StageName = "dependenciesAfterInstall"
Expand Down Expand Up @@ -63,6 +64,7 @@ var AllStages = []StageName{
From,
BeforeInstall,
DependenciesBeforeInstall,
PackagesInstall,
GitArchive,
Install,
DependenciesAfterInstall,
Expand Down
57 changes: 57 additions & 0 deletions pkg/build/stage/packages_install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package stage

import (
"context"
"fmt"
"strings"

"github.com/werf/common-go/pkg/util"
"github.com/werf/werf/v2/pkg/config"
"github.com/werf/werf/v2/pkg/container_backend"
)

func GeneratePackagesInstallStage(_ context.Context, imageBaseConfig *config.StapelImageBase, baseStageOptions *BaseStageOptions) *PackagesInstallStage {
var resolvedPackages []string
for _, pkg := range imageBaseConfig.Packages {
if pkg.Type != config.PackagesDirectiveTypeOSPM {
continue
}
resolvedPackages = append(resolvedPackages, pkg.Spec.Packages...)
}

if len(resolvedPackages) == 0 {
return nil
}

s := &PackagesInstallStage{}
s.resolvedPackages = resolvedPackages
s.BaseStage = NewBaseStage(PackagesInstall, baseStageOptions)

return s
}

type PackagesInstallStage struct {
*BaseStage

resolvedPackages []string
}

func (s *PackagesInstallStage) GetDependencies(_ context.Context, _ Conveyor, _ container_backend.ContainerBackend, _, _ *StageImage, _ container_backend.BuildContextArchiver) (string, error) {
return util.Sha256Hash(s.resolvedPackages...), nil
}

func (s *PackagesInstallStage) PrepareImage(ctx context.Context, c Conveyor, cb container_backend.ContainerBackend, prevBuiltImage, stageImage *StageImage, _ container_backend.BuildContextArchiver) error {
if err := s.BaseStage.PrepareImage(ctx, c, cb, prevBuiltImage, stageImage, nil); err != nil {
return fmt.Errorf("error preparing base stage: %w", err)
}

installCmd := "pm install " + strings.Join(s.resolvedPackages, " ")

if c.UseLegacyStapelBuilder(cb) {
stageImage.Builder.LegacyStapelStageBuilder().BuilderContainer().AddRunCommands(installCmd)
} else {
stageImage.Builder.StapelStageBuilder().AddCommands(installCmd)
}

return nil
}
174 changes: 174 additions & 0 deletions pkg/build/stage/packages_install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package stage

import (
"context"
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/types"

"github.com/werf/common-go/pkg/util"
"github.com/werf/werf/v2/pkg/config"
"github.com/werf/werf/v2/pkg/container_backend/stage_builder"
imagePkg "github.com/werf/werf/v2/pkg/image"
)

var _ = Describe("PackagesInstallStage", func() {
DescribeTable("GeneratePackagesInstallStage", testGeneratePackagesInstallStage,
Entry("returns nil when Packages is nil",
&config.StapelImageBase{},
BeNil(),
),

Entry("returns nil when Packages is empty",
&config.StapelImageBase{
Packages: []*config.PackagesDirective{},
},
BeNil(),
),

Entry("returns nil when Packages has empty spec",
&config.StapelImageBase{
Packages: []*config.PackagesDirective{
{
Type: config.PackagesDirectiveTypeOSPM,
},
},
},
BeNil(),
),

Entry("creates stage with single package",
&config.StapelImageBase{
Packages: []*config.PackagesDirective{
{
Type: config.PackagesDirectiveTypeOSPM,
Spec: config.PackagesSpec{Packages: []string{"curl"}},
},
},
},
ConsistOf("curl"),
),

Entry("creates stage with multiple packages",
&config.StapelImageBase{
Packages: []*config.PackagesDirective{
{
Type: config.PackagesDirectiveTypeOSPM,
Spec: config.PackagesSpec{Packages: []string{"curl", "jq"}},
},
},
},
ConsistOf("curl", "jq"),
),
)

Describe("GetDependencies", func() {
DescribeTable("should return deterministic hash based on resolved packages",
func(ctx context.Context, packages []string) {
stage := &PackagesInstallStage{
resolvedPackages: packages,
BaseStage: NewBaseStage(PackagesInstall, &BaseStageOptions{}),
}
digest, err := stage.GetDependencies(ctx, nil, nil, nil, nil, nil)
Expect(err).To(Succeed())

expected := util.Sha256Hash(packages...)
Expect(digest).To(Equal(expected))
},

Entry("empty list", []string{}),
Entry("single package", []string{"curl"}),
Entry("multiple packages", []string{"curl", "jq"}),
)

It("should be consistent for same packages", func(ctx context.Context) {
stage1 := &PackagesInstallStage{
resolvedPackages: []string{"curl", "jq"},
BaseStage: NewBaseStage(PackagesInstall, &BaseStageOptions{}),
}
stage2 := &PackagesInstallStage{
resolvedPackages: []string{"curl", "jq"},
BaseStage: NewBaseStage(PackagesInstall, &BaseStageOptions{}),
}

digest1, err := stage1.GetDependencies(ctx, nil, nil, nil, nil, nil)
Expect(err).To(Succeed())

digest2, err := stage2.GetDependencies(ctx, nil, nil, nil, nil, nil)
Expect(err).To(Succeed())

Expect(digest1).To(Equal(digest2))
})

It("should produce different hashes for different packages", func(ctx context.Context) {
stage1 := &PackagesInstallStage{
resolvedPackages: []string{"curl"},
BaseStage: NewBaseStage(PackagesInstall, &BaseStageOptions{}),
}
stage2 := &PackagesInstallStage{
resolvedPackages: []string{"jq"},
BaseStage: NewBaseStage(PackagesInstall, &BaseStageOptions{}),
}

digest1, err := stage1.GetDependencies(ctx, nil, nil, nil, nil, nil)
Expect(err).To(Succeed())

digest2, err := stage2.GetDependencies(ctx, nil, nil, nil, nil, nil)
Expect(err).To(Succeed())

Expect(digest1).NotTo(Equal(digest2))
})
})

Describe("PrepareImage", func() {
const commit = "9d8059842b6fde712c58315ca0ab4713d90761c0"

It("should prepare image with stapel builder and add commands", func(ctx SpecContext) {
conveyor := &nonLegacyDependenciesConveyorStub{
ConveyorStub: NewConveyorStubForDependencies(
NewGiterminismManagerStub(NewLocalGitRepoStub(commit), NewGiterminismInspectorStub()),
nil,
),
}
containerBackend := NewContainerBackendStub()

stage := &PackagesInstallStage{
resolvedPackages: []string{"curl", "jq"},
BaseStage: NewBaseStage(PackagesInstall, &BaseStageOptions{ImageName: "test-image"}),
}

_, stageBuilder, stageImage := newStageImage(containerBackend)

err := stage.PrepareImage(ctx, conveyor, containerBackend, nil, stageImage, nil)
Expect(err).To(Succeed())

sb := stageBuilder.GetStapelStageBuilderImplementation()
Expect(sb).NotTo(BeNil())
Expect(sb.Commands).To(ContainElement("pm install curl jq"))
Expect(sb.Labels).To(ContainElement(fmt.Sprintf("%s=%s", imagePkg.WerfProjectRepoCommitLabel, commit)))
})
})
})

func testGeneratePackagesInstallStage(ctx context.Context, imageBaseConfig *config.StapelImageBase, packagesMatcher types.GomegaMatcher) {
options := &BaseStageOptions{ImageName: "test-image", ProjectName: "test-project"}
stage := GeneratePackagesInstallStage(ctx, imageBaseConfig, options)

var packages []string
if stage != nil {
packages = stage.resolvedPackages
}
Expect(packages).To(packagesMatcher)
}

func newStageImage(containerBackend *ContainerBackendStub) (*LegacyImageStub, *stage_builder.StageBuilder, *StageImage) {
img := NewLegacyImageStub()
stageBuilder := stage_builder.NewStageBuilder(containerBackend, "", img)

return img, stageBuilder, &StageImage{
Image: img,
Builder: stageBuilder,
}
}
67 changes: 67 additions & 0 deletions pkg/config/packages_directive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package config

import (
"fmt"
"sort"
)

// PackagesDirectiveType enumerates supported package source types.
type PackagesDirectiveType string

const (
PackagesDirectiveTypeOSPM PackagesDirectiveType = "os-pm"
)

// PackagesSpec stores a list of package names resolved from a packages directive entry.
type PackagesSpec struct {
Packages []string
}

// PackagesDirective represents a single entry in the image-level packages list.
type PackagesDirective struct {
Type PackagesDirectiveType
Spec PackagesSpec
}

func (d *PackagesDirective) validate() error {
if d.Type != PackagesDirectiveTypeOSPM {
return fmt.Errorf("unsupported packages type %q", d.Type)
}

if len(d.Spec.Packages) == 0 {
return fmt.Errorf("packages spec must not be empty for type %q", d.Type)
}

return nil
}

// normalizePackages flattens all packages across every directive, deduplicates
// and sorts them, and returns a single directive with the normalized list.
// This is called during config conversion so that the build stage receives
// a ready-to-use package list without needing to re-resolve or deduplicate.
func normalizePackages(packages []*PackagesDirective) []*PackagesDirective {
seen := map[string]bool{}
var all []string

for _, p := range packages {
for _, name := range p.Spec.Packages {
if !seen[name] {
seen[name] = true
all = append(all, name)
}
}
}

if len(all) == 0 {
return nil
}

sort.Strings(all)

return []*PackagesDirective{
{
Type: PackagesDirectiveTypeOSPM,
Spec: PackagesSpec{Packages: all},
},
}
}
Loading