Skip to content

Commit 44e9c93

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

5 files changed

Lines changed: 268 additions & 32 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+
opts = append(opts, dalec.ProgressGroup("Install spec package"))
36+
37+
debug := llb.Scratch().File(llb.Mkfile("debug", 0o644, []byte(`debug=2`)), opts...)
38+
// This file makes dpkg give more verbose output which can be useful when things go awry.
39+
debugOpt := llb.AddMount("/etc/dpkg/dpkg.cfg.d/99-dalec-debug", debug, llb.SourcePath("debug"), llb.Readonly)
40+
41+
// Distroless images are built from scratch.
42+
baseImg := llb.Scratch().
43+
File(llb.Mkfile("/apt.conf", 0o644, []byte(`Apt::Architecture "amd64";
44+
Apt::Architectures "amd64";
45+
Dir "/tmp/rootfs";
46+
Dir::Etc::TrustedParts "/etc/apt/trusted.gpg.d/";
47+
`)), opts...).
48+
File(llb.Mkdir("/etc", 0o755), opts...).
49+
File(llb.Mkdir("/etc/apt", 0o755), opts...).
50+
File(llb.Mkfile("/etc/apt/sources.list", 0o644, []byte(`deb http://deb.debian.org/debian/ trixie main
51+
`)), opts...).
52+
File(llb.Mkdir("/etc/apt/apt.conf.d", 0o755), opts...).
53+
File(llb.Mkdir("/etc/apt/preferences.d", 0o755), opts...).
54+
File(llb.Mkdir("/var", 0o755), opts...).
55+
File(llb.Mkdir("/var/cache", 0o755), opts...).
56+
File(llb.Mkdir("/var/lib", 0o755), opts...).
57+
File(llb.Mkdir("/var/lib/dpkg", 0o755), opts...)
58+
59+
baseImg = input.Worker.Run(
60+
dalec.WithConstraints(opts...),
61+
withRepos,
62+
debugOpt,
63+
llb.AddEnv("DEBIAN_FRONTEND", "noninteractive"),
64+
llb.AddEnv("APT_CONFIG", "/tmp/rootfs/apt.conf"),
65+
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"),
66+
frontend.IgnoreCache(input.Client, targets.IgnoreCacheKeyContainer),
67+
).AddMount("/tmp/rootfs", baseImg).Run(
68+
dalec.WithConstraints(opts...),
69+
debugOpt,
70+
llb.AddEnv("DEBIAN_FRONTEND", "noninteractive"),
71+
dalec.ShArgs("dpkg --install --force-depends /var/cache/apt/archives/*.deb && rm -rf /var/cache/apt/archives/*.deb"),
72+
frontend.IgnoreCache(input.Client, targets.IgnoreCacheKeyContainer),
73+
).Root()
74+
75+
bi, err := input.Spec.GetSingleBase(input.Target)
76+
if err != nil {
77+
return dalec.ErrorState(llb.Scratch(), err)
78+
}
79+
if bi != nil {
80+
baseImg = bi.ToState(input.SOpt, opts...)
81+
}
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: 69 additions & 22 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,45 @@ 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")
66+
specPackageImageSourceFound := false
67+
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+
}
6180
}
6281

63-
s := ops[0].Op.GetSource()
64-
if s == nil {
65-
t.Fatalf("expected source op, got nil")
82+
raw, err := test.LLBOpsToJSON(ops)
83+
if err != nil {
84+
t.Fatalf("failed to marshal llb op to json: %v", err)
6685
}
6786

68-
if !strings.Contains(s.Identifier, expectedRef) {
69-
t.Fatalf("expected source identifier to contain %q, got %q", expectedRef, s.Identifier)
87+
_ = raw
88+
// t.Errorf("\n%s", raw)
89+
90+
if !specPackageImageSourceFound {
91+
t.Fatalf("Expected to find spec package source in llb ops")
7092
}
7193
})
7294

@@ -78,6 +100,7 @@ func Test_Building_container(t *testing.T) {
78100
expectedRef := "foo"
79101

80102
c := &Config{
103+
ImageRef: "foo",
81104
DefaultOutputImage: expectedRef,
82105
}
83106

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

90113
ctx := t.Context()
91114

92-
state := c.BuildContainer(ctx, client, dalec.SourceOpts{}, spec, "target", llb.State{})
115+
sopt := dalec.SourceOpts{
116+
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
117+
return nil, nil
118+
},
119+
}
120+
121+
state := c.BuildContainer(ctx, client, sopt, spec, "target", llb.State{})
93122

94123
ops, err := test.LLBOpsFromState(ctx, state)
95124
if err != nil {
@@ -119,6 +148,7 @@ func Test_Building_container(t *testing.T) {
119148
extraInstallRepo := "extra-install-repo"
120149

121150
c := &Config{
151+
ImageRef: "foo",
122152
DefaultOutputImage: "foo",
123153
ExtraRepos: []dalec.PackageRepositoryConfig{
124154
{
@@ -150,7 +180,12 @@ func Test_Building_container(t *testing.T) {
150180

151181
ctx := t.Context()
152182

153-
ops, err := test.LLBOpsFromState(ctx, c.BuildContainer(ctx, &testClient{}, dalec.SourceOpts{}, &dalec.Spec{}, "target", llb.State{}))
183+
sopt := dalec.SourceOpts{
184+
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
185+
return nil, nil
186+
},
187+
}
188+
ops, err := test.LLBOpsFromState(ctx, c.BuildContainer(ctx, &testClient{}, sopt, &dalec.Spec{}, "target", llb.State{}))
154189
if err != nil {
155190
t.Fatalf("failed to get llb ops from state: %v", err)
156191
}
@@ -190,6 +225,7 @@ func Test_Building_container(t *testing.T) {
190225
aptCachePrefix := "apt-cache-prefix"
191226

192227
c := &Config{
228+
ImageRef: "foo",
193229
DefaultOutputImage: "foo",
194230
VersionID: "bar",
195231
ContextRef: "distro-context-ref",
@@ -200,9 +236,7 @@ func Test_Building_container(t *testing.T) {
200236

201237
sopt := dalec.SourceOpts{
202238
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
203-
s := llb.Scratch()
204-
205-
return &s, nil
239+
return nil, nil
206240
},
207241
}
208242

@@ -255,6 +289,7 @@ func Test_Building_container(t *testing.T) {
255289
t.Parallel()
256290

257291
c := &Config{
292+
ImageRef: "foo",
258293
DefaultOutputImage: "foo",
259294
BasePackages: []string{"base-package-1"},
260295
VersionID: "bar",
@@ -265,9 +300,7 @@ func Test_Building_container(t *testing.T) {
265300

266301
sopt := dalec.SourceOpts{
267302
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
268-
s := llb.Scratch()
269-
270-
return &s, nil
303+
return nil, nil
271304
},
272305
}
273306

@@ -278,12 +311,16 @@ func Test_Building_container(t *testing.T) {
278311
t.Fatalf("failed to get llb ops from state: %v", err)
279312
}
280313

314+
execOpFound := false
315+
281316
for _, op := range ops {
282317
e := op.Op.GetExec()
283318
if e == nil || op.OpMetadata.ProgressGroup.Name != "Install base image packages" {
284319
continue
285320
}
286321

322+
execOpFound = true
323+
287324
for _, v := range e.Meta.Env {
288325
s := strings.Split(v, "=")
289326
if len(s) != 2 {
@@ -304,13 +341,18 @@ func Test_Building_container(t *testing.T) {
304341
}
305342
}
306343

344+
if !execOpFound {
345+
t.Fatalf("Exec op for installing base packages not found")
346+
}
347+
307348
t.Fatalf("Expected DALEC_UPGRADE to be set when installing base packages")
308349
})
309350

310351
t.Run("before_installing_spec_package", func(t *testing.T) {
311352
t.Parallel()
312353

313354
c := &Config{
355+
ImageRef: "foo",
314356
DefaultOutputImage: "foo",
315357
BasePackages: []string{"base-package-1"},
316358
VersionID: "bar",
@@ -321,9 +363,7 @@ func Test_Building_container(t *testing.T) {
321363

322364
sopt := dalec.SourceOpts{
323365
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
324-
s := llb.Scratch()
325-
326-
return &s, nil
366+
return nil, nil
327367
},
328368
}
329369

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

387427
c := &Config{
428+
ImageRef: "foo",
388429
DefaultOutputImage: "foo",
389430
BasePackages: []string{"base-package-1"},
390431
VersionID: "bar",
@@ -396,9 +437,7 @@ func Test_Building_container(t *testing.T) {
396437

397438
sopt := dalec.SourceOpts{
398439
GetContext: func(string, ...llb.LocalOption) (*llb.State, error) {
399-
s := llb.Scratch()
400-
401-
return &s, nil
440+
return nil, nil
402441
},
403442
}
404443

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

412451
aptCacheFound := false
413452

453+
execOpFound := false
454+
414455
for _, op := range ops {
415456
e := op.Op.GetExec()
416457
if e == nil || op.OpMetadata.ProgressGroup.Name != "Install base image packages" {
417458
continue
418459
}
419460

461+
execOpFound = true
462+
420463
for _, mount := range e.Mounts {
421464
if mount.Dest == "/var/cache/apt" {
422465
aptCacheFound = true
@@ -438,6 +481,10 @@ func Test_Building_container(t *testing.T) {
438481
}
439482
}
440483

484+
if !execOpFound {
485+
t.Fatalf("Exec op for installing base packages not found")
486+
}
487+
441488
if !aptCacheFound {
442489
t.Fatalf("Apt cache mount not found before installing base packages")
443490
}

0 commit comments

Comments
 (0)