Skip to content

Commit 5b5cd00

Browse files
committed
targets/linux/deb/distro: bootstrap container when necessary
Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com>
1 parent 5175083 commit 5b5cd00

6 files changed

Lines changed: 339 additions & 104 deletions

File tree

targets/linux/deb/debian/common.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var (
2727
// base image.
2828
basePackages = []string{
2929
"ca-certificates",
30+
"passwd",
3031
}
3132

3233
targets = map[string]gwclient.BuildFunc{

targets/linux/deb/distro/container.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,131 @@ import (
1010
"github.com/project-dalec/dalec/targets"
1111
)
1212

13+
type BuildDistrolessContainerInput struct {
14+
Config *Config
15+
Client gwclient.Client // Replace with interface.
16+
Worker llb.State
17+
SOpt dalec.SourceOpts
18+
Spec *dalec.Spec
19+
Target string
20+
DebSt llb.State // Why is this DebSt?
21+
Opts []llb.ConstraintsOpt
22+
}
23+
24+
func BuildDistrolessContainer(ctx context.Context, input BuildDistrolessContainerInput) llb.State {
25+
opts := append(input.Opts, frontend.IgnoreCache(input.Client), dalec.ProgressGroup("Build Container Image"))
26+
27+
// Those base repos come from distro configuration.
28+
repos := dalec.GetExtraRepos(input.Config.ExtraRepos, "install")
29+
30+
// These are user specified via spec.
31+
repos = append(repos, input.Spec.GetInstallRepos(input.Target)...)
32+
33+
withRepos := input.Config.RepoMounts(repos, input.SOpt, opts...)
34+
35+
debug := llb.Scratch().File(llb.Mkfile("debug", 0o644, []byte(`debug=2`)), opts...)
36+
// This file makes dpkg give more verbose output which can be useful when things go awry.
37+
debugOpt := llb.AddMount("/etc/dpkg/dpkg.cfg.d/99-dalec-debug", debug, llb.SourcePath("debug"), llb.Readonly)
38+
39+
// Distroless images are built from scratch.
40+
baseImg := llb.Scratch().
41+
File(llb.Mkfile("/apt.conf", 0o644, []byte(`Apt::Architecture "amd64";
42+
Apt::Architectures "amd64";
43+
Dir "/tmp/rootfs";
44+
Dir::Etc::TrustedParts "/etc/apt/trusted.gpg.d/";
45+
`)), opts...).
46+
File(llb.Mkdir("/etc", 0o755), opts...).
47+
File(llb.Mkdir("/etc/apt", 0o755), opts...).
48+
File(llb.Mkfile("/etc/apt/sources.list", 0o644, []byte(`deb http://deb.debian.org/debian/ trixie main
49+
`)), opts...).
50+
File(llb.Mkdir("/etc/apt/apt.conf.d", 0o755), opts...).
51+
File(llb.Mkdir("/etc/apt/preferences.d", 0o755), opts...).
52+
File(llb.Mkdir("/var", 0o755), opts...).
53+
File(llb.Mkdir("/var/cache", 0o755), opts...).
54+
File(llb.Mkdir("/var/lib", 0o755), opts...).
55+
File(llb.Mkdir("/var/lib/dpkg", 0o755), opts...)
56+
57+
baseImg = input.Worker.Run(
58+
dalec.WithConstraints(opts...),
59+
withRepos,
60+
debugOpt,
61+
llb.AddEnv("DEBIAN_FRONTEND", "noninteractive"),
62+
llb.AddEnv("APT_CONFIG", "/tmp/rootfs/apt.conf"),
63+
dalec.ShArgs("set -x; apt-get update && apt-get --yes --download-only install '?essential' && for f in /tmp/rootfs/var/cache/apt/archives/*.deb; do dpkg-deb --extract \"$f\" /tmp/rootfs; done"),
64+
frontend.IgnoreCache(input.Client, targets.IgnoreCacheKeyContainer),
65+
).AddMount("/tmp/rootfs", baseImg).Run(
66+
dalec.WithConstraints(opts...),
67+
debugOpt,
68+
llb.AddEnv("DEBIAN_FRONTEND", "noninteractive"),
69+
dalec.ShArgs("dpkg --install --force-depends /var/cache/apt/archives/*.deb && rm -rf /var/cache/apt/archives/*.deb"),
70+
frontend.IgnoreCache(input.Client, targets.IgnoreCacheKeyContainer),
71+
).Root()
72+
73+
bi, err := input.Spec.GetSingleBase(input.Target)
74+
if err != nil {
75+
return dalec.ErrorState(llb.Scratch(), err)
76+
}
77+
if bi != nil {
78+
baseImg = bi.ToState(input.SOpt, opts...)
79+
}
80+
81+
opts = append(opts, dalec.ProgressGroup("Install spec package"))
82+
83+
// If we have base packages to install, create a meta-package to install them.
84+
if len(input.Config.BasePackages) > 0 {
85+
runtimePkgs := make(dalec.PackageDependencyList, len(input.Config.BasePackages))
86+
for _, pkgName := range input.Config.BasePackages {
87+
runtimePkgs[pkgName] = dalec.PackageConstraints{}
88+
}
89+
basePkgSpec := &dalec.Spec{
90+
Name: "dalec-deb-base-packages",
91+
Packager: "dalec",
92+
Description: "Base Packages for Debian-based Distros",
93+
Version: "0.1",
94+
Revision: "1",
95+
Dependencies: &dalec.PackageDependencies{
96+
Runtime: runtimePkgs,
97+
},
98+
}
99+
100+
basePkg := input.Config.BuildPkg(ctx, input.Client, input.SOpt, basePkgSpec, input.Target, opts...)
101+
102+
// Update the base image to include the base packages.
103+
// This may include things that are necessary to even install the debSt package.
104+
// So this must be done separately from the debSt package.
105+
opts := append(opts, dalec.ProgressGroup("Install base image packages"))
106+
baseImg = baseImg.Run(
107+
dalec.WithConstraints(opts...),
108+
debugOpt,
109+
InstallLocalPkg(basePkg, true, opts...),
110+
dalec.WithMountedAptCache(input.Config.AptCachePrefix, opts...),
111+
).Root()
112+
}
113+
114+
return baseImg.Run(
115+
dalec.WithConstraints(opts...),
116+
withRepos,
117+
debugOpt,
118+
InstallLocalPkg(input.DebSt, true, opts...),
119+
frontend.IgnoreCache(input.Client, targets.IgnoreCacheKeyContainer),
120+
).Root().
121+
With(dalec.InstallPostSymlinks(input.Spec.GetImagePost(input.Target), input.Worker, opts...))
122+
}
123+
13124
func (c *Config) BuildContainer(ctx context.Context, client gwclient.Client, sOpt dalec.SourceOpts, spec *dalec.Spec, targetKey string, debSt llb.State, opts ...llb.ConstraintsOpt) llb.State {
125+
if true {
126+
return BuildDistrolessContainer(ctx, BuildDistrolessContainerInput{
127+
Config: c,
128+
Client: client,
129+
Worker: c.Worker(sOpt, dalec.Platform(sOpt.TargetPlatform), dalec.WithConstraints(opts...)),
130+
SOpt: sOpt,
131+
Spec: spec,
132+
Target: targetKey,
133+
DebSt: debSt,
134+
Opts: opts,
135+
})
136+
}
137+
14138
opts = append(opts, frontend.IgnoreCache(client), dalec.ProgressGroup("Build Container Image"))
15139

16140
baseImg := llb.Image(c.DefaultOutputImage, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...))

targets/linux/deb/distro/container_test.go

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func Test_Building_container(t *testing.T) {
2424
t.Parallel()
2525

2626
c := &Config{
27+
ImageRef: "foo",
2728
DefaultOutputImage: "foo",
2829
}
2930

@@ -49,24 +50,37 @@ func Test_Building_container(t *testing.T) {
4950

5051
ctx := t.Context()
5152

52-
state := c.BuildContainer(ctx, client, dalec.SourceOpts{}, spec, "target", llb.State{})
53+
sopt := dalec.SourceOpts{
54+
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
55+
return nil, nil
56+
},
57+
}
58+
59+
state := c.BuildContainer(ctx, client, sopt, spec, "target", llb.State{})
5360

5461
ops, err := test.LLBOpsFromState(ctx, state)
5562
if err != nil {
5663
t.Fatalf("failed to get llb ops from state: %v", err)
5764
}
5865

59-
if len(ops) == 0 {
60-
t.Fatalf("expected at least one llb op, got none")
61-
}
66+
specPackageImageSourceFound := false
6267

63-
s := ops[0].Op.GetSource()
64-
if s == nil {
65-
t.Fatalf("expected source op, got nil")
68+
for _, op := range ops {
69+
s := op.Op.GetSource()
70+
71+
if s == nil || op.OpMetadata.ProgressGroup.Name != "Build Container Image" {
72+
continue
73+
}
74+
75+
specPackageImageSourceFound = true
76+
77+
if !strings.Contains(s.Identifier, expectedRef) {
78+
t.Fatalf("expected source identifier to contain %q, got %q", expectedRef, s.Identifier)
79+
}
6680
}
6781

68-
if !strings.Contains(s.Identifier, expectedRef) {
69-
t.Fatalf("expected source identifier to contain %q, got %q", expectedRef, s.Identifier)
82+
if !specPackageImageSourceFound {
83+
t.Fatalf("Expected to find spec package source in llb ops")
7084
}
7185
})
7286

@@ -78,6 +92,7 @@ func Test_Building_container(t *testing.T) {
7892
expectedRef := "foo"
7993

8094
c := &Config{
95+
ImageRef: "foo",
8196
DefaultOutputImage: expectedRef,
8297
}
8398

@@ -89,7 +104,13 @@ func Test_Building_container(t *testing.T) {
89104

90105
ctx := t.Context()
91106

92-
state := c.BuildContainer(ctx, client, dalec.SourceOpts{}, spec, "target", llb.State{})
107+
sopt := dalec.SourceOpts{
108+
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
109+
return nil, nil
110+
},
111+
}
112+
113+
state := c.BuildContainer(ctx, client, sopt, spec, "target", llb.State{})
93114

94115
ops, err := test.LLBOpsFromState(ctx, state)
95116
if err != nil {
@@ -119,6 +140,7 @@ func Test_Building_container(t *testing.T) {
119140
extraInstallRepo := "extra-install-repo"
120141

121142
c := &Config{
143+
ImageRef: "foo",
122144
DefaultOutputImage: "foo",
123145
ExtraRepos: []dalec.PackageRepositoryConfig{
124146
{
@@ -150,7 +172,12 @@ func Test_Building_container(t *testing.T) {
150172

151173
ctx := t.Context()
152174

153-
ops, err := test.LLBOpsFromState(ctx, c.BuildContainer(ctx, &testClient{}, dalec.SourceOpts{}, &dalec.Spec{}, "target", llb.State{}))
175+
sopt := dalec.SourceOpts{
176+
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
177+
return nil, nil
178+
},
179+
}
180+
ops, err := test.LLBOpsFromState(ctx, c.BuildContainer(ctx, &testClient{}, sopt, &dalec.Spec{}, "target", llb.State{}))
154181
if err != nil {
155182
t.Fatalf("failed to get llb ops from state: %v", err)
156183
}
@@ -190,6 +217,7 @@ func Test_Building_container(t *testing.T) {
190217
aptCachePrefix := "apt-cache-prefix"
191218

192219
c := &Config{
220+
ImageRef: "foo",
193221
DefaultOutputImage: "foo",
194222
VersionID: "bar",
195223
ContextRef: "distro-context-ref",
@@ -200,9 +228,7 @@ func Test_Building_container(t *testing.T) {
200228

201229
sopt := dalec.SourceOpts{
202230
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
203-
s := llb.Scratch()
204-
205-
return &s, nil
231+
return nil, nil
206232
},
207233
}
208234

@@ -255,6 +281,7 @@ func Test_Building_container(t *testing.T) {
255281
t.Parallel()
256282

257283
c := &Config{
284+
ImageRef: "foo",
258285
DefaultOutputImage: "foo",
259286
BasePackages: []string{"base-package-1"},
260287
VersionID: "bar",
@@ -265,9 +292,7 @@ func Test_Building_container(t *testing.T) {
265292

266293
sopt := dalec.SourceOpts{
267294
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
268-
s := llb.Scratch()
269-
270-
return &s, nil
295+
return nil, nil
271296
},
272297
}
273298

@@ -278,12 +303,16 @@ func Test_Building_container(t *testing.T) {
278303
t.Fatalf("failed to get llb ops from state: %v", err)
279304
}
280305

306+
execOpFound := false
307+
281308
for _, op := range ops {
282309
e := op.Op.GetExec()
283310
if e == nil || op.OpMetadata.ProgressGroup.Name != "Install base image packages" {
284311
continue
285312
}
286313

314+
execOpFound = true
315+
287316
for _, v := range e.Meta.Env {
288317
s := strings.Split(v, "=")
289318
if len(s) != 2 {
@@ -304,13 +333,18 @@ func Test_Building_container(t *testing.T) {
304333
}
305334
}
306335

336+
if !execOpFound {
337+
t.Fatalf("Exec op for installing base packages not found")
338+
}
339+
307340
t.Fatalf("Expected DALEC_UPGRADE to be set when installing base packages")
308341
})
309342

310343
t.Run("before_installing_spec_package", func(t *testing.T) {
311344
t.Parallel()
312345

313346
c := &Config{
347+
ImageRef: "foo",
314348
DefaultOutputImage: "foo",
315349
BasePackages: []string{"base-package-1"},
316350
VersionID: "bar",
@@ -321,9 +355,7 @@ func Test_Building_container(t *testing.T) {
321355

322356
sopt := dalec.SourceOpts{
323357
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
324-
s := llb.Scratch()
325-
326-
return &s, nil
358+
return nil, nil
327359
},
328360
}
329361

@@ -385,6 +417,7 @@ func Test_Building_container(t *testing.T) {
385417
aptCachePrefix := "apt-cache-prefix"
386418

387419
c := &Config{
420+
ImageRef: "foo",
388421
DefaultOutputImage: "foo",
389422
BasePackages: []string{"base-package-1"},
390423
VersionID: "bar",
@@ -396,9 +429,7 @@ func Test_Building_container(t *testing.T) {
396429

397430
sopt := dalec.SourceOpts{
398431
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
399-
s := llb.Scratch()
400-
401-
return &s, nil
432+
return nil, nil
402433
},
403434
}
404435

@@ -411,12 +442,16 @@ func Test_Building_container(t *testing.T) {
411442

412443
aptCacheFound := false
413444

445+
execOpFound := false
446+
414447
for _, op := range ops {
415448
e := op.Op.GetExec()
416449
if e == nil || op.OpMetadata.ProgressGroup.Name != "Install base image packages" {
417450
continue
418451
}
419452

453+
execOpFound = true
454+
420455
for _, mount := range e.Mounts {
421456
if mount.Dest == "/var/cache/apt" {
422457
aptCacheFound = true
@@ -438,6 +473,10 @@ func Test_Building_container(t *testing.T) {
438473
}
439474
}
440475

476+
if !execOpFound {
477+
t.Fatalf("Exec op for installing base packages not found")
478+
}
479+
441480
if !aptCacheFound {
442481
t.Fatalf("Apt cache mount not found before installing base packages")
443482
}

0 commit comments

Comments
 (0)