From 0772f0bc979d5f5604371bee8764216167ff8565 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 31 Oct 2024 12:51:13 -0700 Subject: [PATCH] windows: Add pinned deps and custom repo support This is mostly copied from the jammy implementation since the windows target is currently using jammy as the build worker. Signed-off-by: Brian Goff --- frontend/windows/handle_zip.go | 156 ++++++++++++++++++---- helpers.go | 8 +- test/azlinux_test.go | 236 +++------------------------------ test/custom_repo_test.go | 218 ++++++++++++++++++++++++++++++ test/helpers_test.go | 12 ++ test/jammy_test.go | 2 +- test/windows_test.go | 116 ++++++++++------ 7 files changed, 463 insertions(+), 285 deletions(-) create mode 100644 test/custom_repo_test.go diff --git a/frontend/windows/handle_zip.go b/frontend/windows/handle_zip.go index b6c5c16ee..9048a09ba 100644 --- a/frontend/windows/handle_zip.go +++ b/frontend/windows/handle_zip.go @@ -9,12 +9,12 @@ import ( "github.com/Azure/dalec" "github.com/Azure/dalec/frontend" + "github.com/Azure/dalec/frontend/deb" "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "golang.org/x/exp/maps" - "golang.org/x/exp/slices" ) const ( @@ -38,7 +38,7 @@ func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, e return nil, nil, err } - bin, err := buildBinaries(ctx, spec, worker, client, sOpt, targetKey) + bin, err := buildBinaries(ctx, spec, worker, client, sOpt, targetKey, pg) if err != nil { return nil, nil, fmt.Errorf("unable to build binaries: %w", err) } @@ -82,22 +82,94 @@ func specToSourcesLLB(worker llb.State, spec *dalec.Spec, sOpt dalec.SourceOpts, return out, nil } -func installBuildDeps(deps []string) llb.StateOption { - return func(s llb.State) llb.State { +func installBuildDeps(sOpt dalec.SourceOpts, spec *dalec.Spec, targetKey string) llb.StateOption { + + return func(in llb.State) llb.State { + deps := spec.GetBuildDeps(targetKey) if len(deps) == 0 { - return s + return in } - sorted := slices.Clone(deps) - slices.Sort(sorted) - - return s.Run( - dalec.ShArgs("apt-get update && apt-get install -y "+strings.Join(sorted, " ")), - dalec.WithMountedAptCache(aptCachePrefix), - ).Root() + return in.Async(func(ctx context.Context, in llb.State, c *llb.Constraints) (llb.State, error) { + depsSpec := &dalec.Spec{ + Name: spec.Name + "-deps", + Packager: "Dalec", + Version: spec.Version, + Revision: spec.Revision, + Dependencies: &dalec.PackageDependencies{ + Runtime: deps, + }, + Description: "Build dependencies for " + spec.Name, + } + + opts := []llb.ConstraintsOpt{dalec.WithConstraint(c)} + + srcPkg, err := deb.SourcePackage(sOpt, in, depsSpec, targetKey, "", opts...) + if err != nil { + return in, err + } + + pg := dalec.ProgressGroup("Install build dependencies") + opts = append(opts, pg) + + pkg, err := deb.BuildDeb(in, depsSpec, srcPkg, "", append(opts, dalec.ProgressGroup("Create intermediate deb for build dependnencies"))...) + if err != nil { + return in, errors.Wrap(err, "error creating intermediate package for installing build dependencies") + } + + customRepoOpts, err := customRepoMounts(in, spec.GetBuildRepos(targetKey), sOpt, opts...) + if err != nil { + return in, err + } + + const ( + debPath = "/tmp/dalec/internal/build/deps" + ) + + return in.Run( + installWithConstraints(debPath+"/*.deb", depsSpec.Name, opts...), + llb.AddMount(debPath, pkg, llb.Readonly), + customRepoOpts, + dalec.WithConstraints(opts...), + ).Root(), nil + }) } } +func installWithConstraints(pkgPath string, pkgName string, opts ...llb.ConstraintsOpt) llb.RunOption { + return dalec.RunOptFunc(func(ei *llb.ExecInfo) { + // The apt solver always tries to select the latest package version even when constraints specify that an older version should be installed and that older version is available in a repo. + // This leads the solver to simply refuse to install our target package if the latest version of ANY dependency package is incompatible with the constraints. + // To work around this we first install the .deb for the package with dpkg, specifically ignoring any dependencies so that we can avoid the constraints issue. + // We then use aptitude to fix the (possibly broken) install of the package, and we pass the aptitude solver a hint to REJECT any solution that involves uninstalling the package. + // This forces aptitude to find a solution that will respect the constraints even if the solution involves pinning dependency packages to older versions. + script := llb.Scratch().File( + llb.Mkfile("install.sh", 0o755, []byte(`#!/usr/bin/env sh +# Make sure any cached data from local repos is purged since this should not +# be shared between builds. +rm -f /var/lib/apt/lists/_* +apt autoclean -y + +dpkg -i --force-depends `+pkgPath+` + +apt update + + +set +e +aptitude install -y -f -o "Aptitude::ProblemResolver::Hints::=reject `+pkgName+` :UNINST" && exit +ls -lh /etc/apt/sources.list.d +exit 42 +`), + ), opts...) + + dalec.WithMountedAptCache(aptCachePrefix).SetRunOption(ei) + + p := "/tmp/dalec/internal/deb/install-with-constraints.sh" + llb.AddMount(p, script, llb.SourcePath("install.sh")).SetRunOption(ei) + dalec.ShArgs(p).SetRunOption(ei) + }) +} + func withSourcesMounted(dst string, states map[string]llb.State, sources map[string]dalec.Source) llb.RunOption { opts := make([]llb.RunOption, 0, len(states)) @@ -127,19 +199,16 @@ func withSourcesMounted(dst string, states map[string]llb.State, sources map[str return dalec.WithRunOptions(ordered...) } -func buildBinaries(ctx context.Context, spec *dalec.Spec, worker llb.State, client gwclient.Client, sOpt dalec.SourceOpts, targetKey string) (llb.State, error) { - deps := dalec.SortMapKeys(spec.GetBuildDeps(targetKey)) - - // note: we do not yet support pinning build dependencies for windows workers - worker = worker.With(installBuildDeps(deps)) +func buildBinaries(ctx context.Context, spec *dalec.Spec, worker llb.State, client gwclient.Client, sOpt dalec.SourceOpts, targetKey string, opts ...llb.ConstraintsOpt) (llb.State, error) { + worker = worker.With(installBuildDeps(sOpt, spec, targetKey)) - sources, err := specToSourcesLLB(worker, spec, sOpt) + sources, err := specToSourcesLLB(worker, spec, sOpt, opts...) if err != nil { return llb.Scratch(), errors.Wrap(err, "could not generate sources") } - patched := dalec.PatchSources(worker, spec, sources) - buildScript := createBuildScript(spec) + patched := dalec.PatchSources(worker, spec, sources, opts...) + buildScript := createBuildScript(spec, opts...) binaries := maps.Keys(spec.Artifacts.Binaries) script := generateInvocationScript(binaries) @@ -149,6 +218,7 @@ func buildBinaries(ctx context.Context, spec *dalec.Spec, worker llb.State, clie llb.Dir("/build"), withSourcesMounted("/build", patched, spec.Sources), llb.AddMount("/tmp/scripts", buildScript), + dalec.WithConstraints(opts...), ).AddMount(outputDir, llb.Scratch()) return frontend.MaybeSign(ctx, client, st, spec, targetKey, sOpt) @@ -193,14 +263,22 @@ func workerImg(sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, er 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(), 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 aptitude dpkg-dev debhelper-compat="+deb.DebHelperCompat), + dalec.WithMountedAptCache(aptCachePrefix), + ). + // This file prevents installation of things like docs in ubuntu + // containers We don't want to exclude this because tests want to + // check things for docs in the build container. But we also don't + // want to remove this completely from the base worker image in the + // frontend because we usually don't want such things in the build + // environment. This is only needed because certain tests (which + // are using this customized builder image) are checking for files + // that are being excluded by this config file. + File(llb.Rm("/etc/dpkg/dpkg.cfg.d/excludes", llb.WithAllowNotFound(true))), nil } -func createBuildScript(spec *dalec.Spec) llb.State { +func createBuildScript(spec *dalec.Spec, opts ...llb.ConstraintsOpt) llb.State { buf := bytes.NewBuffer(nil) fmt.Fprintln(buf, "#!/usr/bin/env sh") @@ -229,5 +307,29 @@ func createBuildScript(spec *dalec.Spec) llb.State { } return llb.Scratch(). - File(llb.Mkfile(buildScriptName, 0o770, buf.Bytes())) + File(llb.Mkfile(buildScriptName, 0o770, buf.Bytes()), opts...) +} + +var jammyRepoPlatformCfg = dalec.RepoPlatformConfig{ + ConfigRoot: "/etc/apt/sources.list.d", + GPGKeyRoot: "/usr/share/keyrings", +} + +func customRepoMounts(worker llb.State, repos []dalec.PackageRepositoryConfig, sOpt dalec.SourceOpts, opts ...llb.ConstraintsOpt) (llb.RunOption, error) { + withRepos, err := dalec.WithRepoConfigs(repos, &jammyRepoPlatformCfg, sOpt, opts...) + if err != nil { + return nil, err + } + + withData, err := dalec.WithRepoData(repos, sOpt, opts...) + if err != nil { + return nil, err + } + + keyMounts, _, err := dalec.GetRepoKeys(worker, repos, &jammyRepoPlatformCfg, sOpt, opts...) + if err != nil { + return nil, err + } + + return dalec.WithRunOptions(withRepos, withData, keyMounts), nil } diff --git a/helpers.go b/helpers.go index 73a1915af..631529c43 100644 --- a/helpers.go +++ b/helpers.go @@ -134,6 +134,12 @@ func WithConstraints(ls ...llb.ConstraintsOpt) llb.ConstraintsOpt { }) } +func WithConstraint(in *llb.Constraints) llb.ConstraintsOpt { + return constraintsOptFunc(func(c *llb.Constraints) { + *c = *in + }) +} + func withConstraints(opts []llb.ConstraintsOpt) llb.ConstraintsOpt { return WithConstraints(opts...) } @@ -542,7 +548,7 @@ func GetRepoKeys(worker llb.State, configs []PackageRepositoryConfig, cfg *RepoP outPath := filepath.Join("/tmp/out", name) keySt := worker.Run( // dearmor key if necessary - ShArgs(fmt.Sprintf("cat '%s' | gpg --dearmor --output '%s'", inPath, outPath)), + ShArgs(fmt.Sprintf("gpg --dearmor --output %q < %q", outPath, inPath)), llb.AddMount(inPath, gpgKey, llb.SourcePath(name))). AddMount("/tmp/out/", llb.Scratch()) diff --git a/test/azlinux_test.go b/test/azlinux_test.go index 576f7667f..7a0b33147 100644 --- a/test/azlinux_test.go +++ b/test/azlinux_test.go @@ -16,6 +16,7 @@ import ( "github.com/moby/buildkit/exporter/containerimage/exptypes" gwclient "github.com/moby/buildkit/frontend/gateway/client" moby_buildkit_v1_frontend "github.com/moby/buildkit/frontend/gateway/pb" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" ) @@ -111,6 +112,22 @@ func TestAzlinux3(t *testing.T) { }) } +func signRepoAzLinux(gpgKey llb.State) llb.StateOption { + // key should be a state that has a public key under /public.key + return func(in llb.State) llb.State { + return in.Run( + dalec.ShArgs("gpg --import < /tmp/gpg/private.key"), + llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly), + dalec.ProgressGroup("Importing gpg key")). + Run( + dalec.ShArgs(`ID=$(gpg --list-keys --keyid-format LONG | grep -B 2 'test@example.com' | grep 'pub' | awk '{print $2}' | cut -d'/' -f2) && \ + gpg --list-keys --keyid-format LONG && \ + gpg --detach-sign --default-key "$ID" --armor --yes /opt/repo/repodata/repomd.xml`), + llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly), + ).Root() + } +} + func azlinuxWithRepo(rpms llb.State, opts ...llb.StateOption) llb.StateOption { return func(in llb.State) llb.State { localRepo := []byte(` @@ -154,6 +171,7 @@ type workerConfig struct { ContextName string TestRepoConfig map[string]dalec.Source Constraints constraintsSymbols + Platform *ocispecs.Platform } type constraintsSymbols struct { @@ -1350,7 +1368,7 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot t.Parallel() ctx := startTestSpan(baseCtx, t) - testCustomRepo(ctx, t, testConfig) + testCustomRepo(ctx, t, testConfig.Worker, testConfig.Target) }) t.Run("test library artifacts", func(t *testing.T) { @@ -1473,220 +1491,6 @@ func testCustomLinuxWorker(ctx context.Context, t *testing.T, targetCfg targetCo }) } -func signRepoAzLinux(gpgKey llb.State) llb.StateOption { - // key should be a state that has a public key under /public.key - return func(in llb.State) llb.State { - return in.Run( - dalec.ShArgs("gpg --import < /tmp/gpg/PUBLIC-RPM-GPG-KEY"), - dalec.ShArgs("gpg --import < /tmp/gpg/private.key"), - llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly), - dalec.ProgressGroup("Importing gpg key")). - Run( - dalec.ShArgs(`ID=$(gpg --list-keys --keyid-format LONG | grep -B 2 'test@example.com' | grep 'pub' | awk '{print $2}' | cut -d'/' -f2) && \ - gpg --list-keys --keyid-format LONG && \ - gpg --detach-sign --default-key "$ID" --armor --yes /opt/repo/repodata/repomd.xml`), - llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly), - ).Root() - } -} - -func generateGPGKey(worker llb.State) llb.State { - pg := dalec.ProgressGroup("Generate GPG Key for Testing") - - st := worker. - Run(dalec.ShArgs(`gpg --batch --gen-key < /tmp/gpg/public.key; gpg --export-secret-keys --armor test@example.com > /tmp/gpg/private.key"), pg). - AddMount("/tmp/gpg", llb.Scratch()) - - return st -} - -func testCustomRepo(ctx context.Context, t *testing.T, cfg testLinuxConfig) { - // provide a unique suffix per test otherwise, depending on the test case, - // you can end up with a false positive result due to apt package caching. - // e.g. there may not be a public key for the repo under test, but if the - // package is already in the package cache (due to other tests that injected - // a public key) then apt may use that package anyway. - getDepSpec := func(suffix string) *dalec.Spec { - return &dalec.Spec{ - Name: "dalec-test-package" + suffix, - Version: "0.0.1", - Revision: "1", - Description: "A basic package for various testing uses", - License: "MIT", - Sources: map[string]dalec.Source{ - "version.txt": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: "version: " + "0.0.1", - }, - }, - }, - }, - - Artifacts: dalec.Artifacts{ - Docs: map[string]dalec.ArtifactConfig{ - "version.txt": {}, - }, - }, - } - } - - getSpec := func(dep *dalec.Spec, keyConfig map[string]dalec.Source) *dalec.Spec { - return &dalec.Spec{ - Name: "dalec-test-custom-repo", - Version: "0.0.1", - Revision: "1", - Description: "Testing allowing a custom repo to be provided", - License: "MIT", - Dependencies: &dalec.PackageDependencies{ - Build: map[string]dalec.PackageConstraints{ - dep.Name: {}, - }, - Runtime: map[string]dalec.PackageConstraints{ - dep.Name: {}, - }, - - Test: []string{ - dep.Name, - "bash", - "coreutils", - }, - - ExtraRepos: []dalec.PackageRepositoryConfig{ - { - Config: cfg.Worker.TestRepoConfig, - Data: []dalec.SourceMount{ - { - Dest: "/opt/repo", - Spec: dalec.Source{ - Context: &dalec.SourceContext{ - Name: "test-repo", - }, - }, - }, - }, - Keys: keyConfig, - Envs: []string{"build", "install", "test"}, - }, - }, - }, - Build: dalec.ArtifactBuild{ - Steps: []dalec.BuildStep{ - { - Command: `set -x; [ "$(cat /usr/share/doc/` + dep.Name + `/version.txt)" = "version: 0.0.1" ]`, - }, - }, - }, - - Tests: []*dalec.TestSpec{ - { - Name: "Check test dependency installed from custom repo", - // Dummy command here to force test steps to run and install test stage dependency - // from custom repo - Steps: []dalec.TestStep{ - { - Command: "ls -lrt", - }, - }, - }, - }, - } - - } - - getRepoState := func(ctx context.Context, t *testing.T, client gwclient.Client, w llb.State, key llb.State, depSpec *dalec.Spec) llb.State { - sr := newSolveRequest(withSpec(ctx, t, depSpec), withBuildTarget(cfg.Target.Package)) - pkg := reqToState(ctx, client, sr, t) - - // create a repo using our existing worker - workerWithRepo := w.With(cfg.Worker.CreateRepo(pkg, cfg.Worker.SignRepo(key))) - - // copy out just the contents of the repo - return llb.Scratch().File(llb.Copy(workerWithRepo, "/opt/repo", "/", &llb.CopyInfo{CopyDirContentsOnly: true})) - } - - t.Run("no public key", func(t *testing.T) { - t.Parallel() - - testNoPublicKey := func(ctx context.Context, gwc gwclient.Client) { - sr := newSolveRequest(withBuildTarget(cfg.Target.Worker), withSpec(ctx, t, nil)) - w := reqToState(ctx, gwc, sr, t) - - // generate a gpg public/private key pair - gpgKey := generateGPGKey(w) - - depSpec := getDepSpec("no-public-key") - repoState := getRepoState(ctx, t, gwc, w, gpgKey, depSpec) - - sr = newSolveRequest( - withSpec(ctx, t, getSpec(depSpec, nil)), - withBuildContext(ctx, t, "test-repo", repoState), - withBuildTarget(cfg.Target.Container), - ) - - _, err := gwc.Solve(ctx, sr) - if err == nil { - t.Fatal("expected solve to fail") - } - } - - testEnv.RunTest(ctx, t, testNoPublicKey) - }) - - t.Run("with public key", func(t *testing.T) { - t.Parallel() - - testWithPublicKey := func(ctx context.Context, gwc gwclient.Client) { - sr := newSolveRequest(withBuildTarget(cfg.Target.Worker), withSpec(ctx, t, nil)) - w := reqToState(ctx, gwc, sr, t) - - // generate a gpg key to sign the repo - // under /public.key - gpgKey := generateGPGKey(w) - depSpec := getDepSpec("with-public-key") - repoState := getRepoState(ctx, t, gwc, w, gpgKey, depSpec) - - spec := getSpec(depSpec, map[string]dalec.Source{ - // in the dalec spec, the public key will be passed in via build context - "public.key": { - Context: &dalec.SourceContext{ - Name: "repo-public-key", - }, - Path: "public.key", - }, - }) - - sr = newSolveRequest( - withSpec(ctx, t, spec), - withBuildContext(ctx, t, "test-repo", repoState), - withBuildContext(ctx, t, "repo-public-key", gpgKey), - withBuildTarget(cfg.Target.Container), - ) - - res := solveT(ctx, t, gwc, sr) - _, err := res.SingleRef() - if err != nil { - t.Fatal(err) - } - } - - testEnv.RunTest(ctx, t, testWithPublicKey) - }) -} - func testPinnedBuildDeps(ctx context.Context, t *testing.T, cfg testLinuxConfig) { pkgName := "dalec-test-package-pinned" @@ -1806,7 +1610,7 @@ func testPinnedBuildDeps(ctx context.Context, t *testing.T, cfg testLinuxConfig) testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { worker := getWorker(ctx, t, gwc) - sr := newSolveRequest(withSpec(ctx, t, spec), withBuildContext(ctx, t, cfg.Worker.ContextName, worker), withBuildTarget(cfg.Target.Container)) + sr := newSolveRequest(withSpec(ctx, t, spec), withBuildContext(ctx, t, cfg.Worker.ContextName, worker), withBuildTarget(cfg.Target.Container), withPlatformPtr(cfg.Worker.Platform)) res := solveT(ctx, t, gwc, sr) _, err := res.SingleRef() if err != nil { diff --git a/test/custom_repo_test.go b/test/custom_repo_test.go new file mode 100644 index 000000000..370fbdd60 --- /dev/null +++ b/test/custom_repo_test.go @@ -0,0 +1,218 @@ +package test + +import ( + "context" + "testing" + + "github.com/Azure/dalec" + "github.com/moby/buildkit/client/llb" + gwclient "github.com/moby/buildkit/frontend/gateway/client" +) + +func testCustomRepo(ctx context.Context, t *testing.T, workerCfg workerConfig, targetCfg targetConfig) { + // provide a unique suffix per test otherwise, depending on the test case, + // you can end up with a false positive result due to apt package caching. + // e.g. there may not be a public key for the repo under test, but if the + // package is already in the package cache (due to other tests that injected + // a public key) then apt may use that package anyway. + getDepSpec := func(suffix string) *dalec.Spec { + return &dalec.Spec{ + Name: "dalec-test-package" + suffix, + Version: "0.0.1", + Revision: "1", + Description: "A basic package for various testing uses", + License: "MIT", + Sources: map[string]dalec.Source{ + "version.txt": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "version: " + "0.0.1", + }, + }, + }, + }, + + Artifacts: dalec.Artifacts{ + Docs: map[string]dalec.ArtifactConfig{ + "version.txt": {}, + }, + }, + } + } + + getSpec := func(dep *dalec.Spec, keyConfig map[string]dalec.Source) *dalec.Spec { + spec := &dalec.Spec{ + Name: "dalec-test-custom-repo", + Version: "0.0.1", + Revision: "1", + Description: "Testing allowing a custom repo to be provided", + License: "MIT", + Dependencies: &dalec.PackageDependencies{ + Build: map[string]dalec.PackageConstraints{ + dep.Name: {}, + }, + Runtime: map[string]dalec.PackageConstraints{ + dep.Name: {}, + }, + + Test: []string{ + dep.Name, + "bash", + "coreutils", + }, + + ExtraRepos: []dalec.PackageRepositoryConfig{ + { + Config: workerCfg.TestRepoConfig, + Data: []dalec.SourceMount{ + { + Dest: "/opt/repo", + Spec: dalec.Source{ + Context: &dalec.SourceContext{ + Name: "test-repo", + }, + }, + }, + }, + Keys: keyConfig, + Envs: []string{"build", "install", "test"}, + }, + }, + }, + + Build: dalec.ArtifactBuild{ + Steps: []dalec.BuildStep{ + { + Command: `set -x; [ "$(cat /usr/share/doc/` + dep.Name + `/version.txt)" = "version: 0.0.1" ]`, + }, + }, + }, + + Tests: []*dalec.TestSpec{ + { + Name: "Check test dependency installed from custom repo", + // Dummy command here to force test steps to run and install test stage dependency + // from custom repo + Steps: []dalec.TestStep{ + { + Command: "ls -lrt", + }, + }, + }, + }, + } + + if workerCfg.Platform != nil && workerCfg.Platform.OS == "windows" { + spec.Dependencies.Runtime = nil + spec.Dependencies.Test = nil + spec.Tests = nil + } + return spec + } + + getRepoState := func(ctx context.Context, t *testing.T, client gwclient.Client, w llb.State, key llb.State, depSpec *dalec.Spec) llb.State { + sr := newSolveRequest(withSpec(ctx, t, depSpec), withBuildTarget(targetCfg.Package)) + pkg := reqToState(ctx, client, sr, t) + + // create a repo using our existing worker + workerWithRepo := w.With(workerCfg.CreateRepo(pkg, workerCfg.SignRepo(key))) + + // copy out just the contents of the repo + return llb.Scratch().File(llb.Copy(workerWithRepo, "/opt/repo", "/", &llb.CopyInfo{CopyDirContentsOnly: true})) + } + + t.Run("no public key", func(t *testing.T) { + t.Parallel() + ctx := startTestSpan(baseCtx, t) + + testNoPublicKey := func(ctx context.Context, gwc gwclient.Client) { + sr := newSolveRequest(withBuildTarget(targetCfg.Worker), withSpec(ctx, t, nil)) + w := reqToState(ctx, gwc, sr, t) + + // generate a gpg public/private key pair + gpgKey := generateGPGKey(w) + + depSpec := getDepSpec("no-public-key") + repoState := getRepoState(ctx, t, gwc, w, gpgKey, depSpec) + + sr = newSolveRequest( + withSpec(ctx, t, getSpec(depSpec, nil)), + withBuildContext(ctx, t, "test-repo", repoState), + withBuildTarget(targetCfg.Container), + withPlatformPtr(workerCfg.Platform), + ) + + _, err := gwc.Solve(ctx, sr) + if err == nil { + t.Fatal("expected solve to fail") + } + } + + testEnv.RunTest(ctx, t, testNoPublicKey) + }) + + t.Run("with public key", func(t *testing.T) { + t.Parallel() + ctx := startTestSpan(baseCtx, t) + + testWithPublicKey := func(ctx context.Context, gwc gwclient.Client) { + sr := newSolveRequest(withBuildTarget(targetCfg.Worker), withSpec(ctx, t, nil)) + w := reqToState(ctx, gwc, sr, t) + + // generate a gpg key to sign the repo + // under /public.key + gpgKey := generateGPGKey(w) + depSpec := getDepSpec("with-public-key") + repoState := getRepoState(ctx, t, gwc, w, gpgKey, depSpec) + + spec := getSpec(depSpec, map[string]dalec.Source{ + // in the dalec spec, the public key will be passed in via build context + "public.key": { + Context: &dalec.SourceContext{ + Name: "repo-public-key", + }, + Path: "public.key", + }, + }) + + sr = newSolveRequest( + withSpec(ctx, t, spec), + withBuildContext(ctx, t, "test-repo", repoState), + withBuildContext(ctx, t, "repo-public-key", gpgKey), + withBuildTarget(targetCfg.Container), + withPlatformPtr(workerCfg.Platform), + ) + + res := solveT(ctx, t, gwc, sr) + _, err := res.SingleRef() + if err != nil { + t.Fatal(err) + } + } + + testEnv.RunTest(ctx, t, testWithPublicKey) + }) +} + +func generateGPGKey(worker llb.State) llb.State { + pg := dalec.ProgressGroup("Generate GPG Key for Testing") + + st := worker. + Run(dalec.ShArgs(`gpg --batch --gen-key < /tmp/gpg/public.key; gpg --export-secret-keys --armor test@example.com > /tmp/gpg/private.key"), pg). + AddMount("/tmp/gpg", llb.Scratch()) + + return st +} diff --git a/test/helpers_test.go b/test/helpers_test.go index fb46462ac..2d2cd2e5d 100644 --- a/test/helpers_test.go +++ b/test/helpers_test.go @@ -199,10 +199,22 @@ func newSolveRequest(opts ...srOpt) gwclient.SolveRequest { func withPlatform(platform ocispecs.Platform) srOpt { return func(cfg *newSolveRequestConfig) { + if cfg.req.FrontendOpt == nil { + cfg.req.FrontendOpt = make(map[string]string) + } cfg.req.FrontendOpt["platform"] = platforms.Format(platform) } } +func withPlatformPtr(p *ocispecs.Platform) srOpt { + return func(cfg *newSolveRequestConfig) { + if p == nil { + return + } + withPlatform(*p)(cfg) + } +} + func withBuildArg(k, v string) srOpt { return func(cfg *newSolveRequestConfig) { cfg.req.FrontendOpt["build-arg:"+k] = v diff --git a/test/jammy_test.go b/test/jammy_test.go index 8bde7893d..6565f4218 100644 --- a/test/jammy_test.go +++ b/test/jammy_test.go @@ -69,7 +69,7 @@ deb [trusted=yes] copy:/opt/repo/ / `) return func(in llb.State) llb.State { withRepo := in.Run( - dalec.ShArgs("apt-get update && apt-get install -y apt-utils gnupg"), + dalec.ShArgs("apt-get update && apt-get install -y apt-utils gnupg2"), dalec.WithMountedAptCache(jammy.AptCachePrefix), ).File(llb.Copy(pkg, "/", "/opt/repo")). Run( diff --git a/test/windows_test.go b/test/windows_test.go index 250cac204..0813f0334 100644 --- a/test/windows_test.go +++ b/test/windows_test.go @@ -8,12 +8,16 @@ import ( "testing" "github.com/Azure/dalec" + "github.com/Azure/dalec/frontend/jammy" "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" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" ) +var windowsAmd64 = ocispecs.Platform{OS: "windows", Architecture: "amd64"} + func TestWindows(t *testing.T) { t.Parallel() @@ -23,46 +27,81 @@ func TestWindows(t *testing.T) { Container: "windowscross/container", }) + tcfg := 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. + // So... use jammy to create a deb which we'll use to create a repo. + Package: "jammy/deb", + Worker: "windowscross/worker", + FormatDepEqual: func(ver, rev string) string { + return ver + "-ubuntu22.04u" + rev + }, + } + + wcfg := workerConfig{ + ContextName: windows.WindowscrossWorkerContextName, + SignRepo: signRepoJammy, + TestRepoConfig: jammyTestRepoConfig, + Platform: &windowsAmd64, + Constraints: constraintsSymbols{ + Equal: "=", + GreaterThan: ">>", + GreaterThanOrEqual: ">=", + LessThan: "<<", + LessThanOrEqual: "<=", + }, + CreateRepo: func(pkg llb.State, opts ...llb.StateOption) llb.StateOption { + return func(in llb.State) llb.State { + repoFile := []byte(` +deb [trusted=yes] copy:/opt/repo/ / +`) + withRepo := in.Run( + dalec.ShArgs("apt-get update && apt-get install -y apt-utils gnupg2"), + dalec.WithMountedAptCache(jammy.AptCachePrefix), + ).File(llb.Copy(pkg, "/", "/opt/repo")). + Run( + llb.Dir("/opt/repo"), + dalec.ShArgs("apt-ftparchive packages . > Packages"), + ). + Run( + llb.Dir("/opt/repo"), + dalec.ShArgs("apt-ftparchive release . > Release"), + ).Root() + + for _, opt := range opts { + withRepo = opt(withRepo) + } + + return withRepo. + File(llb.Mkfile("/etc/apt/sources.list.d/test-dalec-local-repo.list", 0o644, repoFile)) + + } + }, + } + + t.Run("custom repo", func(t *testing.T) { + t.Parallel() + ctx := startTestSpan(baseCtx, t) + testCustomRepo(ctx, t, wcfg, tcfg) + }) + 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, opts ...llb.StateOption) llb.StateOption { - return func(in llb.State) llb.State { - dt := []byte(` -deb [trusted=yes] copy:/tmp/repo / -`) + testCustomWindowscrossWorker(ctx, t, tcfg, wcfg) + }) - 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")) - } - }, - }) + t.Run("pinned build dependencies", func(t *testing.T) { + t.Parallel() + ctx := startTestSpan(baseCtx, t) + testConfig := testLinuxConfig{ + Worker: wcfg, + Target: tcfg, + } + testPinnedBuildDeps(ctx, t, testConfig) }) } @@ -72,10 +111,7 @@ deb [trusted=yes] copy:/tmp/repo / // 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(cfg *newSolveRequestConfig) { - if cfg.req.FrontendOpt == nil { - cfg.req.FrontendOpt = make(map[string]string) - } - cfg.req.FrontendOpt["platform"] = "windows/amd64" + withPlatform(windowsAmd64)(cfg) } func testWindows(ctx context.Context, t *testing.T, cfg targetConfig) {