Skip to content

Commit 97c0856

Browse files
committed
frontend: rewrite HandleDepsOnly to use BuildContainerWithPackages
Replace the broken --downloadonly --alldeps approach in HandleDepsOnly with a new BuildContainerWithPackages method that passes package names directly to the package manager for resolution from repos. This avoids the --alldeps flag which is unsupported by dnf. Add deps-only integration tests for all RPM distros with two sub-tests: - minimal spec: only runtime deps, verifies curl is installed - full spec: includes sources, build steps, and a shell script artifact; verifies runtime deps are installed and build artifacts are excluded - replaces the "e2e" test in docker-bake.hcl and tets all relevant distros Signed-off-by: Brian Goff <cpuguy83@gmail.com>
1 parent f75b4df commit 97c0856

File tree

7 files changed

+160
-71
lines changed

7 files changed

+160
-71
lines changed

docker-bake.hcl

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ group "default" {
33
}
44

55
group "test" {
6-
targets = ["runc-test", "test-deps-only"]
6+
targets = ["runc-test"]
77
}
88

99
variable "FRONTEND_REF" {
@@ -227,42 +227,6 @@ target "examples" {
227227
tags = ["local/dalec/examples/${f}:${distro}"]
228228
}
229229

230-
target "deps-only" {
231-
name = "deps-only-${distro}"
232-
matrix = {
233-
distro = ["mariner2"]
234-
}
235-
dockerfile-inline = <<EOT
236-
dependencies:
237-
runtime:
238-
patch: {}
239-
bash: {}
240-
EOT
241-
args = {
242-
"BUILDKIT_SYNTAX" = "dalec_frontend"
243-
}
244-
contexts = {
245-
"dalec_frontend" = "target:frontend"
246-
}
247-
target = "${distro}/container/depsonly"
248-
tags = ["local/dalec/deps-only:${distro}"]
249-
}
250-
251-
target "test-deps-only" {
252-
dockerfile-inline = <<EOT
253-
FROM deps-only-context
254-
# Make sure the deps-only target has the runtime dependencies we expect and not, for instance, "rpm"
255-
RUN command -v bash
256-
RUN command -v patch
257-
RUN if command -v rpm; then echo should be a distroless image but rpm binary is installed; exit 1; fi
258-
EOT
259-
260-
contexts = {
261-
"deps-only-context" = "target:deps-only-mariner2"
262-
}
263-
}
264-
265-
266230
variable "CI_FRONTEND_CACHE_SCOPE" {
267231
default = "dalec/frontend/ci"
268232
}

targets/linux/rpm/distro/container.go

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,67 @@ func (cfg *Config) BuildContainer(ctx context.Context, client gwclient.Client, s
8080
return rootfs
8181
}
8282

83+
// BuildContainerWithPackages builds a container image by installing the given
84+
// package names (resolved from repos) instead of local RPM files.
85+
// This is used by the deps-only target where there are no locally-built RPMs.
86+
func (cfg *Config) BuildContainerWithPackages(ctx context.Context, client gwclient.Client, sOpt dalec.SourceOpts, spec *dalec.Spec, targetKey string, packages []string, opts ...llb.ConstraintsOpt) llb.State {
87+
opts = append(opts, dalec.ProgressGroup("Install packages"))
88+
opts = append(opts, frontend.IgnoreCache(client))
89+
90+
const workPath = "/tmp/rootfs"
91+
92+
bi, err := spec.GetSingleBase(targetKey)
93+
if err != nil {
94+
return dalec.ErrorState(llb.Scratch(), err)
95+
}
96+
97+
skipBase := bi != nil
98+
rootfs := bi.ToState(sOpt, opts...)
99+
100+
installTimeRepos := spec.GetInstallRepos(targetKey)
101+
repoMounts, keyPaths := cfg.RepoMounts(installTimeRepos, sOpt, opts...)
102+
importRepos := []DnfInstallOpt{
103+
DnfWithMounts(repoMounts),
104+
DnfImportKeys(keyPaths),
105+
DnfInstallWithConstraints(opts),
106+
}
107+
108+
installOpts := []DnfInstallOpt{DnfAtRoot(workPath)}
109+
installOpts = append(installOpts, importRepos...)
110+
installOpts = append(installOpts, IncludeDocs(spec.GetArtifacts(targetKey).HasDocs()))
111+
112+
pkgs := append([]string{}, packages...)
113+
114+
if !skipBase && len(cfg.BasePackages) > 0 {
115+
baseMountPath := "/tmp/rpms-base"
116+
opts := append(opts, dalec.ProgressGroup("Create base virtual package"))
117+
118+
var basePkgStates []llb.State
119+
for _, spec := range cfg.BasePackages {
120+
pkg := cfg.BuildPkg(ctx, client, sOpt, &spec, targetKey, opts...)
121+
basePkgStates = append(basePkgStates, pkg)
122+
}
123+
124+
basePkgs := dalec.MergeAtPath(llb.Scratch().File(llb.Mkdir("/RPMS", 0o755), opts...), basePkgStates, "/", opts...)
125+
installOpts = append(installOpts, DnfWithMounts(llb.AddMount(baseMountPath, basePkgs, llb.SourcePath("/RPMS"))))
126+
pkgs = append(pkgs, filepath.Join(baseMountPath, "**/*.rpm"))
127+
}
128+
129+
worker := cfg.Worker(sOpt, dalec.Platform(sOpt.TargetPlatform), dalec.WithConstraints(opts...))
130+
131+
rootfs = worker.Run(
132+
dalec.WithConstraints(opts...),
133+
cfg.Install(pkgs, installOpts...),
134+
frontend.IgnoreCache(client, targets.IgnoreCacheKeyContainer),
135+
).AddMount(workPath, rootfs)
136+
137+
if post := spec.GetImagePost(targetKey); post != nil && len(post.Symlinks) > 0 {
138+
rootfs = rootfs.With(dalec.InstallPostSymlinks(post, worker, opts...))
139+
}
140+
141+
return rootfs
142+
}
143+
83144
func (cfg *Config) HandleDepsOnly(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) {
84145
return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) {
85146
rtDeps := spec.GetPackageDeps(targetKey).GetRuntime()
@@ -95,16 +156,9 @@ func (cfg *Config) HandleDepsOnly(ctx context.Context, client gwclient.Client) (
95156
}
96157

97158
pc := dalec.Platform(platform)
98-
worker := cfg.Worker(sOpt, pg, pc)
99-
100159
deps := dalec.SortMapKeys(rtDeps)
101160

102-
withDownloads := worker.Run(dalec.ShArgs("set -ex; mkdir -p /tmp/rpms/RPMS/$(uname -m)")).
103-
Run(cfg.Install(deps,
104-
DnfDownloadAllDeps("/tmp/rpms/RPMS/$(uname -m)")), pg).Root()
105-
rpmDir := llb.Scratch().File(llb.Copy(withDownloads, "/tmp/rpms", "/", dalec.WithDirContentsOnly()), pg)
106-
107-
ctr := cfg.BuildContainer(ctx, client, sOpt, spec, targetKey, rpmDir, pg, pc)
161+
ctr := cfg.BuildContainerWithPackages(ctx, client, sOpt, spec, targetKey, deps, pg, pc)
108162

109163
def, err := ctr.Marshal(ctx, pc)
110164
if err != nil {

targets/linux/rpm/distro/dnf_install.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,6 @@ type dnfInstallConfig struct {
3434

3535
constraints []llb.ConstraintsOpt
3636

37-
downloadOnly bool
38-
39-
allDeps bool
40-
41-
downloadDir string
42-
4337
// When true, don't omit docs from the installed RPMs.
4438
includeDocs bool
4539

@@ -82,14 +76,6 @@ func DnfForceArch(arch string) DnfInstallOpt {
8276
}
8377
}
8478

85-
func DnfDownloadAllDeps(dest string) DnfInstallOpt {
86-
return func(cfg *dnfInstallConfig) {
87-
cfg.downloadOnly = true
88-
cfg.allDeps = true
89-
cfg.downloadDir = dest
90-
}
91-
}
92-
9379
func IncludeDocs(v bool) DnfInstallOpt {
9480
return func(cfg *dnfInstallConfig) {
9581
cfg.includeDocs = v
@@ -110,18 +96,6 @@ func dnfInstallFlags(cfg *dnfInstallConfig) string {
11096
cmdOpts += " --setopt=reposdir=/etc/yum.repos.d"
11197
}
11298

113-
if cfg.downloadOnly {
114-
cmdOpts += " --downloadonly"
115-
}
116-
117-
if cfg.allDeps {
118-
cmdOpts += " --alldeps"
119-
}
120-
121-
if cfg.downloadDir != "" {
122-
cmdOpts += " --downloaddir " + cfg.downloadDir
123-
}
124-
12599
if !cfg.includeDocs {
126100
cmdOpts += " --setopt=tsflags=nodocs"
127101
}

test/linux_target_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ type targetConfig struct {
6161
Package string
6262
// Container is the target for creating a container
6363
Container string
64+
// DepsOnly is the target for creating a deps-only container (no package built, only runtime deps installed).
65+
DepsOnly string
6466
// Worker is the target for creating the worker image.
6567
Worker string
6668
// Sysext is the target for creating a systemd system extension.
@@ -177,6 +179,16 @@ func testLinuxDistro(ctx context.Context, t *testing.T, testConfig testLinuxConf
177179
})
178180
})
179181

182+
t.Run("container/depsonly", func(t *testing.T) {
183+
if testConfig.Target.DepsOnly == "" {
184+
t.Skip("depsonly target not defined")
185+
}
186+
187+
t.Parallel()
188+
ctx := startTestSpan(ctx, t)
189+
testDepsOnly(ctx, t, testConfig)
190+
})
191+
180192
t.Run("target-prebuilt-packages", func(t *testing.T) {
181193
t.Parallel()
182194
ctx := startTestSpan(ctx, t)
@@ -5403,6 +5415,85 @@ echo "This is a third test binary"
54035415
})
54045416
}
54055417

5418+
func testDepsOnly(ctx context.Context, t *testing.T, testConfig testLinuxConfig) {
5419+
t.Run("minimal spec", func(t *testing.T) {
5420+
t.Parallel()
5421+
ctx := startTestSpan(ctx, t)
5422+
5423+
spec := &dalec.Spec{
5424+
Dependencies: &dalec.PackageDependencies{
5425+
Runtime: map[string]dalec.PackageConstraints{
5426+
"curl": {},
5427+
},
5428+
},
5429+
}
5430+
5431+
testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) {
5432+
req := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(testConfig.Target.DepsOnly))
5433+
res := solveT(ctx, t, client, req)
5434+
5435+
ref, err := res.SingleRef()
5436+
assert.NilError(t, err)
5437+
5438+
_, err = ref.StatFile(ctx, gwclient.StatRequest{Path: "/usr/bin/curl"})
5439+
assert.NilError(t, err)
5440+
})
5441+
})
5442+
5443+
t.Run("full spec", func(t *testing.T) {
5444+
t.Parallel()
5445+
ctx := startTestSpan(ctx, t)
5446+
5447+
// Full spec includes sources, build steps, and a shell script artifact.
5448+
// The deps-only target should install only runtime deps (curl) and NOT
5449+
// include the built artifact (/usr/bin/my-script) or its implicit dep.
5450+
spec := fillMetadata("test-deps-only-full", &dalec.Spec{
5451+
Sources: map[string]dalec.Source{
5452+
"my-script": {
5453+
Inline: &dalec.SourceInline{
5454+
File: &dalec.SourceInlineFile{
5455+
Contents: "#!/usr/bin/env bash\necho hello from deps-only test\n",
5456+
Permissions: 0o700,
5457+
},
5458+
},
5459+
},
5460+
},
5461+
Build: dalec.ArtifactBuild{
5462+
Steps: []dalec.BuildStep{
5463+
{Command: "/bin/true"},
5464+
},
5465+
},
5466+
Artifacts: dalec.Artifacts{
5467+
Binaries: map[string]dalec.ArtifactConfig{
5468+
"my-script": {},
5469+
},
5470+
},
5471+
Dependencies: &dalec.PackageDependencies{
5472+
Runtime: map[string]dalec.PackageConstraints{
5473+
"curl": {},
5474+
},
5475+
},
5476+
})
5477+
5478+
testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) {
5479+
req := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(testConfig.Target.DepsOnly))
5480+
res := solveT(ctx, t, client, req)
5481+
5482+
ref, err := res.SingleRef()
5483+
assert.NilError(t, err)
5484+
5485+
// Runtime dep should be installed.
5486+
_, err = ref.StatFile(ctx, gwclient.StatRequest{Path: "/usr/bin/curl"})
5487+
assert.NilError(t, err)
5488+
5489+
// The shell script artifact should NOT be present — deps-only
5490+
// never builds the package, so no artifacts are installed.
5491+
_, err = ref.StatFile(ctx, gwclient.StatRequest{Path: "/usr/bin/my-script"})
5492+
assert.ErrorContains(t, err, "no such file")
5493+
})
5494+
})
5495+
}
5496+
54065497
func testLinuxSpec(t *testing.T, userSpec dalec.Spec) dalec.Spec {
54075498
t.Helper()
54085499

test/target_almalinux_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func TestAlmalinux9(t *testing.T) {
1717
Key: "almalinux9",
1818
Package: "almalinux9/rpm",
1919
Container: "almalinux9/container",
20+
DepsOnly: "almalinux9/container/depsonly",
2021
Worker: "almalinux9/worker",
2122
FormatDepEqual: func(v, _ string) string {
2223
return v
@@ -65,6 +66,7 @@ func TestAlmalinux8(t *testing.T) {
6566
Target: targetConfig{
6667
Package: "almalinux8/rpm",
6768
Container: "almalinux8/container",
69+
DepsOnly: "almalinux8/container/depsonly",
6870
Worker: "almalinux8/worker",
6971
FormatDepEqual: func(v, _ string) string {
7072
return v

test/target_azlinux_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func TestMariner2(t *testing.T) {
4646
Key: azlinux.Mariner2TargetKey,
4747
Package: "mariner2/rpm",
4848
Container: "mariner2/container",
49+
DepsOnly: "mariner2/container/depsonly",
4950
Worker: "mariner2/worker",
5051
FormatDepEqual: func(v, _ string) string {
5152
return v
@@ -91,6 +92,7 @@ func TestAzlinux3(t *testing.T) {
9192
Key: "azlinux3",
9293
Package: "azlinux3/rpm",
9394
Container: "azlinux3/container",
95+
DepsOnly: "azlinux3/container/depsonly",
9496
Worker: "azlinux3/worker",
9597
Sysext: "azlinux3/testing/sysext",
9698
ListExpectedSignFiles: azlinuxListSignFiles("azl3"),

test/target_rockylinux_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func TestRockylinux9(t *testing.T) {
1717
Key: "rockylinux9",
1818
Package: "rockylinux9/rpm",
1919
Container: "rockylinux9/container",
20+
DepsOnly: "rockylinux9/container/depsonly",
2021
Worker: "rockylinux9/worker",
2122
FormatDepEqual: func(v, _ string) string {
2223
return v
@@ -65,6 +66,7 @@ func TestRockylinux8(t *testing.T) {
6566
Target: targetConfig{
6667
Package: "rockylinux8/rpm",
6768
Container: "rockylinux8/container",
69+
DepsOnly: "rockylinux8/container/depsonly",
6870
Worker: "rockylinux8/worker",
6971
FormatDepEqual: func(v, _ string) string {
7072
return v

0 commit comments

Comments
 (0)