diff --git a/frontend/azlinux/azlinux3.go b/frontend/azlinux/azlinux3.go index 819a2087b..1765c5bee 100644 --- a/frontend/azlinux/azlinux3.go +++ b/frontend/azlinux/azlinux3.go @@ -16,8 +16,11 @@ const ( AzLinux3TargetKey = "azlinux3" tdnfCacheNameAzlinux3 = "azlinux3-tdnf-cache" - azlinux3Ref = "azurelinuxpreview.azurecr.io/public/azurelinux/base/core:3.0" - azlinux3DistrolessRef = "azurelinuxpreview.azurecr.io/public/azurelinux/distroless/base:3.0" + // Azlinux3Ref is the image ref used for the base worker image + Azlinux3Ref = "azurelinuxpreview.azurecr.io/public/azurelinux/base/core:3.0" + // Azlinux3WorkerContextName is the build context name that can be used to lookup + Azlinux3WorkerContextName = "dalec-azlinux3-worker" + azlinux3DistrolessRef = "azurelinuxpreview.azurecr.io/public/azurelinux/distroless/base:3.0" ) func NewAzlinux3Handler() gwclient.BuildFunc { @@ -26,11 +29,29 @@ func NewAzlinux3Handler() gwclient.BuildFunc { type azlinux3 struct{} -func (w azlinux3) Base(resolver llb.ImageMetaResolver, opts ...llb.ConstraintsOpt) llb.State { - return llb.Image(azlinux3Ref, llb.WithMetaResolver(resolver), dalec.WithConstraints(opts...)).Run( +func (w azlinux3) Base(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) { + base, err := sOpt.GetContext(Azlinux3Ref, dalec.WithConstraints(opts...)) + if err != nil { + return llb.Scratch(), err + } + + if base != nil { + return *base, nil + } + + base, err = sOpt.GetContext(Azlinux3WorkerContextName, dalec.WithConstraints(opts...)) + if err != nil { + return llb.Scratch(), nil + } + if base != nil { + return *base, nil + } + + img := llb.Image(Azlinux3Ref, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...)) + return img.Run( w.Install([]string{"rpm-build", "mariner-rpm-macros", "build-essential", "ca-certificates"}, installWithConstraints(opts)), dalec.WithConstraints(opts...), - ).Root() + ).Root(), nil } func (w azlinux3) Install(pkgs []string, opts ...installOpt) llb.RunOption { @@ -53,6 +74,20 @@ func (azlinux3) DefaultImageConfig(ctx context.Context, resolver llb.ImageMetaRe return &cfg, nil } +func (azlinux3) WorkerImageConfig(ctx context.Context, resolver llb.ImageMetaResolver, platform *ocispecs.Platform) (*dalec.DockerImageSpec, error) { + _, _, dt, err := resolver.ResolveImageConfig(ctx, Azlinux3Ref, sourceresolver.Opt{Platform: platform}) + if err != nil { + return nil, err + } + + var cfg dalec.DockerImageSpec + if err := json.Unmarshal(dt, &cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + func (azlinux3) tdnfCacheMount(root string) llb.RunOption { return llb.AddMount(filepath.Join(root, tdnfCacheDir), llb.Scratch(), llb.AsPersistentCacheDir(tdnfCacheNameAzlinux3, llb.CacheMountLocked)) } diff --git a/frontend/azlinux/handle_container.go b/frontend/azlinux/handle_container.go index b6fe4c3bd..09871f7b7 100644 --- a/frontend/azlinux/handle_container.go +++ b/frontend/azlinux/handle_container.go @@ -35,7 +35,7 @@ func handleContainer(w worker) gwclient.BuildFunc { return nil, nil, err } - st, err := specToContainerLLB(w, client, spec, targetKey, rpmDir, rpms, sOpt, pg) + st, err := specToContainerLLB(w, spec, targetKey, rpmDir, rpms, sOpt, pg) if err != nil { return nil, nil, err } @@ -62,12 +62,17 @@ func handleContainer(w worker) gwclient.BuildFunc { return nil, nil, err } + base, err := w.Base(sOpt, pg) + if err != nil { + return nil, nil, err + } + withTestDeps := func(in llb.State) llb.State { deps := spec.GetTestDeps(targetKey) if len(deps) == 0 { return in } - return w.Base(client, pg).Run( + return base.Run( w.Install(spec.GetTestDeps(targetKey), atRoot("/tmp/rootfs")), pg, dalec.ProgressGroup("Install test dependencies"), @@ -130,11 +135,14 @@ func readRPMs(ctx context.Context, client gwclient.Client, st llb.State) ([]stri return out, nil } -func specToContainerLLB(w worker, client gwclient.Client, spec *dalec.Spec, targetKey string, rpmDir llb.State, files []string, sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) { +func specToContainerLLB(w worker, spec *dalec.Spec, targetKey string, rpmDir llb.State, files []string, sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) { opts = append(opts, dalec.ProgressGroup("Install RPMs")) const workPath = "/tmp/rootfs" - builderImg := w.Base(client, opts...) + builderImg, err := w.Base(sOpt, opts...) + if err != nil { + return llb.Scratch(), err + } rootfs := llb.Scratch() if ref := dalec.GetBaseOutputImage(spec, targetKey); ref != "" { diff --git a/frontend/azlinux/handle_depsonly.go b/frontend/azlinux/handle_depsonly.go index 6377059d6..068d516f1 100644 --- a/frontend/azlinux/handle_depsonly.go +++ b/frontend/azlinux/handle_depsonly.go @@ -15,7 +15,16 @@ func handleDepsOnly(w worker) gwclient.BuildFunc { return func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { 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) { pg := dalec.ProgressGroup("Build mariner2 deps-only container: " + spec.Name) - baseImg := w.Base(client, pg) + + sOpt, err := frontend.SourceOptFromClient(ctx, client) + if err != nil { + return nil, nil, err + } + + baseImg, err := w.Base(sOpt, pg) + if err != nil { + return nil, nil, err + } rpmDir := baseImg.Run( dalec.ShArgs(`set -ex; dir="/tmp/rpms/RPMS/$(uname -m)"; mkdir -p "${dir}"; tdnf install -y --releasever=2.0 --downloadonly --alldeps --downloaddir "${dir}" `+strings.Join(spec.GetRuntimeDeps(targetKey), " ")), pg, @@ -27,11 +36,7 @@ func handleDepsOnly(w worker) gwclient.BuildFunc { return nil, nil, err } - sOpt, err := frontend.SourceOptFromClient(ctx, client) - if err != nil { - return nil, nil, err - } - st, err := specToContainerLLB(w, client, spec, targetKey, rpmDir, files, sOpt, pg) + st, err := specToContainerLLB(w, spec, targetKey, rpmDir, files, sOpt, pg) if err != nil { return nil, nil, err } diff --git a/frontend/azlinux/handle_rpm.go b/frontend/azlinux/handle_rpm.go index 5f6798e6a..d376f3254 100644 --- a/frontend/azlinux/handle_rpm.go +++ b/frontend/azlinux/handle_rpm.go @@ -65,7 +65,12 @@ func installBuildDeps(w worker, spec *dalec.Spec, targetKey string, opts ...llb. } func specToRpmLLB(ctx context.Context, w worker, client gwclient.Client, spec *dalec.Spec, sOpt dalec.SourceOpts, targetKey string, opts ...llb.ConstraintsOpt) (llb.State, error) { - base := w.Base(client, opts...).With(installBuildDeps(w, spec, targetKey, opts...)) + base, err := w.Base(sOpt, opts...) + base = base.With(installBuildDeps(w, spec, targetKey, opts...)) + if err != nil { + return llb.Scratch(), err + } + br, err := rpm.SpecToBuildrootLLB(base, spec, sOpt, targetKey, opts...) if err != nil { return llb.Scratch(), err diff --git a/frontend/azlinux/handler.go b/frontend/azlinux/handler.go index 883266b59..06581a206 100644 --- a/frontend/azlinux/handler.go +++ b/frontend/azlinux/handler.go @@ -19,9 +19,10 @@ const ( ) type worker interface { - Base(resolver llb.ImageMetaResolver, opts ...llb.ConstraintsOpt) llb.State + Base(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) Install(pkgs []string, opts ...installOpt) llb.RunOption DefaultImageConfig(context.Context, llb.ImageMetaResolver, *ocispecs.Platform) (*dalec.DockerImageSpec, error) + WorkerImageConfig(context.Context, llb.ImageMetaResolver, *ocispecs.Platform) (*dalec.DockerImageSpec, error) } func newHandler(w worker) gwclient.BuildFunc { @@ -44,18 +45,30 @@ func newHandler(w worker) gwclient.BuildFunc { Description: "Builds a container image with only the runtime dependencies installed.", }) + mux.Add("worker", handleBaseImg(w), &targets.Target{ + Name: "worker", + Description: "Builds the base worker image responsible for building the rpm", + }) + return mux.Handle } func handleDebug(w worker) gwclient.BuildFunc { return func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { - return rpm.HandleDebug(getSpecWorker(w))(ctx, client) + sOpt, err := frontend.SourceOptFromClient(ctx, client) + if err != nil { + return nil, err + } + return rpm.HandleDebug(getSpecWorker(w, sOpt))(ctx, client) } } -func getSpecWorker(w worker) rpm.WorkerFunc { +func getSpecWorker(w worker, sOpt dalec.SourceOpts) rpm.WorkerFunc { return func(resolver llb.ImageMetaResolver, spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) (llb.State, error) { - st := w.Base(resolver, opts...) + st, err := w.Base(sOpt, opts...) + if err != nil { + return llb.Scratch(), err + } if spec.HasGomods() { deps := spec.GetBuildDeps(targetKey) hasGolang := func(s string) bool { @@ -70,3 +83,46 @@ func getSpecWorker(w worker) rpm.WorkerFunc { return st, nil } } + +func handleBaseImg(w worker) gwclient.BuildFunc { + return func(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { + 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) { + + sOpt, err := frontend.SourceOptFromClient(ctx, client) + if err != nil { + return nil, nil, err + } + + st, err := w.Base(sOpt) + if err != nil { + return nil, nil, err + } + + def, err := st.Marshal(ctx) + if err != nil { + return nil, nil, err + } + + req := gwclient.SolveRequest{ + Definition: def.ToPB(), + } + + res, err := client.Solve(ctx, req) + if err != nil { + return nil, nil, err + } + + ref, err := res.SingleRef() + if err != nil { + return nil, nil, err + } + + cfg, err := w.DefaultImageConfig(ctx, client, platform) + if err != nil { + return nil, nil, err + } + + return ref, cfg, nil + }) + } +} diff --git a/frontend/azlinux/mariner2.go b/frontend/azlinux/mariner2.go index 54a98f9ae..e0424c55a 100644 --- a/frontend/azlinux/mariner2.go +++ b/frontend/azlinux/mariner2.go @@ -16,8 +16,9 @@ const ( Mariner2TargetKey = "mariner2" tdnfCacheNameMariner2 = "mariner2-tdnf-cache" - mariner2Ref = "mcr.microsoft.com/cbl-mariner/base/core:2.0" - mariner2DistrolessRef = "mcr.microsoft.com/cbl-mariner/distroless/base:2.0" + Mariner2Ref = "mcr.microsoft.com/cbl-mariner/base/core:2.0" + Mariner2WorkerContextName = "dalec-mariner2-worker" + mariner2DistrolessRef = "mcr.microsoft.com/cbl-mariner/distroless/base:2.0" ) func NewMariner2Handler() gwclient.BuildFunc { @@ -26,11 +27,28 @@ func NewMariner2Handler() gwclient.BuildFunc { type mariner2 struct{} -func (w mariner2) Base(resolver llb.ImageMetaResolver, opts ...llb.ConstraintsOpt) llb.State { - return llb.Image(mariner2Ref, llb.WithMetaResolver(resolver), dalec.WithConstraints(opts...)).Run( - w.Install([]string{"rpm-build", "mariner-rpm-macros", "systemd-rpm-macros", "build-essential", "ca-certificates"}, installWithConstraints(opts)), +func (w mariner2) Base(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) { + base, err := sOpt.GetContext(Mariner2Ref, dalec.WithConstraints(opts...)) + if err != nil { + return llb.Scratch(), err + } + + if base == nil { + base, err = sOpt.GetContext(Mariner2WorkerContextName, dalec.WithConstraints(opts...)) + if err != nil { + return llb.Scratch(), nil + } + } + + if base == nil { + st := llb.Image(Mariner2Ref, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...)) + base = &st + } + + return base.Run( + w.Install([]string{"rpm-build", "mariner-rpm-macros", "build-essential", "ca-certificates"}, installWithConstraints(opts)), dalec.WithConstraints(opts...), - ).Root() + ).Root(), nil } func (w mariner2) Install(pkgs []string, opts ...installOpt) llb.RunOption { @@ -55,6 +73,20 @@ func (mariner2) DefaultImageConfig(ctx context.Context, resolver llb.ImageMetaRe return &cfg, nil } +func (mariner2) WorkerImageConfig(ctx context.Context, resolver llb.ImageMetaResolver, platform *ocispecs.Platform) (*dalec.DockerImageSpec, error) { + _, _, dt, err := resolver.ResolveImageConfig(ctx, Mariner2Ref, sourceresolver.Opt{Platform: platform}) + if err != nil { + return nil, err + } + + var cfg dalec.DockerImageSpec + if err := json.Unmarshal(dt, &cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + func (mariner2) tdnfCacheMount(root string) llb.RunOption { return llb.AddMount(filepath.Join(root, tdnfCacheDir), llb.Scratch(), llb.AsPersistentCacheDir(tdnfCacheNameMariner2, llb.CacheMountLocked)) } diff --git a/frontend/windows/handle_container.go b/frontend/windows/handle_container.go index 0f7448702..4e9368776 100644 --- a/frontend/windows/handle_container.go +++ b/frontend/windows/handle_container.go @@ -54,7 +54,10 @@ func handleContainer(ctx context.Context, client gwclient.Client) (*gwclient.Res } pg := dalec.ProgressGroup("Build windows container: " + spec.Name) - worker := workerImg(sOpt, pg) + worker, err := workerImg(sOpt, pg) + if err != nil { + return nil, nil, err + } bin, err := buildBinaries(ctx, spec, worker, client, sOpt, targetKey) if err != nil { diff --git a/frontend/windows/handle_zip.go b/frontend/windows/handle_zip.go index 5f0ff72db..866391c31 100644 --- a/frontend/windows/handle_zip.go +++ b/frontend/windows/handle_zip.go @@ -18,10 +18,11 @@ import ( ) const ( - workerImgRef = "mcr.microsoft.com/mirror/docker/library/ubuntu:jammy" - outputDir = "/tmp/output" - buildScriptName = "_build.sh" - aptCachePrefix = "jammy-windowscross" + workerImgRef = "mcr.microsoft.com/mirror/docker/library/ubuntu:jammy" + WindowscrossWorkerContextName = "dalec-windowscross-worker" + outputDir = "/tmp/output" + buildScriptName = "_build.sh" + aptCachePrefix = "jammy-windowscross" ) func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { @@ -32,7 +33,10 @@ func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, e } pg := dalec.ProgressGroup("Build windows container: " + spec.Name) - worker := workerImg(sOpt, pg) + worker, err := workerImg(sOpt, pg) + if err != nil { + return nil, nil, err + } bin, err := buildBinaries(ctx, spec, worker, client, sOpt, targetKey) if err != nil { @@ -168,13 +172,29 @@ func generateInvocationScript(binaries []string) *strings.Builder { return script } -func workerImg(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) llb.State { - // TODO: support named context override... also this should possibly be its own image, maybe? +func workerImg(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) { + base, err := sOpt.GetContext(workerImgRef, dalec.WithConstraints(opts...)) + if err != nil { + return llb.Scratch(), err + } + + if base != nil { + return *base, nil + } + + base, err = sOpt.GetContext(WindowscrossWorkerContextName, dalec.WithConstraints(opts...)) + if err != nil { + return llb.Scratch(), nil + } + if base != nil { + return *base, nil + } + return llb.Image(workerImgRef, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...)). Run( dalec.ShArgs("apt-get update && apt-get install -y build-essential binutils-mingw-w64 g++-mingw-w64-x86-64 gcc git make pkg-config quilt zip"), dalec.WithMountedAptCache(aptCachePrefix), - ).Root() + ).Root(), nil } func createBuildScript(spec *dalec.Spec) llb.State { diff --git a/frontend/windows/handler.go b/frontend/windows/handler.go index 0e27afa7b..55f0a8327 100644 --- a/frontend/windows/handler.go +++ b/frontend/windows/handler.go @@ -2,10 +2,15 @@ package windows import ( "context" + "encoding/json" + "github.com/Azure/dalec" "github.com/Azure/dalec/frontend" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/client/llb/sourceresolver" gwclient "github.com/moby/buildkit/frontend/gateway/client" bktargets "github.com/moby/buildkit/frontend/subrequests/targets" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" ) const ( @@ -26,5 +31,62 @@ func Handle(ctx context.Context, client gwclient.Client) (*gwclient.Result, erro Description: "Builds binaries and installs them into a Windows base image", Default: true, }) + + mux.Add("worker", handleWorker, &bktargets.Target{ + Name: "worker", + Description: "Builds the base worker image responsible for building the package", + }) + return mux.Handle(ctx, client) } + +func handleWorker(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { + 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) { + sOpt, err := frontend.SourceOptFromClient(ctx, client) + if err != nil { + return nil, nil, err + } + + var opts []llb.ConstraintsOpt + if platform != nil { + opts = append(opts, llb.Platform(*platform)) + } + + st, err := workerImg(sOpt, opts...) + if err != nil { + return nil, nil, err + } + + def, err := st.Marshal(ctx) + if err != nil { + return nil, nil, err + } + + res, err := client.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + + if err != nil { + return nil, nil, err + } + + _, _, dt, err := client.ResolveImageConfig(ctx, workerImgRef, sourceresolver.Opt{ + Platform: platform, + }) + if err != nil { + return nil, nil, err + } + + var img dalec.DockerImageSpec + if err := json.Unmarshal(dt, &img); err != nil { + return nil, nil, err + } + + ref, err := res.SingleRef() + if err != nil { + return nil, nil, err + } + + return ref, &img, nil + }) +} diff --git a/test/azlinux_test.go b/test/azlinux_test.go index d8eee5be7..3dcdf9f1e 100644 --- a/test/azlinux_test.go +++ b/test/azlinux_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/Azure/dalec" + "github.com/Azure/dalec/frontend/azlinux" "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" moby_buildkit_v1_frontend "github.com/moby/buildkit/frontend/gateway/pb" @@ -19,9 +20,12 @@ func TestMariner2(t *testing.T) { ctx := startTestSpan(baseCtx, t) testLinuxDistro(ctx, t, testLinuxConfig{ - BuildTarget: "mariner2/container", - SignTarget: "mariner2/rpm", - LicenseDir: "/usr/share/licenses", + Target: targetConfig{ + Package: "mariner2/rpm", + Container: "mariner2/container", + Worker: "mariner2/worker", + }, + LicenseDir: "/usr/share/licenses", SystemdDir: struct { Units string Targets string @@ -29,6 +33,10 @@ func TestMariner2(t *testing.T) { Units: "/usr/lib/systemd", Targets: "/etc/systemd/system", }, + Worker: workerConfig{ + ContextName: azlinux.Mariner2WorkerContextName, + CreateRepo: azlinuxWithRepo, + }, }) } @@ -37,9 +45,12 @@ func TestAzlinux3(t *testing.T) { ctx := startTestSpan(baseCtx, t) testLinuxDistro(ctx, t, testLinuxConfig{ - BuildTarget: "azlinux3/container", - SignTarget: "azlinux3/rpm", - LicenseDir: "/usr/share/licenses", + Target: targetConfig{ + Package: "azlinux3/rpm", + Container: "azlinux3/container", + Worker: "azlinux3/worker", + }, + LicenseDir: "/usr/share/licenses", SystemdDir: struct { Units string Targets string @@ -47,17 +58,65 @@ func TestAzlinux3(t *testing.T) { Units: "/usr/lib/systemd", Targets: "/etc/systemd/system", }, + Worker: workerConfig{ + ContextName: azlinux.Azlinux3WorkerContextName, + CreateRepo: azlinuxWithRepo, + }, }) } +func azlinuxWithRepo(rpms llb.State) llb.StateOption { + return func(in llb.State) llb.State { + localRepo := []byte(` +[Local] +name=Local Repository +baseurl=file:///opt/repo +gpgcheck=0 +priority=0 +enabled=1 +`) + pg := dalec.ProgressGroup("Install local repo for test") + return in. + File(llb.Mkdir("/opt/repo/RPMS", 0o755, llb.WithParents(true)), pg). + File(llb.Mkdir("/opt/repo/SRPMS", 0o755), pg). + Run(dalec.ShArgs("tdnf install -y createrepo"), pg). + File(llb.Mkfile("/etc/yum.repos.d/local.repo", 0o644, localRepo), pg). + Run( + llb.AddMount("/tmp/st", rpms, llb.Readonly), + dalec.ShArgs("cp /tmp/st/RPMS/$(uname -m)/* /opt/repo/RPMS/ && cp /tmp/st/SRPMS/* /opt/repo/SRPMS"), + pg, + ). + Run(dalec.ShArgs("createrepo --compatibility /opt/repo"), pg). + Root() + } +} + +type workerConfig struct { + // CreateRepo takes in a state which is the output of the sign target, + // the output [llb.StateOption] should install the repo into the worker image. + CreateRepo func(llb.State) llb.StateOption + // ContextName is the name of the worker context that the build target will use + // to see if a custom worker is proivded in a context + ContextName string +} + +type targetConfig struct { + // Package is the target for creating a package. + Package string + // Container is the target for creating a container. + Container string + // Target is the build target for creating the worker image. + Worker string +} + type testLinuxConfig struct { - BuildTarget string - SignTarget string - LicenseDir string - SystemdDir struct { + Target targetConfig + LicenseDir string + SystemdDir struct { Units string Targets string } + Worker workerConfig } func testLinuxDistro(ctx context.Context, t *testing.T, testConfig testLinuxConfig) { @@ -82,7 +141,7 @@ func testLinuxDistro(ctx context.Context, t *testing.T, testConfig testLinuxConf } testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { - sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(testConfig.BuildTarget)) + sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(testConfig.Target.Container)) sr.Evaluate = true _, err := gwc.Solve(ctx, sr) var xErr *moby_buildkit_v1_frontend.ExitError @@ -116,7 +175,7 @@ func testLinuxDistro(ctx context.Context, t *testing.T, testConfig testLinuxConf } testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { - sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(testConfig.BuildTarget)) + sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(testConfig.Target.Container)) sr.Evaluate = true _, err := gwc.Solve(ctx, sr) @@ -348,7 +407,7 @@ echo "$BAR" > bar.txt testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { sr := newSolveRequest( withSpec(ctx, t, &spec), - withBuildTarget(testConfig.BuildTarget), + withBuildTarget(testConfig.Target.Container), withBuildContext(ctx, t, src2Patch3ContextName, src2Patch3Context), ) sr.Evaluate = true @@ -438,7 +497,7 @@ WantedBy=multi-user.target } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) solveT(ctx, t, client, req) }) @@ -464,7 +523,7 @@ WantedBy=multi-user.target } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) solveT(ctx, t, client, req) }) @@ -493,7 +552,7 @@ WantedBy=multi-user.target } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) solveT(ctx, t, client, req) }) }) @@ -607,7 +666,7 @@ WantedBy=multi-user.target } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) solveT(ctx, t, client, req) }) }) @@ -663,7 +722,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) solveT(ctx, t, client, req) }) }) @@ -719,7 +778,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) solveT(ctx, t, client, req) }) }) @@ -770,7 +829,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) res := solveT(ctx, t, client, req) ref, err := res.SingleRef() @@ -859,7 +918,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) res := solveT(ctx, t, client, req) ref, err := res.SingleRef() @@ -935,7 +994,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - sr := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + sr := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) sr.Evaluate = true solveT(ctx, t, client, sr) }) @@ -1014,7 +1073,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - sr := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + sr := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) sr.Evaluate = true solveT(ctx, t, client, sr) }) @@ -1040,7 +1099,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(testConfig.BuildTarget), withSpec(ctx, t, spec)) + req := newSolveRequest(withBuildTarget(testConfig.Target.Container), withSpec(ctx, t, spec)) res := solveT(ctx, t, client, req) ref, err := res.SingleRef() if err != nil { @@ -1055,6 +1114,103 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot } }) }) + + t.Run("custom worker", func(t *testing.T) { + t.Parallel() + ctx := startTestSpan(baseCtx, t) + testCustomLinuxWorker(ctx, t, testConfig.Target, testConfig.Worker) + }) +} + +func testCustomLinuxWorker(ctx context.Context, t *testing.T, targetCfg targetConfig, workerCfg workerConfig) { + testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { + // base package that will be used as a build dependency of the main package. + depSpec := &dalec.Spec{ + Name: "dalec-test-package", + Version: "0.0.1", + Revision: "1", + Description: "A basic package for various testing uses", + License: "MIT", + Sources: map[string]dalec.Source{ + "hello.txt": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "hello world!", + }, + }, + }, + }, + Artifacts: dalec.Artifacts{ + Docs: map[string]dalec.ArtifactConfig{ + "hello.txt": {}, + }, + }, + } + + // Main package, this should fail to build without a custom worker that has + // the base package available. + spec := &dalec.Spec{ + Name: "test-dalec-custom-worker", + Version: "0.0.1", + Revision: "1", + Description: "Testing allowing custom worker images to be provided", + License: "MIT", + Dependencies: &dalec.PackageDependencies{ + Build: map[string]dalec.PackageConstraints{ + depSpec.Name: {}, + }, + Runtime: map[string]dalec.PackageConstraints{ + depSpec.Name: {}, + }, + }, + } + + // Make sure the built-in worker can't build this package + sr := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(targetCfg.Container)) + _, err := gwc.Solve(ctx, sr) + if err == nil { + t.Fatal("expected solve to fail") + } + + var xErr *moby_buildkit_v1_frontend.ExitError + if !errors.As(err, &xErr) { + t.Fatalf("got unexpected error, expected error type %T: %v", xErr, err) + } + + // Build the base package + sr = newSolveRequest(withSpec(ctx, t, depSpec), withBuildTarget(targetCfg.Package)) + pkg := reqToState(ctx, gwc, sr, t) + + // Build the worker target, this will give us the worker image as an output. + // Note: Currently we need to provide a dalec spec just due to how the router is setup. + // The spec can be nil, though, it just needs to be parsable by yaml unmarshaller. + sr = newSolveRequest(withBuildTarget(targetCfg.Worker), withSpec(ctx, t, nil)) + worker := reqToState(ctx, gwc, sr, t) + + // Add the base package + repo to the worker + // This should make it so when dalec installs build deps it can use the package + // we built above. + worker = worker.With(workerCfg.CreateRepo(pkg)) + + // Now build again with our custom worker + // Note, we are solving the main spec, not depSpec here. + sr = newSolveRequest(withSpec(ctx, t, spec), withBuildContext(ctx, t, workerCfg.ContextName, worker), withBuildTarget(targetCfg.Container)) + res := solveT(ctx, t, gwc, sr) + ref, err := res.SingleRef() + if err != nil { + t.Fatal(err) + } + + // Since we also added the dep as a runtime dep, the file in the base package should be installed in the output container. + _, err = ref.StatFile(ctx, gwclient.StatRequest{Path: "/usr/share/doc/" + depSpec.Name + "/hello.txt"}) + if err != nil { + t.Fatal(err) + } + + // TODO: we should have a test to make sure this also works with source policies. + // Unfortunately it seems like there is an issue with the gateway client passing + // in source policies. + }) } func validatePathAndPermissions(ctx context.Context, ref gwclient.Reference, path string, expected os.FileMode) error { diff --git a/test/helpers_test.go b/test/helpers_test.go index 2ff0ea7eb..632400024 100644 --- a/test/helpers_test.go +++ b/test/helpers_test.go @@ -14,6 +14,7 @@ import ( "github.com/containerd/containerd/platforms" "github.com/goccy/go-yaml" "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/exporter/containerimage/exptypes" "github.com/moby/buildkit/frontend/dockerui" gwclient "github.com/moby/buildkit/frontend/gateway/client" "github.com/moby/buildkit/frontend/subrequests/targets" @@ -266,3 +267,29 @@ func withBuildContext(ctx context.Context, t *testing.T, name string, st llb.Sta sr.FrontendInputs[name] = def.ToPB() } } + +func reqToState(ctx context.Context, gwc gwclient.Client, sr gwclient.SolveRequest, t *testing.T) llb.State { + t.Helper() + res := solveT(ctx, t, gwc, sr) + + ref, err := res.SingleRef() + if err != nil { + t.Fatal(err) + } + + st, err := ref.ToState() + if err != nil { + t.Fatal(err) + } + + dt, ok := res.Metadata[exptypes.ExporterPlatformsKey] + if ok { + var pls exptypes.Platforms + if err := json.Unmarshal(dt, &pls); err != nil { + t.Fatal(err) + } + st = st.Platform(pls.Platforms[0].Platform) + } + + return st +} diff --git a/test/signing_test.go b/test/signing_test.go index 4af1b197b..fcf19bddd 100644 --- a/test/signing_test.go +++ b/test/signing_test.go @@ -66,13 +66,13 @@ func linuxSigningTests(ctx context.Context, testConfig testLinuxConfig) func(*te t.Run("root config", func(t *testing.T) { t.Parallel() spec := newSigningSpec() - runTest(t, distroSigningTest(t, spec, testConfig.SignTarget)) + runTest(t, distroSigningTest(t, spec, testConfig.Target.Package)) }) t.Run("with target config", func(t *testing.T) { t.Parallel() spec := newSigningSpec() - first, _, _ := strings.Cut(testConfig.SignTarget, "/") + first, _, _ := strings.Cut(testConfig.Target.Package, "/") spec.Targets = map[string]dalec.Target{ first: { PackageConfig: &dalec.PackageConfig{ @@ -82,7 +82,7 @@ func linuxSigningTests(ctx context.Context, testConfig testLinuxConfig) func(*te } spec.PackageConfig.Signer = nil - runTest(t, distroSigningTest(t, spec, testConfig.SignTarget)) + runTest(t, distroSigningTest(t, spec, testConfig.Target.Package)) }) t.Run("target config takes precedence when root config is there", func(t *testing.T) { @@ -102,7 +102,7 @@ func linuxSigningTests(ctx context.Context, testConfig testLinuxConfig) func(*te } } - first, _, _ := strings.Cut(testConfig.SignTarget, "/") + first, _, _ := strings.Cut(testConfig.Target.Package, "/") spec.Targets = map[string]dalec.Target{ first: { PackageConfig: &dalec.PackageConfig{ @@ -116,7 +116,7 @@ func linuxSigningTests(ctx context.Context, testConfig testLinuxConfig) func(*te } spec.PackageConfig.Signer.Image = "notexist" - runTest(t, distroSigningTest(t, spec, testConfig.SignTarget), testenv.WithSolveStatusFn(handleStatus)) + runTest(t, distroSigningTest(t, spec, testConfig.Target.Package), testenv.WithSolveStatusFn(handleStatus)) assert.Assert(t, found, "Spec signing override warning message not emitted") }) @@ -129,7 +129,7 @@ func linuxSigningTests(ctx context.Context, testConfig testLinuxConfig) func(*te "HELLO": "world", "FOO": "bar", } - runTest(t, distroSigningTest(t, spec, testConfig.SignTarget)) + runTest(t, distroSigningTest(t, spec, testConfig.Target.Package)) }) t.Run("with path build arg and build context", func(t *testing.T) { @@ -145,7 +145,7 @@ signer: runTest(t, distroSigningTest( t, spec, - testConfig.SignTarget, + testConfig.Target.Package, withBuildContext(ctx, t, "dalec_signing_config", signConfig), withBuildArg("DALEC_SIGNING_CONFIG_CONTEXT_NAME", "dalec_signing_config"), withBuildArg("DALEC_SIGNING_CONFIG_PATH", "/unusual_place.yml"), @@ -179,7 +179,7 @@ signer: distroSigningTest( t, spec, - testConfig.SignTarget, + testConfig.Target.Package, withBuildContext(ctx, t, "dalec_signing_config", signConfig), withBuildArg("DALEC_SIGNING_CONFIG_CONTEXT_NAME", "dalec_signing_config"), withBuildArg("DALEC_SIGNING_CONFIG_PATH", "/unusual_place.yml"), @@ -203,7 +203,7 @@ signer: runTest(t, distroSigningTest( t, spec, - testConfig.SignTarget, + testConfig.Target.Package, withBuildContext(ctx, t, "dalec_signing_config", signConfig), withBuildArg("DALEC_SIGNING_CONFIG_CONTEXT_NAME", "dalec_signing_config"), withBuildArg("DALEC_SIGNING_CONFIG_PATH", "/unusual_place.yml"), @@ -224,7 +224,7 @@ signer: runTest(t, distroSigningTest( t, spec, - testConfig.SignTarget, + testConfig.Target.Package, withMainContext(ctx, t, signConfig), withBuildArg("DALEC_SIGNING_CONFIG_PATH", "/sign_config.yml"), )) @@ -256,7 +256,7 @@ signer: runTest(t, distroSigningTest( t, spec, - testConfig.SignTarget, + testConfig.Target.Package, withMainContext(ctx, t, signConfig), withBuildArg("DALEC_SIGNING_CONFIG_PATH", "/sign_config.yml"), ), testenv.WithSolveStatusFn(handleStatus)) @@ -280,7 +280,7 @@ signer: } } } - runTest(t, distroSkipSigningTest(t, spec, testConfig.SignTarget), testenv.WithSolveStatusFn(handleStatus)) + runTest(t, distroSkipSigningTest(t, spec, testConfig.Target.Package), testenv.WithSolveStatusFn(handleStatus)) assert.Assert(t, found, "Signing disabled warning message not emitted") }) @@ -313,7 +313,7 @@ signer: runTest(t, distroSkipSigningTest( t, spec, - testConfig.SignTarget, + testConfig.Target.Package, withBuildArg("DALEC_SIGNING_CONFIG_CONTEXT_NAME", "dalec_signing_config"), withBuildArg("DALEC_SIGNING_CONFIG_PATH", "/sign_config.yml"), withBuildContext(ctx, t, "dalec_signing_config", signConfig), @@ -351,7 +351,7 @@ signer: runTest(t, distroSkipSigningTest( t, spec, - testConfig.SignTarget, + testConfig.Target.Package, withMainContext(ctx, t, signConfig), withBuildArg("DALEC_SIGNING_CONFIG_PATH", "/sign_config.yml"), ), testenv.WithSolveStatusFn(handleStatus)) diff --git a/test/testenv/builld.go b/test/testenv/builld.go index 5265f3ca7..09630a425 100644 --- a/test/testenv/builld.go +++ b/test/testenv/builld.go @@ -52,6 +52,7 @@ func buildBaseFrontend(ctx context.Context, c gwclient.Client) (*gwclient.Result dockerui.DefaultLocalNameContext: defPB, dockerui.DefaultLocalNameDockerfile: dockerfileDef.ToPB(), }, + Evaluate: true, }) } diff --git a/test/windows_test.go b/test/windows_test.go index f7694d84a..2aceb5e92 100644 --- a/test/windows_test.go +++ b/test/windows_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/Azure/dalec" + "github.com/Azure/dalec/frontend/windows" "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" moby_buildkit_v1_frontend "github.com/moby/buildkit/frontend/gateway/pb" @@ -18,21 +19,63 @@ func TestWindows(t *testing.T) { ctx := startTestSpan(baseCtx, t) testWindows(ctx, t, "windowscross/container") + + t.Run("custom worker", func(t *testing.T) { + t.Parallel() + ctx := startTestSpan(baseCtx, t) + testCustomWindowscrossWorker(ctx, t, targetConfig{ + Container: "windowscross/container", + // The way the test uses the package target is to generate a package which + // it then feeds back into a custom repo and adds that package as a build dep + // to another package. + // We don't build system packages for the windowscross base image. + // There's also no .deb support (currently) + // So... use a mariner2 rpm and then in CreateRepo, convert the rpm to a deb package + // which we'll use to create the repo... + // We can switch to this jammy/deb when that is available. + Package: "mariner2/rpm", + Worker: "windowscross/worker", + }, workerConfig{ + ContextName: windows.WindowscrossWorkerContextName, + CreateRepo: func(pkg llb.State) llb.StateOption { + return func(in llb.State) llb.State { + dt := []byte(` +deb [trusted=yes] copy:/tmp/repo / +`) + + repo := in. + Run( + dalec.ShArgs("apt-get update && apt-get install -y apt-utils alien"), + dalec.WithMountedAptCache("test-windowscross"), + ). + Run( + llb.Dir("/tmp/repo"), + dalec.ShArgs("set -e; for i in ./RPMS/*/*.rpm; do alien --to-deb \"$i\"; done; rm -rf ./RPMS; rm -rf ./SRPMS; apt-ftparchive packages . | gzip -1 > Packages.gz"), + ). + AddMount("/tmp/repo", pkg) + + return in. + File(llb.Mkfile("/etc/apt/sources.list.d/windowscross.list", 0o644, dt)). + File(llb.Copy(repo, "/", "/tmp/repo")) + } + }, + }) + }) } -func testWindows(ctx context.Context, t *testing.T, buildTarget string) { - // Windows is only supported on amd64 (ie there is no arm64 windows image currently) - // This allows the test to run on arm64 machines. - // I looked at having a good way to skip the test on non-amd64 and it all ends up - // being a bit janky and error prone. - // I'd rather just let the test run since it will work when we set an explicit platform - withAmd64Platform := func(sr *gwclient.SolveRequest) { - if sr.FrontendOpt == nil { - sr.FrontendOpt = make(map[string]string) - } - sr.FrontendOpt["platform"] = "windows/amd64" +// Windows is only supported on amd64 (ie there is no arm64 windows image currently) +// This allows the test to run on arm64 machines. +// I looked at having a good way to skip the test on non-amd64 and it all ends up +// being a bit janky and error prone. +// I'd rather just let the test run since it will work when we set an explicit platform +func withWindowsAmd64(sr *gwclient.SolveRequest) { + if sr.FrontendOpt == nil { + sr.FrontendOpt = make(map[string]string) } + sr.FrontendOpt["platform"] = "windows/amd64" +} +func testWindows(ctx context.Context, t *testing.T, buildTarget string) { t.Run("Fail when non-zero exit code during build", func(t *testing.T) { t.Parallel() spec := dalec.Spec{ @@ -54,7 +97,7 @@ func testWindows(ctx context.Context, t *testing.T, buildTarget string) { } testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { - sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(buildTarget), withAmd64Platform) + sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(buildTarget), withWindowsAmd64) sr.Evaluate = true _, err := gwc.Solve(ctx, sr) var xErr *moby_buildkit_v1_frontend.ExitError @@ -85,7 +128,7 @@ func testWindows(ctx context.Context, t *testing.T, buildTarget string) { } testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { - sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(buildTarget), withAmd64Platform) + sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(buildTarget), withWindowsAmd64) sr.Evaluate = true _, err := gwc.Solve(ctx, sr) @@ -219,7 +262,7 @@ echo "$BAR" > bar.txt } testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { - sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(buildTarget), withAmd64Platform) + sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(buildTarget), withWindowsAmd64) sr.Evaluate = true res := solveT(ctx, t, gwc, sr) @@ -310,7 +353,7 @@ echo "$BAR" > bar.txt } testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) { - req := newSolveRequest(withBuildTarget(buildTarget), withSpec(ctx, t, spec), withAmd64Platform) + req := newSolveRequest(withBuildTarget(buildTarget), withSpec(ctx, t, spec), withWindowsAmd64) solveT(ctx, t, client, req) }) }) @@ -371,23 +414,6 @@ func getZipperState(ctx context.Context, t *testing.T, gwc gwclient.Client) llb. return zipper } -func reqToState(ctx context.Context, gwc gwclient.Client, sr gwclient.SolveRequest, t *testing.T) llb.State { - t.Helper() - res := solveT(ctx, t, gwc, sr) - - ref, err := res.SingleRef() - if err != nil { - t.Fatal(err) - } - - st, err := ref.ToState() - if err != nil { - t.Fatal(err) - } - - return st -} - func fillMetadata(fakename string, s *dalec.Spec) *dalec.Spec { s.Name = fakename s.Version = "0.0.1" @@ -400,3 +426,81 @@ func fillMetadata(fakename string, s *dalec.Spec) *dalec.Spec { return s } + +func testCustomWindowscrossWorker(ctx context.Context, t *testing.T, targetCfg targetConfig, workerCfg workerConfig) { + testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { + // base package that will be used as a build dependency of the main package. + depSpec := &dalec.Spec{ + Name: "dalec-test-package", + Version: "0.0.1", + Revision: "1", + Description: "A basic package for various testing uses", + License: "MIT", + Sources: map[string]dalec.Source{ + "hello.txt": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "hello world!", + }, + }, + }, + }, + Artifacts: dalec.Artifacts{ + Docs: map[string]dalec.ArtifactConfig{ + "hello.txt": {}, + }, + }, + } + + // Main package, this should fail to build without a custom worker that has + // the base package available. + spec := &dalec.Spec{ + Name: "test-dalec-custom-worker", + Version: "0.0.1", + Revision: "1", + Description: "Testing allowing custom worker images to be provided", + License: "MIT", + Dependencies: &dalec.PackageDependencies{ + Build: map[string]dalec.PackageConstraints{ + depSpec.Name: {}, + }, + }, + } + + // Make sure the built-in worker can't build this package + sr := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(targetCfg.Container), withWindowsAmd64) + _, err := gwc.Solve(ctx, sr) + if err == nil { + t.Fatal("expected solve to fail") + } + + var xErr *moby_buildkit_v1_frontend.ExitError + if !errors.As(err, &xErr) { + t.Fatalf("got unexpected error, expected error type %T: %v", xErr, err) + } + + // Build the base package + sr = newSolveRequest(withSpec(ctx, t, depSpec), withBuildTarget(targetCfg.Package)) + pkg := reqToState(ctx, gwc, sr, t) + + // Build the worker target, this will give us the worker image as an output. + // Note: Currently we need to provide a dalec spec just due to how the router is setup. + // The spec can be nil, though, it just needs to be parsable by yaml unmarshaller. + sr = newSolveRequest(withBuildTarget(targetCfg.Worker), withSpec(ctx, t, nil)) + worker := reqToState(ctx, gwc, sr, t) + + // Add the base package + repo to the worker + // This should make it so when dalec installs build deps it can use the package + // we built above. + worker = worker.With(workerCfg.CreateRepo(pkg)) + + // Now build again with our custom worker + // Note, we are solving the main spec, not depSpec here. + sr = newSolveRequest(withSpec(ctx, t, spec), withBuildContext(ctx, t, workerCfg.ContextName, worker), withBuildTarget(targetCfg.Container), withWindowsAmd64) + solveT(ctx, t, gwc, sr) + + // TODO: we should have a test to make sure this also works with source policies. + // Unfortunately it seems like there is an issue with the gateway client passing + // in source policies. + }) +} diff --git a/website/docs/targets.md b/website/docs/targets.md index d306d2016..bf01ffb01 100644 --- a/website/docs/targets.md +++ b/website/docs/targets.md @@ -11,14 +11,14 @@ Many components, such as package dependencies and base images, are specific to a To print a list of available build targets: ```shell -docker build --print=targets -f test/fixtures/moby-runc.yml . -TARGET DESCRIPTION +GET DESCRIPTION azlinux3/container (default) Builds a container image for azlinux3/container/depsonly Builds a container image with only the runtime dependencies installed. azlinux3/rpm Builds an rpm and src.rpm. azlinux3/rpm/debug/buildroot Outputs an rpm buildroot suitable for passing to rpmbuild. azlinux3/rpm/debug/sources Outputs all the sources specified in the spec file in the format given to rpmbuild. azlinux3/rpm/debug/spec Outputs the generated RPM spec file +azlinux3/worker Builds the base worker image responsible for building the rpm debug/gomods Outputs all the gomodule dependencies for the spec debug/resolve Outputs the resolved dalec spec file with build args applied. debug/sources Outputs all sources from a dalec spec file. @@ -28,7 +28,9 @@ mariner2/rpm Builds an rpm and src.rpm. mariner2/rpm/debug/buildroot Outputs an rpm buildroot suitable for passing to rpmbuild. mariner2/rpm/debug/sources Outputs all the sources specified in the spec file in the format given to rpmbuild. mariner2/rpm/debug/spec Outputs the generated RPM spec file +mariner2/worker Builds the base worker image responsible for building the rpm windowscross/container (default) Builds binaries and installs them into a Windows base image +windowscross/worker Builds the base worker image responsible for building the rpm windowscross/zip Builds binaries combined into a zip file ``` @@ -62,3 +64,50 @@ targets: frontend: image: docker.io/my/custom:mariner2 ``` + +## Advanced Customization + +### Worker images + +In some cases you may need to have additional things installed in the worker +image that are not typically available in the base image. As an example, a +package dependency may not be available in the default package repositories. + +You can have Dalec output an image with the target's worker image with +`/worker>` build target, e.g. `--target=mariner2/worker`. You can then +add any customizations and feed that back in via [source polices](#source-policies) +or [named build contexts](#named-build-contexts). + + +#### Source Policies + +`docker buildx build` has experimental support for providing a +[source policy](https://docs.docker.com/build/building/variables/#experimental_buildkit_source_policy) +which updates the base image ref used to create the worker image. This method +will update any and all references to the matched image used for any part of +the build. It also requires knowing the image(s) that are used ahead of time and +creating the right set of match rules and potentially having to update this in +the future if the worker image refs in Dalec change. + +A finer grained approach is to use [named build contexts](#named-build-contexts). + +#### Named Build Contexts + +`docker buildx build` has a flag called `--build-context` +([doc](https://docs.docker.com/reference/cli/docker/buildx/build/#build-context)) +which allows you to provide additional build contexts apart from the main build +context in the form of `=`. See the prior linked documentation for +what can go into ``. + +In the `mariner2` target, Dalec looks for a named context called either + +1. The actual base image used internally for mariner2 + i. `--build-context mcr.microsoft.com/cbl-mariner/base/core:2.0=` +2. A build context named `dalec-mariner2-worker` + i. `--build-context dalec-mariner2-worker=` + +If 1 is provided, then 2 is ignored. + +This works the same way in the `azlinux3` target but with the `azlinux3` base image +(not currently displayed here since it is still preview, this will be updated +once azlinux3 is GA) OR a build context named `dalec-azlinux3-worker`.