Skip to content
Open
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
16 changes: 8 additions & 8 deletions docs/_data/werf_yaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -588,23 +588,23 @@ sections:
- name: type
value: "string"
description:
en: "Package ecosystem type (go-mod)"
ru: "Тип экосистемы пакетов (go-mod)"
en: "Package ecosystem type (go-mod, python-uv, python-pip, python-poetry)"
ru: "Тип экосистемы пакетов (go-mod, python-uv, python-pip, python-poetry)"
- name: workdir
value: "string"
description:
en: "Directory inside the image that contains the module files (required for go-mod)"
ru: "Директория внутри образа, содержащая файлы модуля (обязательно для go-mod)"
en: "Directory inside the image that contains the spec files (required for go-mod and python-*)"
ru: "Директория внутри образа, содержащая файлы спецификации (обязательно для go-mod и python-*)"
- name: spec
value: "string"
description:
en: "Module file name (for go-mod, default: go.mod)"
ru: "Имя файла модуля (для go-mod, по умолчанию: go.mod)"
en: "Spec file name (defaults: go.mod for go-mod, pyproject.toml for python-uv/python-poetry, requirements.txt for python-pip)"
ru: "Имя файла спецификации (по умолчанию: go.mod для go-mod, pyproject.toml для python-uv/python-poetry, requirements.txt для python-pip)"
- name: lock
value: "string"
description:
en: "Checksum file name (for go-mod, default: go.sum)"
ru: "Имя файла контрольных сумм (для go-mod, по умолчанию: go.sum)"
en: "Lock file name (defaults: go.sum for go-mod, uv.lock for python-uv, poetry.lock for python-poetry; empty for python-pip)"
ru: "Имя lock-файла (по умолчанию: go.sum для go-mod, uv.lock для python-uv, poetry.lock для python-poetry; пусто для python-pip)"
- <<: *meta-section-build-cache-version
- name: platform
description:
Expand Down
2 changes: 1 addition & 1 deletion docs/pages_en/usage/build/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ Currently, this option uses the following _defaults_:
| Property | Value |
|-----------------------------------|----------------------------------------------------------------------------------------|
| **Scanner** | syft |
| **Scanner Image** | anchore/syft:v1.23.1 |
| **Scanner Image** | anchore/syft:v1.45.1 |
| **Image Pull Policy** | `PullIfMissing` |
| **Data Source Connection Method** | daemon + socket via volume (for Docker) |
| **Path in Source Image** | OS root |
Expand Down
2 changes: 1 addition & 1 deletion docs/pages_ru/usage/build/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ build:
| Свойство | Значение |
|-------------------------------------------|------------------------------------------------------------------------------------------|
| **Сканер** | syft |
| **Образ сканера** | anchore/syft:v1.23.1 |
| **Образ сканера** | anchore/syft:v1.45.1 |
| **Политика получения образа** | `PullIfMissing` |
| **Способ подключения к источнику данных** | daemon + socket via volume (для Docker) |
| **Путь в образе источнике** | корень OS |
Expand Down
86 changes: 58 additions & 28 deletions pkg/build/stage/packages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var _ = Describe("PackagesStage", func() {
)

It("returns stage when packages commands present", func(ctx context.Context) {
stage := generateTestPackagesStage(ctx, "cd /app && go mod download")
stage := generateTestPackagesStage(ctx, "cd \"/app\" && go mod download")
Expect(stage).NotTo(BeNil())
})
})
Expand All @@ -42,7 +42,7 @@ var _ = Describe("PackagesStage", func() {
)

It("Name returns packages", func(ctx context.Context) {
stage := generateTestPackagesStage(ctx, "cd /app && go mod download")
stage := generateTestPackagesStage(ctx, "cd \"/app\" && go mod download")
Expect(stage.Name()).To(Equal(Packages))
})

Expand All @@ -67,21 +67,21 @@ var _ = Describe("PackagesStage", func() {
},

Entry("same commands produce same hash",
[]string{"cd /app && go mod download"},
[]string{"cd /app && go mod download"},
[]string{"cd \"/app\" && go mod download"},
[]string{"cd \"/app\" && go mod download"},
true),
Entry("different commands produce different hash",
[]string{"cd /app && go mod download"},
[]string{"cd /lib && go mod download"},
[]string{"cd \"/app\" && go mod download"},
[]string{"cd \"/lib\" && go mod download"},
false),
Entry("different number of commands produce different hash",
[]string{"cd /app && go mod download"},
[]string{"cd /app && go mod download", "cd /lib && go mod download"},
[]string{"cd \"/app\" && go mod download"},
[]string{"cd \"/app\" && go mod download", "cd \"/lib\" && go mod download"},
false),
)

It("returns non-empty hash", func(ctx context.Context) {
s := generateTestPackagesStage(ctx, "cd /app && go mod download")
s := generateTestPackagesStage(ctx, "cd \"/app\" && go mod download")
digest, err := s.GetDependencies(ctx, testConveyor(), nil, nil, nil, nil)
Expect(err).NotTo(HaveOccurred())
Expect(digest).NotTo(BeEmpty())
Expand All @@ -92,7 +92,7 @@ var _ = Describe("PackagesStage", func() {
It("does not affect install stage creation", func(ctx context.Context) {
imageBaseConfig := &config.StapelImageBase{
Shell: &config.Shell{
Packages: []string{"cd /app && go mod download"},
Packages: []string{"cd \"/app\" && go mod download"},
Install: []string{"go build ./..."},
},
}
Expand Down Expand Up @@ -157,21 +157,21 @@ var _ = Describe("GeneratePackagesCommands", func() {
},

Entry("go-mod /app", []*config.PackagesDirective{
{Type: config.PackagesDirectiveTypeGoMod, GoMod: config.GoModSpec{Workdir: "/app"}},
}, []string{"cd /app && go mod download"}),
{Type: config.PackagesDirectiveTypeGoMod, FileBased: config.FileBasedSpec{Workdir: "/app"}},
}, []string{"cd \"/app\" && go mod download"}),

Entry("go-mod /src/backend", []*config.PackagesDirective{
{Type: config.PackagesDirectiveTypeGoMod, GoMod: config.GoModSpec{Workdir: "/src/backend"}},
}, []string{"cd /src/backend && go mod download"}),
{Type: config.PackagesDirectiveTypeGoMod, FileBased: config.FileBasedSpec{Workdir: "/src/backend"}},
}, []string{"cd \"/src/backend\" && go mod download"}),

Entry("multiple go-mod entries", []*config.PackagesDirective{
{Type: config.PackagesDirectiveTypeGoMod, GoMod: config.GoModSpec{Workdir: "/app"}},
{Type: config.PackagesDirectiveTypeGoMod, GoMod: config.GoModSpec{Workdir: "/lib"}},
}, []string{"cd /app && go mod download", "cd /lib && go mod download"}),
{Type: config.PackagesDirectiveTypeGoMod, FileBased: config.FileBasedSpec{Workdir: "/app"}},
{Type: config.PackagesDirectiveTypeGoMod, FileBased: config.FileBasedSpec{Workdir: "/lib"}},
}, []string{"cd \"/app\" && go mod download", "cd \"/lib\" && go mod download"}),

Entry("root workdir", []*config.PackagesDirective{
{Type: config.PackagesDirectiveTypeGoMod, GoMod: config.GoModSpec{Workdir: "/"}},
}, []string{"cd / && go mod download"}),
{Type: config.PackagesDirectiveTypeGoMod, FileBased: config.FileBasedSpec{Workdir: "/"}},
}, []string{"cd \"/\" && go mod download"}),

Entry("unsupported type", []*config.PackagesDirective{
{Type: config.PackagesDirectiveType("unknown")},
Expand All @@ -187,11 +187,41 @@ var _ = Describe("GeneratePackagesCommands", func() {
{Type: config.PackagesDirectiveTypeOSPM, Spec: config.PackagesSpec{Packages: []string{"curl", "wget", "jq"}}},
}, []string{config.ContainerFactoryVersionSnapshotCmd(), "pm install curl", "pm install wget", "pm install jq"}),

Entry("mixed types: os-pm and go-mod produce commands", []*config.PackagesDirective{
Entry("mixed types: os-pm and go-mod skip unsupported cargo type", []*config.PackagesDirective{
{Type: config.PackagesDirectiveTypeOSPM, Spec: config.PackagesSpec{Packages: []string{"curl"}}},
{Type: config.PackagesDirectiveTypeGoMod, GoMod: config.GoModSpec{Workdir: "/app"}},
{Type: config.PackagesDirectiveType("pip")},
}, []string{config.ContainerFactoryVersionSnapshotCmd(), "pm install curl", "cd /app && go mod download"}),
{Type: config.PackagesDirectiveTypeGoMod, FileBased: config.FileBasedSpec{Workdir: "/app"}},
{Type: config.PackagesDirectiveType("cargo")},
}, []string{config.ContainerFactoryVersionSnapshotCmd(), "pm install curl", "cd \"/app\" && go mod download"}),

Entry("python-pip /app requirements.txt", []*config.PackagesDirective{
{
Type: config.PackagesDirectiveTypePythonPip,
FileBased: config.FileBasedSpec{Workdir: "/app", Spec: "requirements.txt"},
},
}, []string{"cd \"/app\" && pip install --no-cache-dir -r \"requirements.txt\""}),

Entry("python-poetry /svc", []*config.PackagesDirective{
{
Type: config.PackagesDirectiveTypePythonPoetry,
FileBased: config.FileBasedSpec{Workdir: "/svc"},
},
}, []string{"cd \"/svc\" && poetry install --no-root"}),

Entry("python-uv /api", []*config.PackagesDirective{
{
Type: config.PackagesDirectiveTypePythonUV,
FileBased: config.FileBasedSpec{Workdir: "/api"},
},
}, []string{"cd \"/api\" && uv sync --frozen"}),

Entry("mixed: go-mod + python-uv + os-pm all produce commands", []*config.PackagesDirective{
{Type: config.PackagesDirectiveTypeGoMod, FileBased: config.FileBasedSpec{Workdir: "/app"}},
{
Type: config.PackagesDirectiveTypePythonUV,
FileBased: config.FileBasedSpec{Workdir: "/lib"},
},
{Type: config.PackagesDirectiveTypeOSPM, Spec: config.PackagesSpec{Packages: []string{"curl"}}},
}, []string{"cd \"/app\" && go mod download", "cd \"/lib\" && uv sync --frozen", config.ContainerFactoryVersionSnapshotCmd(), "pm install curl"}),
)
})

Expand Down Expand Up @@ -228,7 +258,7 @@ var _ = Describe("Shell.Packages config field", func() {
}
},

Entry("populated", &config.Shell{Packages: []string{"cd /app && go mod download"}}, 1, "cd /app && go mod download"),
Entry("populated", &config.Shell{Packages: []string{"cd \"/app\" && go mod download"}}, 1, "cd \"/app\" && go mod download"),
Entry("empty", &config.Shell{}, 0, ""),
)

Expand Down Expand Up @@ -260,12 +290,12 @@ var _ = Describe("Builder interface Packages methods", func() {
},

Entry("same commands — same checksum",
[]string{"cd /app && go mod download"},
[]string{"cd /app && go mod download"},
[]string{"cd \"/app\" && go mod download"},
[]string{"cd \"/app\" && go mod download"},
true),
Entry("different commands — different checksum",
[]string{"cd /app && go mod download"},
[]string{"cd /lib && go mod download"},
[]string{"cd \"/app\" && go mod download"},
[]string{"cd \"/lib\" && go mod download"},
false),
)

Expand Down
22 changes: 22 additions & 0 deletions pkg/config/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package config

import (
. "github.com/onsi/gomega"
"gopkg.in/yaml.v2"
)

func directivesFromYaml(giterminismManager *GiterminismManagerStub, yamlMap map[string]interface{}) ([]*PackagesDirective, error) {
rawYaml, err := yaml.Marshal(yamlMap)
Expect(err).To(Succeed())

doc := &doc{Content: rawYaml}
rawStapelImage := &rawStapelImage{doc: doc}

Expect(yaml.UnmarshalStrict(doc.Content, rawStapelImage)).To(Succeed())

stapelImage, err := rawStapelImage.toStapelImageDirective(giterminismManager, &Meta{}, "image1")
if err != nil {
return nil, err
}
return stapelImage.Packages, nil
}
9 changes: 5 additions & 4 deletions pkg/config/packages_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ func GeneratePackagesCommands(packages []*PackagesDirective) []string {
var commands []string
snapshotted := false
for _, pkg := range packages {
switch pkg.Type {
case PackagesDirectiveTypeOSPM:
if pkg.Type == PackagesDirectiveTypeOSPM {
if len(pkg.Spec.Packages) == 0 {
continue
}
Expand All @@ -29,8 +28,10 @@ func GeneratePackagesCommands(packages []*PackagesDirective) []string {
for _, p := range pkg.Spec.Packages {
commands = append(commands, fmt.Sprintf("pm install %s", p))
}
case PackagesDirectiveTypeGoMod:
commands = append(commands, fmt.Sprintf("cd %s && go mod download", pkg.GoMod.Workdir))
continue
}
if eco, ok := ecosystems[pkg.Type]; ok {
commands = append(commands, eco.InstallCmd(pkg.FileBased.Workdir, pkg.FileBased.Spec))
}
}
return commands
Expand Down
85 changes: 64 additions & 21 deletions pkg/config/packages_directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,97 @@ package config

import (
"fmt"
"maps"
"sort"
)

type PackagesDirectiveType string

const (
PackagesDirectiveTypeOSPM PackagesDirectiveType = "os-pm"
PackagesDirectiveTypeGoMod PackagesDirectiveType = "go-mod"
)

const (
goModDefaultSpec = "go.mod"
goModDefaultLock = "go.sum"
PackagesDirectiveTypeOSPM PackagesDirectiveType = "os-pm"
PackagesDirectiveTypeGoMod PackagesDirectiveType = "go-mod"
PackagesDirectiveTypePythonUV PackagesDirectiveType = "python-uv"
PackagesDirectiveTypePythonPip PackagesDirectiveType = "python-pip"
PackagesDirectiveTypePythonPoetry PackagesDirectiveType = "python-poetry"
)

type PackagesSpec struct {
Packages []string
}

// GoModSpec describes Go module files inside the image. Workdir is the directory
// holding the module files; Spec and Lock default to "go.mod" and "go.sum".
type GoModSpec struct {
type FileBasedSpec struct {
Workdir string
Spec string
Lock string
}

type PackageEcosystem struct {
Type PackagesDirectiveType
DefaultSpec string
DefaultLock string
InstallCmd func(workdir, spec string) string
CatalogerName string
}

var ecosystems = map[PackagesDirectiveType]PackageEcosystem{
PackagesDirectiveTypeGoMod: {
Type: PackagesDirectiveTypeGoMod,
DefaultSpec: "go.mod",
DefaultLock: "go.sum",
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && go mod download", workdir) },
CatalogerName: "go-module-file-cataloger",
},
PackagesDirectiveTypePythonUV: {
Type: PackagesDirectiveTypePythonUV,
DefaultSpec: "pyproject.toml",
DefaultLock: "uv.lock",
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && uv sync --frozen", workdir) },
CatalogerName: "python-installed-package-cataloger",
},
PackagesDirectiveTypePythonPip: {
Type: PackagesDirectiveTypePythonPip,
DefaultSpec: "requirements.txt",
DefaultLock: "",
InstallCmd: func(workdir, spec string) string {
return fmt.Sprintf("cd %q && pip install --no-cache-dir -r %q", workdir, spec)
},
CatalogerName: "python-installed-package-cataloger",
},
PackagesDirectiveTypePythonPoetry: {
Type: PackagesDirectiveTypePythonPoetry,
DefaultSpec: "pyproject.toml",
DefaultLock: "poetry.lock",
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && poetry install --no-root", workdir) },
CatalogerName: "python-installed-package-cataloger",
},
}

// Ecosystems returns a defensive copy of the file-based package ecosystems registry.
// The returned map is safe to iterate; mutating it does not affect the internal registry.
func Ecosystems() map[PackagesDirectiveType]PackageEcosystem {
return maps.Clone(ecosystems)
}

type PackagesDirective struct {
Type PackagesDirectiveType
Spec PackagesSpec
GoMod GoModSpec
Type PackagesDirectiveType
Spec PackagesSpec
FileBased FileBasedSpec
}

func (d *PackagesDirective) validate() error {
switch d.Type {
case PackagesDirectiveTypeOSPM:
if d.Type == PackagesDirectiveTypeOSPM {
if len(d.Spec.Packages) == 0 {
return fmt.Errorf("packages spec must not be empty for type %q", d.Type)
}
case PackagesDirectiveTypeGoMod:
if d.GoMod.Workdir == "" {
return nil
}
if _, ok := ecosystems[d.Type]; ok {
if d.FileBased.Workdir == "" {
return fmt.Errorf("the `workdir` is required for type %q", d.Type)
}
default:
return fmt.Errorf("unsupported packages type %q", d.Type)
return nil
}

return nil
return fmt.Errorf("unsupported packages type %q", d.Type)
}

// normalizePackages flattens all packages across every directive, deduplicates
Expand Down
Loading
Loading