Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix custom repo test #415

Merged
merged 3 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/jammy/handle_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func buildImageRootfs(worker llb.State, spec *dalec.Spec, sOpt dalec.SourceOpts,
opts = append(opts, dalec.ProgressGroup("Install spec package"))

return baseImg.Run(
dalec.ShArgs("set -x; apt update && apt install -y /tmp/pkg/*.deb"),
installPackages([]string{"/tmp/pkg/*.deb"}, opts...),
customRepoOpts,
llb.AddEnv("DEBIAN_FRONTEND", "noninteractive"),
llb.AddMount("/tmp/pkg", deb, llb.Readonly),
Expand Down
55 changes: 44 additions & 11 deletions frontend/jammy/handle_deb.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package jammy

import (
"context"
"fmt"
"io/fs"
"strings"

Expand Down Expand Up @@ -145,26 +144,59 @@ func customRepoMounts(worker llb.State, repos []dalec.PackageRepositoryConfig, s
return dalec.WithRunOptions(withRepos, withData, keyMounts), nil
}

func installPackages(ls []string, rOpts ...llb.RunOption) llb.RunOption {
func installPackages(ls []string, opts ...llb.ConstraintsOpt) llb.RunOption {
script := llb.Scratch().File(
llb.Mkfile("install.sh", 0o755, []byte(`#!/usr/bin/env sh
set -ex

# 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

apt update
apt install -y `+strings.Join(ls, " ")+`
`,
)),
opts...)

p := "/tmp/dalec/internal/deb/install.sh"
return dalec.RunOptFunc(func(ei *llb.ExecInfo) {
// This only runs apt-get update if the pkgcache is older than 10 minutes.
dalec.ShArgs(`set -ex; apt update; apt install -y ` + strings.Join(ls, " ")).SetRunOption(ei)
llb.AddMount(p, script, llb.SourcePath("install.sh")).SetRunOption(ei)
dalec.ShArgs(p).SetRunOption(ei)
dalec.WithMountedAptCache(AptCachePrefix).SetRunOption(ei)
dalec.WithRunOptions(rOpts...).SetRunOption(ei)
})
}

func installWithConstraints(pkgPath string, pkgName string, rOpts ...llb.RunOption) llb.RunOption {
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.
dalec.ShArgs(`set -ex; dpkg -i --force-depends ` + pkgPath +
fmt.Sprintf(`; apt update; aptitude install -y -f -o "Aptitude::ProblemResolver::Hints::=reject %s :UNINST"`, pkgName)).SetRunOption(ei)
script := llb.Scratch().File(
llb.Mkfile("install.sh", 0o755, []byte(`#!/usr/bin/env sh
set -ex

# 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
aptitude install -y -f -o "Aptitude::ProblemResolver::Hints::=reject `+pkgName+` :UNINST"
`),
), opts...)

dalec.WithMountedAptCache(AptCachePrefix).SetRunOption(ei)
dalec.WithRunOptions(rOpts...).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)

})
}

Expand Down Expand Up @@ -231,7 +263,7 @@ func basePackages(opts ...llb.ConstraintsOpt) llb.StateOption {
return func(in llb.State) llb.State {
opts = append(opts, dalec.ProgressGroup("Install base packages"))
return in.Run(
installPackages([]string{"aptitude", "dpkg-dev", "devscripts", "equivs", "fakeroot", "dh-make", "build-essential", "dh-apparmor", "dh-make", "dh-exec", "debhelper-compat=" + deb.DebHelperCompat}),
installPackages([]string{"aptitude", "dpkg-dev", "devscripts", "equivs", "fakeroot", "dh-make", "build-essential", "dh-apparmor", "dh-make", "dh-exec", "debhelper-compat=" + deb.DebHelperCompat}, opts...),
dalec.WithConstraints(opts...),
).Root()
}
Expand Down Expand Up @@ -285,7 +317,8 @@ func buildDepends(worker llb.State, sOpt dalec.SourceOpts, spec *dalec.Spec, tar
)

return in.Run(
installWithConstraints(debPath+"/*.deb", depsSpec.Name, customRepoOpts),
installWithConstraints(debPath+"/*.deb", depsSpec.Name, opts...),
customRepoOpts,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for cleaning this up and addressing your review suggestion from before to add the customRepoOpts instead of threading through install...

llb.AddMount(debPath, pkg, llb.Readonly),
dalec.WithConstraints(opts...),
).Root()
Expand Down
154 changes: 85 additions & 69 deletions test/azlinux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1415,7 +1415,7 @@ func testCustomLinuxWorker(ctx context.Context, t *testing.T, targetCfg targetCo
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",
Name: "dalec-test-package-custom-worker-dep",
Version: "0.0.1",
Revision: "1",
Description: "A basic package for various testing uses",
Expand Down Expand Up @@ -1543,30 +1543,37 @@ EOF
}

func testCustomRepo(ctx context.Context, t *testing.T, cfg testLinuxConfig) {
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{
"version.txt": {
Inline: &dalec.SourceInline{
File: &dalec.SourceInlineFile{
Contents: "version: " + "0.0.1",
// 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": {},
Artifacts: dalec.Artifacts{
Docs: map[string]dalec.ArtifactConfig{
"version.txt": {},
},
},
},
}
}

getSpec := func(keyConfig map[string]dalec.Source) *dalec.Spec {
getSpec := func(dep *dalec.Spec, keyConfig map[string]dalec.Source) *dalec.Spec {
return &dalec.Spec{
Name: "dalec-test-custom-repo",
Version: "0.0.1",
Expand All @@ -1575,14 +1582,14 @@ func testCustomRepo(ctx context.Context, t *testing.T, cfg testLinuxConfig) {
License: "MIT",
Dependencies: &dalec.PackageDependencies{
Build: map[string]dalec.PackageConstraints{
"dalec-test-package": {},
dep.Name: {},
},
Runtime: map[string]dalec.PackageConstraints{
"dalec-test-package": {},
dep.Name: {},
},

Test: []string{
"dalec-test-package",
dep.Name,
"bash",
"coreutils",
},
Expand All @@ -1608,7 +1615,7 @@ func testCustomRepo(ctx context.Context, t *testing.T, cfg testLinuxConfig) {
Build: dalec.ArtifactBuild{
Steps: []dalec.BuildStep{
{
Command: `set -x; [ "$(cat /usr/share/doc/dalec-test-package/version.txt)" = "version: 0.0.1" ]`,
Command: `set -x; [ "$(cat /usr/share/doc/` + dep.Name + `/version.txt)" = "version: 0.0.1" ]`,
},
},
},
Expand All @@ -1629,7 +1636,7 @@ func testCustomRepo(ctx context.Context, t *testing.T, cfg testLinuxConfig) {

}

getRepoState := func(ctx context.Context, client gwclient.Client, w llb.State, key llb.State) llb.State {
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)

Expand All @@ -1640,68 +1647,77 @@ func testCustomRepo(ctx context.Context, t *testing.T, cfg testLinuxConfig) {
return llb.Scratch().File(llb.Copy(workerWithRepo, "/opt/repo", "/", &llb.CopyInfo{CopyDirContentsOnly: true}))
}

testNoPublicKey := func(ctx context.Context, gwc gwclient.Client) {
sr := newSolveRequest(withBuildTarget(cfg.Target.Worker), withSpec(ctx, t, nil))
w := reqToState(ctx, gwc, sr, t)
t.Run("no public key", func(t *testing.T) {
t.Parallel()

// generate a gpg public/private key pair
gpgKey := generateGPGKey(w)
testNoPublicKey := func(ctx context.Context, gwc gwclient.Client) {
sr := newSolveRequest(withBuildTarget(cfg.Target.Worker), withSpec(ctx, t, nil))
w := reqToState(ctx, gwc, sr, t)

repoState := getRepoState(ctx, gwc, w, gpgKey)
// generate a gpg public/private key pair
gpgKey := generateGPGKey(w)

sr = newSolveRequest(withSpec(ctx, t, getSpec(nil)), withBuildContext(ctx, t, "test-repo", repoState), withBuildTarget(cfg.Target.Container))
// don't error here, the logs are intended to be checked by
// RunTestExpecting
depSpec := getDepSpec("no-public-key")
repoState := getRepoState(ctx, t, gwc, w, gpgKey, depSpec)

_, err := gwc.Solve(ctx, sr)
if err == nil {
t.Fatal("expected solve to fail")
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")
}
}
}

testWithPublicKey := func(ctx context.Context, gwc gwclient.Client) {
sr := newSolveRequest(withBuildTarget(cfg.Target.Worker), withSpec(ctx, t, nil))
w := reqToState(ctx, gwc, sr, t)
testEnv.RunTest(ctx, t, testNoPublicKey)
})

t.Run("with public key", func(t *testing.T) {
t.Parallel()

// generate a gpg key to sign the repo
// under /public.key
gpgKey := generateGPGKey(w)
repoState := getRepoState(ctx, gwc, w, gpgKey)
testWithPublicKey := func(ctx context.Context, gwc gwclient.Client) {
sr := newSolveRequest(withBuildTarget(cfg.Target.Worker), withSpec(ctx, t, nil))
w := reqToState(ctx, gwc, sr, t)

spec := getSpec(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",
// 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",
},
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))
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)
res := solveT(ctx, t, gwc, sr)
_, err := res.SingleRef()
if err != nil {
t.Fatal(err)
}
}
}

t.Run("no public key", func(t *testing.T) {
t.Parallel()
testEnv.RunTest(ctx, t, testNoPublicKey)
})

t.Run("with public key", func(t *testing.T) {
t.Parallel()
testEnv.RunTest(ctx, t, testWithPublicKey)
})
}

func testPinnedBuildDeps(ctx context.Context, t *testing.T, cfg testLinuxConfig) {
pkgName := "dalec-test-package"
pkgName := "dalec-test-package-pinned"

getTestPackageSpec := func(version string) *dalec.Spec {
depSpec := &dalec.Spec{
Expand Down Expand Up @@ -1794,7 +1810,7 @@ func testPinnedBuildDeps(ctx context.Context, t *testing.T, cfg testLinuxConfig)
},
}

getWorker := func(ctx context.Context, client gwclient.Client) llb.State {
getWorker := func(ctx context.Context, t *testing.T, client gwclient.Client) llb.State {
// 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.
Expand All @@ -1817,7 +1833,7 @@ func testPinnedBuildDeps(ctx context.Context, t *testing.T, cfg testLinuxConfig)
t.Parallel()

testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) {
worker := getWorker(ctx, gwc)
worker := getWorker(ctx, t, gwc)

sr := newSolveRequest(withSpec(ctx, t, spec), withBuildContext(ctx, t, cfg.Worker.ContextName, worker), withBuildTarget(cfg.Target.Container))
res := solveT(ctx, t, gwc, sr)
Expand Down
2 changes: 1 addition & 1 deletion test/windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ func testCustomWindowscrossWorker(ctx context.Context, t *testing.T, targetCfg t
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",
Name: "dalec-test-package-windows-worker-dep",
Version: "0.0.1",
Revision: "1",
Description: "A basic package for various testing uses",
Expand Down