From 8e221b4029d3988fe466768933be537e930d1b16 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Tue, 23 Apr 2024 00:39:47 +0000 Subject: [PATCH] Prepare for adding azlinux3 support This moves some things around to make the mariner2 implementation re-usable for azlinux3. --- cmd/frontend/main.go | 11 +- frontend/azlinux/handle_container.go | 256 ++++++++++++++++++++ frontend/azlinux/handle_depsonly.go | 65 +++++ frontend/azlinux/handle_rpm.go | 79 ++++++ frontend/azlinux/handler.go | 45 ++++ frontend/azlinux/mariner2.go | 68 ++++++ frontend/imgconfig.go | 4 +- frontend/mariner2/handle_container.go | 193 --------------- frontend/mariner2/handle_depsonly.go | 59 ----- frontend/mariner2/handle_rpm.go | 113 --------- frontend/mariner2/handler.go | 37 --- frontend/rpm/handle_sources.go | 4 - frontend/rpm/{handle_rpm.go => rpmbuild.go} | 4 + 13 files changed, 524 insertions(+), 414 deletions(-) create mode 100644 frontend/azlinux/handle_container.go create mode 100644 frontend/azlinux/handle_depsonly.go create mode 100644 frontend/azlinux/handle_rpm.go create mode 100644 frontend/azlinux/handler.go create mode 100644 frontend/azlinux/mariner2.go delete mode 100644 frontend/mariner2/handle_container.go delete mode 100644 frontend/mariner2/handle_depsonly.go delete mode 100644 frontend/mariner2/handle_rpm.go delete mode 100644 frontend/mariner2/handler.go rename frontend/rpm/{handle_rpm.go => rpmbuild.go} (96%) diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index 0c6c37b78..e6f3e2d49 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -4,16 +4,15 @@ import ( _ "embed" "os" + "github.com/Azure/dalec/frontend" + "github.com/Azure/dalec/frontend/azlinux" + "github.com/Azure/dalec/frontend/debug" + "github.com/Azure/dalec/frontend/windows" "github.com/moby/buildkit/frontend/gateway/grpcclient" "github.com/moby/buildkit/util/appcontext" "github.com/moby/buildkit/util/bklog" "github.com/sirupsen/logrus" "google.golang.org/grpc/grpclog" - - "github.com/Azure/dalec/frontend" - "github.com/Azure/dalec/frontend/debug" - "github.com/Azure/dalec/frontend/mariner2" - "github.com/Azure/dalec/frontend/windows" ) const ( @@ -32,7 +31,7 @@ func main() { if err := grpcclient.RunFromEnvironment(ctx, mux.Handler( // copy/paster's beware: [frontend.WithTargetForwardingHandler] should not be set except for the root dalec frontend. - frontend.WithBuiltinHandler(mariner2.DefaultTargetKey, mariner2.Handle), + frontend.WithBuiltinHandler(azlinux.Mariner2TargetKey, azlinux.NewMariner2Handler()), frontend.WithBuiltinHandler(windows.DefaultTargetKey, windows.Handle), frontend.WithTargetForwardingHandler, )); err != nil { diff --git a/frontend/azlinux/handle_container.go b/frontend/azlinux/handle_container.go new file mode 100644 index 000000000..91ae062e9 --- /dev/null +++ b/frontend/azlinux/handle_container.go @@ -0,0 +1,256 @@ +package azlinux + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "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" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +func handleContainer(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 + } + + pg := dalec.ProgressGroup("Build mariner2 container: " + spec.Name) + + rpmDir, err := specToRpmLLB(w, client, spec, sOpt, targetKey, pg) + if err != nil { + return nil, nil, fmt.Errorf("error creating rpm: %w", err) + } + + rpms, err := readRPMs(ctx, client, rpmDir) + if err != nil { + return nil, nil, err + } + + st, err := specToContainerLLB(w, client, spec, targetKey, rpmDir, rpms, sOpt, pg) + if err != nil { + return nil, nil, err + } + + def, err := st.Marshal(ctx, pg) + if err != nil { + return nil, nil, fmt.Errorf("error marshalling llb: %w", err) + } + + res, err := client.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + return nil, nil, err + } + + var img *dalec.DockerImageSpec + if base := frontend.GetBaseOutputImage(spec, targetKey, ""); base != "" { + _, _, dt, err := client.ResolveImageConfig(ctx, base, sourceresolver.Opt{}) + if err != nil { + return nil, nil, errors.Wrap(err, "error resolving base image config") + } + var cfg dalec.DockerImageSpec + if err := json.Unmarshal(dt, &cfg); err != nil { + return nil, nil, errors.Wrap(err, "error unmarshalling base image config") + } + img = &cfg + } else { + img, err = w.DefaultImageConfig(ctx, client) + if err != nil { + return nil, nil, errors.Wrap(err, "could not get default image config") + } + } + + // TODO: DNM: This is not merging the image config from the spec. + specImg := frontend.MergeSpecImage(spec, targetKey) + + err = dalec.MergeImageConfig(&img.Config, specImg) + if err != nil { + return nil, nil, err + } + + ref, err := res.SingleRef() + if err != nil { + return nil, nil, err + } + + if err := frontend.RunTests(ctx, client, spec, ref, targetKey); err != nil { + return nil, nil, err + } + + return ref, img, err + }) + } +} + +func readRPMs(ctx context.Context, client gwclient.Client, st llb.State) ([]string, error) { + def, err := st.Marshal(ctx) + if err != nil { + return nil, err + } + + res, err := client.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + return nil, err + } + + ref, err := res.SingleRef() + if err != nil { + return nil, err + } + + // Directory layout will have arch-specific sub-folders and/or `noarch` + // RPMs will be in those subdirectories. + arches, err := ref.ReadDir(ctx, gwclient.ReadDirRequest{ + Path: "/RPMS", + }) + if err != nil { + return nil, errors.Wrap(err, "error reading output state") + } + + var out []string + + for _, arch := range arches { + files, err := ref.ReadDir(ctx, gwclient.ReadDirRequest{ + Path: filepath.Join("/RPMS", arch.Path), + IncludePattern: "*.rpm", + }) + + if err != nil { + return nil, errors.Wrap(err, "could not read arch specific output dir") + } + + for _, e := range files { + out = append(out, filepath.Join(arch.Path, e.Path)) + } + } + + return out, nil +} + +func specToContainerLLB(w worker, client gwclient.Client, spec *dalec.Spec, target 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...) + + // TODO: This is mariner specific and should probably be moved out of this + // package. + mfstDir := filepath.Join(workPath, "var/lib/rpmmanifest") + mfst1 := filepath.Join(mfstDir, "container-manifest-1") + mfst2 := filepath.Join(mfstDir, "container-manifest-2") + rpmdbDir := filepath.Join(workPath, "var/lib/rpm") + + chrootedPaths := []string{ + filepath.Join(workPath, "/usr/local/bin"), + filepath.Join(workPath, "/usr/local/sbin"), + filepath.Join(workPath, "/usr/bin"), + filepath.Join(workPath, "/usr/sbin"), + filepath.Join(workPath, "/bin"), + filepath.Join(workPath, "/sbin"), + } + chrootedPathEnv := strings.Join(chrootedPaths, ":") + + rootfs := llb.Scratch() + if ref := frontend.GetBaseOutputImage(spec, target, ""); ref != "" { + rootfs = llb.Image(ref, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...)) + } + + if len(files) > 0 { + rpmMountDir := "/tmp/rpms" + updated := make([]string, 0, len(files)) + for _, f := range files { + updated = append(updated, filepath.Join(rpmMountDir, f)) + } + + rootfs = builderImg.Run( + w.Install(workPath, updated, true), + llb.AddMount(rpmMountDir, rpmDir, llb.SourcePath("/RPMS")), + dalec.WithConstraints(opts...), + ).AddMount(workPath, rootfs) + } + + manifestCmd := ` +#!/usr/bin/env sh + +# If the rpm command is in the rootfs then we don't need to do anything +# If not then this is a distroless image and we need to generate manifests of the installed rpms and cleanup the rpmdb. + +PATH="` + chrootedPathEnv + `" command -v rpm && exit 0 + +set -e +mkdir -p ` + mfstDir + ` +rpm --dbpath=` + rpmdbDir + ` -qa > ` + mfst1 + ` +rpm --dbpath=` + rpmdbDir + ` -qa --qf "%{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{BUILDTIME}\t%{VENDOR}\t(none)\t%{SIZE}\t%{ARCH}\t%{EPOCHNUM}\t%{SOURCERPM}\n" > ` + mfst2 + ` +rm -rf ` + rpmdbDir + ` +` + + manifestSh := llb.Scratch().File(llb.Mkfile("manifest.sh", 0o755, []byte(manifestCmd)), opts...) + rootfs = builderImg. + Run( + shArgs("/tmp/manifest.sh"), + llb.AddMount("/tmp/manifest.sh", manifestSh, llb.SourcePath("manifest.sh")), + dalec.WithConstraints(opts...), + ).AddMount(workPath, rootfs) + + if post := getImagePostInstall(spec, target); post != nil && len(post.Symlinks) > 0 { + rootfs = builderImg. + Run(dalec.WithConstraints(opts...), addImagePost(post, workPath)). + AddMount(workPath, rootfs) + } + + return rootfs, nil +} + +func getImagePostInstall(spec *dalec.Spec, targetKey string) *dalec.PostInstall { + tgt, ok := spec.Targets[targetKey] + if ok && tgt.Image != nil && tgt.Image.Post != nil { + return tgt.Image.Post + } + + if spec.Image == nil { + return nil + } + return spec.Image.Post +} + +func addImagePost(post *dalec.PostInstall, rootfsPath string) llb.RunOption { + return runOptionFunc(func(ei *llb.ExecInfo) { + if post == nil { + return + } + + if len(post.Symlinks) == 0 { + return + } + + buf := bytes.NewBuffer(nil) + buf.WriteString("set -ex\n") + fmt.Fprintf(buf, "cd %q\n", rootfsPath) + + for src, tgt := range post.Symlinks { + fmt.Fprintf(buf, "ln -s %q %q\n", src, filepath.Join(rootfsPath, tgt.Path)) + } + shArgs(buf.String()).SetRunOption(ei) + dalec.ProgressGroup("Add post-install symlinks").SetRunOption(ei) + }) +} + +type runOptionFunc func(*llb.ExecInfo) + +func (f runOptionFunc) SetRunOption(ei *llb.ExecInfo) { + f(ei) +} diff --git a/frontend/azlinux/handle_depsonly.go b/frontend/azlinux/handle_depsonly.go new file mode 100644 index 000000000..04b271aa4 --- /dev/null +++ b/frontend/azlinux/handle_depsonly.go @@ -0,0 +1,65 @@ +package azlinux + +import ( + "context" + "strings" + + "github.com/Azure/dalec" + "github.com/Azure/dalec/frontend" + "github.com/moby/buildkit/client/llb" + gwclient "github.com/moby/buildkit/frontend/gateway/client" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" +) + +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) + rpmDir := baseImg.Run( + 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, + ). + AddMount("/tmp/rpms", llb.Scratch()) + + files, err := readRPMs(ctx, client, rpmDir) + if err != nil { + 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) + if err != nil { + return nil, nil, err + } + + def, err := st.Marshal(ctx, pg) + if err != nil { + return nil, nil, err + } + + res, err := client.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + return nil, nil, err + } + + base := frontend.GetBaseOutputImage(spec, targetKey, "") + img, err := frontend.BuildImageConfig(ctx, client, spec, targetKey, base) + if err != nil { + return nil, nil, err + } + + ref, err := res.SingleRef() + if err != nil { + return nil, nil, err + } + + return ref, img, nil + }) + } +} diff --git a/frontend/azlinux/handle_rpm.go b/frontend/azlinux/handle_rpm.go new file mode 100644 index 000000000..d167d2797 --- /dev/null +++ b/frontend/azlinux/handle_rpm.go @@ -0,0 +1,79 @@ +package azlinux + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/Azure/dalec" + "github.com/Azure/dalec/frontend" + "github.com/Azure/dalec/frontend/rpm" + "github.com/moby/buildkit/client/llb" + gwclient "github.com/moby/buildkit/frontend/gateway/client" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" +) + +func handleRPM(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) { + if err := rpm.ValidateSpec(spec); err != nil { + return nil, nil, fmt.Errorf("rpm: invalid spec: %w", err) + } + + pg := dalec.ProgressGroup("Building " + targetKey + " rpm: " + spec.Name) + sOpt, err := frontend.SourceOptFromClient(ctx, client) + if err != nil { + return nil, nil, err + } + + st, err := specToRpmLLB(w, client, spec, sOpt, targetKey, pg) + if err != nil { + return nil, nil, err + } + + def, err := st.Marshal(ctx, pg) + if err != nil { + return nil, nil, fmt.Errorf("error marshalling llb: %w", err) + } + + res, err := client.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + return nil, nil, err + } + + ref, err := res.SingleRef() + if err != nil { + return nil, nil, err + } + return ref, nil, nil + }) + } +} + +func shArgs(cmd string) llb.RunOption { + return llb.Args([]string{"sh", "-c", cmd}) +} + +func installBuildDeps(w worker, spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) llb.StateOption { + return func(in llb.State) llb.State { + deps := spec.GetBuildDeps(targetKey) + if len(deps) == 0 { + return in + } + opts = append(opts, dalec.ProgressGroup("Install build deps")) + + return in.Run(w.Install("/", deps, false), dalec.WithConstraints(opts...)).Root() + } +} + +func specToRpmLLB(w worker, client gwclient.Client, spec *dalec.Spec, sOpt dalec.SourceOpts, targetKey string, opts ...llb.ConstraintsOpt) (llb.State, error) { + br, err := rpm.SpecToBuildrootLLB(spec, sOpt, targetKey, opts...) + if err != nil { + return llb.Scratch(), err + } + specPath := filepath.Join("SPECS", spec.Name, spec.Name+".spec") + withDeps := w.Base(client, opts...).With(installBuildDeps(w, spec, targetKey, opts...)) + return rpm.Build(br, withDeps, specPath, opts...), nil +} diff --git a/frontend/azlinux/handler.go b/frontend/azlinux/handler.go new file mode 100644 index 000000000..e74a0f5ea --- /dev/null +++ b/frontend/azlinux/handler.go @@ -0,0 +1,45 @@ +package azlinux + +import ( + "context" + + "github.com/Azure/dalec" + "github.com/Azure/dalec/frontend" + "github.com/Azure/dalec/frontend/rpm" + "github.com/moby/buildkit/client/llb" + gwclient "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/frontend/subrequests/targets" +) + +const ( + tdnfCacheDir = "/var/cache/tdnf" +) + +type worker interface { + Base(client gwclient.Client, opts ...llb.ConstraintsOpt) llb.State + Install(root string, pkgs []string, skipGPG bool) llb.RunOption + DefaultImageConfig(context.Context, gwclient.Client) (*dalec.DockerImageSpec, error) +} + +func newHandler(d worker) gwclient.BuildFunc { + var mux frontend.BuildMux + + mux.Add("rpm", handleRPM(d), &targets.Target{ + Name: "rpm", + Description: "Builds an rpm and src.rpm.", + }) + mux.Add("rpm/debug", rpm.HandleDebug(), nil) + + mux.Add("container", handleContainer(d), &targets.Target{ + Name: "container", + Description: "Builds a container image for", + Default: true, + }) + + mux.Add("container/depsonly", handleDepsOnly(d), &targets.Target{ + Name: "container/depsonly", + Description: "Builds a container image with only the runtime dependencies installed.", + }) + + return mux.Handle +} diff --git a/frontend/azlinux/mariner2.go b/frontend/azlinux/mariner2.go new file mode 100644 index 000000000..e8ba0a989 --- /dev/null +++ b/frontend/azlinux/mariner2.go @@ -0,0 +1,68 @@ +package azlinux + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "github.com/Azure/dalec" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/client/llb/sourceresolver" + gwclient "github.com/moby/buildkit/frontend/gateway/client" +) + +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" +) + +func NewMariner2Handler() gwclient.BuildFunc { + return newHandler(mariner2{}) +} + +type mariner2 struct{} + +func (w mariner2) Base(client gwclient.Client, opts ...llb.ConstraintsOpt) llb.State { + return llb.Image(mariner2Ref, llb.WithMetaResolver(client), dalec.WithConstraints(opts...)).Run( + w.Install("/", []string{"rpm-build", "mariner-rpm-macros", "build-essential"}, false), + ).Root() +} + +func (w mariner2) Install(root string, pkgs []string, skipGPG bool) llb.RunOption { + if root == "" { + root = "/" + } + + var gpgCheckFl string + if skipGPG { + gpgCheckFl = "--nogpgcheck" + } + + cmdArgs := fmt.Sprintf("set -x; tdnf install -y %s --setopt=reposdir=/etc/yum.repos.d --installroot=%s --releasever=2.0 %s", gpgCheckFl, root, strings.Join(pkgs, " ")) + cmd := shArgs(cmdArgs) + + return dalec.WithRunOptions(cmd, w.tdnfCacheMount(root)) +} + +func (mariner2) DefaultImageConfig(ctx context.Context, client gwclient.Client) (*dalec.DockerImageSpec, error) { + _, _, dt, err := client.ResolveImageConfig(ctx, mariner2DistrolessRef, sourceresolver.Opt{}) + 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/imgconfig.go b/frontend/imgconfig.go index b9a525d8e..40058e4db 100644 --- a/frontend/imgconfig.go +++ b/frontend/imgconfig.go @@ -57,7 +57,7 @@ func BuildImageConfig(ctx context.Context, client gwclient.Client, spec *dalec.S } cfg := img.Config - if err := dalec.MergeImageConfig(&cfg, mergeSpecImage(spec, targetKey)); err != nil { + if err := dalec.MergeImageConfig(&cfg, MergeSpecImage(spec, targetKey)); err != nil { return nil, err } @@ -73,7 +73,7 @@ func GetBaseOutputImage(spec *dalec.Spec, target, defaultBase string) string { return i.Base } -func mergeSpecImage(spec *dalec.Spec, targetKey string) *dalec.ImageConfig { +func MergeSpecImage(spec *dalec.Spec, targetKey string) *dalec.ImageConfig { var cfg dalec.ImageConfig if spec.Image != nil { diff --git a/frontend/mariner2/handle_container.go b/frontend/mariner2/handle_container.go deleted file mode 100644 index 6112aa2c5..000000000 --- a/frontend/mariner2/handle_container.go +++ /dev/null @@ -1,193 +0,0 @@ -package mariner2 - -import ( - "bytes" - "context" - "fmt" - "path/filepath" - "strings" - - "github.com/Azure/dalec" - "github.com/Azure/dalec/frontend" - "github.com/moby/buildkit/client/llb" - gwclient "github.com/moby/buildkit/frontend/gateway/client" - ocispecs "github.com/opencontainers/image-spec/specs-go/v1" -) - -const ( - marinerDistrolessRef = "mcr.microsoft.com/cbl-mariner/distroless/base:2.0" -) - -func handleContainer(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 - } - - pg := dalec.ProgressGroup("Build mariner2 container: " + spec.Name) - - rpmDir, err := specToRpmLLB(spec, sOpt, targetKey, pg) - if err != nil { - return nil, nil, fmt.Errorf("error creating rpm: %w", err) - } - - st, err := specToContainerLLB(spec, targetKey, getWorkerImage(client, pg), rpmDir, sOpt, pg) - if err != nil { - return nil, nil, err - } - - def, err := st.Marshal(ctx, pg) - if err != nil { - return nil, nil, fmt.Errorf("error marshalling llb: %w", err) - } - - res, err := client.Solve(ctx, gwclient.SolveRequest{ - Definition: def.ToPB(), - }) - if err != nil { - return nil, nil, err - } - - base := frontend.GetBaseOutputImage(spec, targetKey, marinerDistrolessRef) - img, err := frontend.BuildImageConfig(ctx, client, spec, targetKey, base) - if err != nil { - return nil, nil, err - } - - ref, err := res.SingleRef() - if err != nil { - return nil, nil, err - } - - if err := frontend.RunTests(ctx, client, spec, ref, targetKey); err != nil { - return nil, nil, err - } - - return ref, img, err - }) -} - -func specToContainerLLB(spec *dalec.Spec, target string, builderImg llb.State, rpmDir llb.State, sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) { - opts = append(opts, dalec.ProgressGroup("Install RPMs")) - const workPath = "/tmp/rootfs" - - mfstDir := filepath.Join(workPath, "var/lib/rpmmanifest") - mfst1 := filepath.Join(mfstDir, "container-manifest-1") - mfst2 := filepath.Join(mfstDir, "container-manifest-2") - rpmdbDir := filepath.Join(workPath, "var/lib/rpm") - - chrootedPaths := []string{ - filepath.Join(workPath, "/usr/local/bin"), - filepath.Join(workPath, "/usr/local/sbin"), - filepath.Join(workPath, "/usr/bin"), - filepath.Join(workPath, "/usr/sbin"), - filepath.Join(workPath, "/bin"), - filepath.Join(workPath, "/sbin"), - } - chrootedPathEnv := strings.Join(chrootedPaths, ":") - - installCmd := ` -#!/usr/bin/env sh - -check_non_empty() { - ls ${1} > /dev/null 2>&1 -} - -arch_dir="/tmp/rpms/$(uname -m)" -noarch_dir="/tmp/rpms/noarch" - -rpms="" - -if check_non_empty "${noarch_dir}/*.rpm"; then - rpms="${noarch_dir}/*.rpm" -fi - -if check_non_empty "${arch_dir}/*.rpm"; then - rpms="${rpms} ${arch_dir}/*.rpm" -fi - -if [ -n "${rpms}" ]; then - tdnf -v install --releasever=2.0 -y --nogpgcheck --installroot "` + workPath + `" --setopt=reposdir=/etc/yum.repos.d ${rpms} || exit -fi - -# If the rpm command is in the rootfs then we don't need to do anything -# If not then this is a distroless image and we need to generate manifests of the installed rpms and cleanup the rpmdb. - -PATH="` + chrootedPathEnv + `" command -v rpm && exit 0 - -set -e -mkdir -p ` + mfstDir + ` -rpm --dbpath=` + rpmdbDir + ` -qa > ` + mfst1 + ` -rpm --dbpath=` + rpmdbDir + ` -qa --qf "%{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{BUILDTIME}\t%{VENDOR}\t(none)\t%{SIZE}\t%{ARCH}\t%{EPOCHNUM}\t%{SOURCERPM}\n" > ` + mfst2 + ` -rm -rf ` + rpmdbDir + ` -` - - installer := llb.Scratch().File(llb.Mkfile("install.sh", 0o755, []byte(installCmd)), opts...) - - baseImg := llb.Image(frontend.GetBaseOutputImage(spec, target, marinerDistrolessRef), llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...)) - worker := builderImg. - Run( - shArgs("/tmp/install.sh"), - defaultTdnfCacheMount(), - llb.AddMount("/tmp/rpms", rpmDir, llb.SourcePath("/RPMS")), - llb.AddMount("/tmp/install.sh", installer, llb.SourcePath("install.sh")), - // Mount the tdnf cache into the workpath so that: - // 1. tdnf will use the cache - // 2. Repo data and packages are not left behind in the final image. - tdnfCacheMountWithPrefix(workPath), - dalec.WithConstraints(opts...), - ) - - // This adds a mount to the worker so that all the commands are run with this mount added - // The return value is the state representing the contents of the mounted directory after the commands are run - rootfs := worker.AddMount(workPath, baseImg) - - if post := getImagePostInstall(spec, target); post != nil && len(post.Symlinks) > 0 { - rootfs = worker. - Run(dalec.WithConstraints(opts...), addImagePost(post, workPath)). - AddMount(workPath, rootfs) - } - - return rootfs, nil -} - -func getImagePostInstall(spec *dalec.Spec, targetKey string) *dalec.PostInstall { - tgt, ok := spec.Targets[targetKey] - if ok && tgt.Image != nil && tgt.Image.Post != nil { - return tgt.Image.Post - } - - if spec.Image == nil { - return nil - } - return spec.Image.Post -} - -func addImagePost(post *dalec.PostInstall, rootfsPath string) llb.RunOption { - return runOptionFunc(func(ei *llb.ExecInfo) { - if post == nil { - return - } - - if len(post.Symlinks) == 0 { - return - } - - buf := bytes.NewBuffer(nil) - buf.WriteString("set -ex\n") - fmt.Fprintf(buf, "cd %q\n", rootfsPath) - - for src, tgt := range post.Symlinks { - fmt.Fprintf(buf, "ln -s %q %q\n", src, filepath.Join(rootfsPath, tgt.Path)) - } - shArgs(buf.String()).SetRunOption(ei) - dalec.ProgressGroup("Add post-install symlinks").SetRunOption(ei) - }) -} - -type runOptionFunc func(*llb.ExecInfo) - -func (f runOptionFunc) SetRunOption(ei *llb.ExecInfo) { - f(ei) -} diff --git a/frontend/mariner2/handle_depsonly.go b/frontend/mariner2/handle_depsonly.go deleted file mode 100644 index 947a6d460..000000000 --- a/frontend/mariner2/handle_depsonly.go +++ /dev/null @@ -1,59 +0,0 @@ -package mariner2 - -import ( - "context" - "strings" - - "github.com/Azure/dalec" - "github.com/Azure/dalec/frontend" - "github.com/moby/buildkit/client/llb" - gwclient "github.com/moby/buildkit/frontend/gateway/client" - ocispecs "github.com/opencontainers/image-spec/specs-go/v1" -) - -func handleDepsOnly(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 := getWorkerImage(client, pg) - rpmDir := baseImg.Run( - 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), " ")), - defaultTdnfCacheMount(), - pg, - ). - AddMount("/tmp/rpms", llb.Scratch()) - - sOpt, err := frontend.SourceOptFromClient(ctx, client) - if err != nil { - return nil, nil, err - } - st, err := specToContainerLLB(spec, targetKey, baseImg, rpmDir, sOpt, pg) - if err != nil { - return nil, nil, err - } - - def, err := st.Marshal(ctx, pg) - if err != nil { - return nil, nil, err - } - - res, err := client.Solve(ctx, gwclient.SolveRequest{ - Definition: def.ToPB(), - }) - if err != nil { - return nil, nil, err - } - - base := frontend.GetBaseOutputImage(spec, targetKey, marinerDistrolessRef) - img, err := frontend.BuildImageConfig(ctx, client, spec, targetKey, base) - if err != nil { - return nil, nil, err - } - - ref, err := res.SingleRef() - if err != nil { - return nil, nil, err - } - - return ref, img, nil - }) -} diff --git a/frontend/mariner2/handle_rpm.go b/frontend/mariner2/handle_rpm.go deleted file mode 100644 index 7de82a353..000000000 --- a/frontend/mariner2/handle_rpm.go +++ /dev/null @@ -1,113 +0,0 @@ -package mariner2 - -import ( - "context" - "fmt" - "path/filepath" - "strings" - - "github.com/Azure/dalec" - "github.com/Azure/dalec/frontend" - "github.com/Azure/dalec/frontend/rpm" - "github.com/moby/buildkit/client/llb" - gwclient "github.com/moby/buildkit/frontend/gateway/client" - ocispecs "github.com/opencontainers/image-spec/specs-go/v1" -) - -const ( - marinerRef = "mcr.microsoft.com/cbl-mariner/base/core:2.0" - - tdnfCacheDir = "/var/cache/tdnf" - tdnfCacheName = "mariner2-tdnf-cache" -) - -func defaultTdnfCacheMount() llb.RunOption { - return tdnfCacheMountWithPrefix("") -} - -func tdnfCacheMountWithPrefix(prefix string) llb.RunOption { - // note: We are using CacheMountLocked here because without it, while there are concurrent builds happening, tdnf exits with a lock error. - // dnf is supposed to wait for locks, but it seems like that is not happening with tdnf. - return llb.AddMount(filepath.Join(prefix, tdnfCacheDir), llb.Scratch(), llb.AsPersistentCacheDir(tdnfCacheName, llb.CacheMountLocked)) -} - -func handleRPM(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) { - if err := rpm.ValidateSpec(spec); err != nil { - return nil, nil, fmt.Errorf("rpm: invalid spec: %w", err) - } - - pg := dalec.ProgressGroup("Building mariner2 rpm: " + spec.Name) - sOpt, err := frontend.SourceOptFromClient(ctx, client) - if err != nil { - return nil, nil, err - } - - st, err := specToRpmLLB(spec, sOpt, targetKey, pg) - if err != nil { - return nil, nil, err - } - - def, err := st.Marshal(ctx, pg) - if err != nil { - return nil, nil, fmt.Errorf("error marshalling llb: %w", err) - } - - res, err := client.Solve(ctx, gwclient.SolveRequest{ - Definition: def.ToPB(), - }) - if err != nil { - return nil, nil, err - } - - ref, err := res.SingleRef() - if err != nil { - return nil, nil, err - } - return ref, nil, nil - }) -} - -func shArgs(cmd string) llb.RunOption { - return llb.Args([]string{"sh", "-c", cmd}) -} - -func getWorkerImage(resolver llb.ImageMetaResolver, opts ...llb.ConstraintsOpt) llb.State { - opts = append(opts, dalec.ProgressGroup("Prepare worker image")) - return llb.Image(marinerRef, llb.WithMetaResolver(resolver), dalec.WithConstraints(opts...)). - Run( - shArgs("tdnf install -y rpm-build mariner-rpm-macros build-essential"), - defaultTdnfCacheMount(), - dalec.WithConstraints(opts...), - ). - Root() -} - -func installBuildDeps(spec *dalec.Spec, targetKey string, opts ...llb.ConstraintsOpt) llb.StateOption { - return func(in llb.State) llb.State { - deps := spec.GetBuildDeps(targetKey) - if len(deps) == 0 { - return in - } - opts = append(opts, dalec.ProgressGroup("Install build deps")) - - return in. - Run( - shArgs(fmt.Sprintf("tdnf install --releasever=2.0 -y %s", strings.Join(deps, " "))), - defaultTdnfCacheMount(), - dalec.WithConstraints(opts...), - ). - Root() - } -} - -func specToRpmLLB(spec *dalec.Spec, sOpt dalec.SourceOpts, targetKey string, opts ...llb.ConstraintsOpt) (llb.State, error) { - br, err := rpm.SpecToBuildrootLLB(spec, sOpt, targetKey, opts...) - if err != nil { - return llb.Scratch(), err - } - specPath := filepath.Join("SPECS", spec.Name, spec.Name+".spec") - - base := getWorkerImage(sOpt.Resolver, opts...).With(installBuildDeps(spec, targetKey, opts...)) - return rpm.Build(br, base, specPath, opts...), nil -} diff --git a/frontend/mariner2/handler.go b/frontend/mariner2/handler.go deleted file mode 100644 index 805bc8165..000000000 --- a/frontend/mariner2/handler.go +++ /dev/null @@ -1,37 +0,0 @@ -package mariner2 - -import ( - "context" - - "github.com/Azure/dalec/frontend" - "github.com/Azure/dalec/frontend/rpm" - gwclient "github.com/moby/buildkit/frontend/gateway/client" - bktargets "github.com/moby/buildkit/frontend/subrequests/targets" -) - -const ( - DefaultTargetKey = "mariner2" -) - -func Handle(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { - var mux frontend.BuildMux - - mux.Add("rpm", handleRPM, &bktargets.Target{ - Name: "rpm", - Description: "Builds an rpm and src.rpm for mariner2.", - }) - mux.Add("rpm/debug", rpm.HandleDebug(), nil) - - mux.Add("container", handleContainer, &bktargets.Target{ - Name: "container", - Description: "Builds a container image for mariner2.", - Default: true, - }) - - mux.Add("container/depsonly", handleDepsOnly, &bktargets.Target{ - Name: "container/depsonly", - Description: "Builds a container image with only the runtime dependencies installed.", - }) - - return mux.Handle(ctx, client) -} diff --git a/frontend/rpm/handle_sources.go b/frontend/rpm/handle_sources.go index 778f1292e..d47bc22fa 100644 --- a/frontend/rpm/handle_sources.go +++ b/frontend/rpm/handle_sources.go @@ -17,10 +17,6 @@ import ( // Currently this image needs /bin/sh and tar in $PATH var TarImageRef = "busybox:latest" -func shArgs(cmd string) llb.RunOption { - return llb.Args([]string{"sh", "-c", cmd}) -} - func tar(src llb.State, dest string, opts ...llb.ConstraintsOpt) llb.State { tarImg := llb.Image(TarImageRef, dalec.WithConstraints(opts...)) diff --git a/frontend/rpm/handle_rpm.go b/frontend/rpm/rpmbuild.go similarity index 96% rename from frontend/rpm/handle_rpm.go rename to frontend/rpm/rpmbuild.go index c85f21e72..92cbbcf32 100644 --- a/frontend/rpm/handle_rpm.go +++ b/frontend/rpm/rpmbuild.go @@ -61,3 +61,7 @@ func ValidateSpec(spec *dalec.Spec) (out error) { } return out } + +func shArgs(cmd string) llb.RunOption { + return llb.Args([]string{"sh", "-c", cmd}) +}