Skip to content

Commit d465f16

Browse files
committed
frontend: prefer dnf over tdnf to work around tdnf GPG and forcearch limitations
tdnf fails when installing signed local RPMs (from the @cmdline virtual repo) into an installroot with a populated RPM database, because it requires a gpgkey entry for @cmdline which is a synthetic repo with no config. This manifests when building containers with a custom base image on azlinux/mariner distros. Rather than working around individual tdnf bugs, prefer dnf when it is available. The install script now checks for dnf at runtime and switches from tdnf transparently. The same-platform worker bootstrap is updated to install dnf as a separate first step (mirroring the cross-arch path) so that subsequent installs benefit from dnf. Also remove hardcoded GPG email from test key ID lookups in favor of selecting the first available key. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
1 parent 9a57e74 commit d465f16

File tree

7 files changed

+158
-19
lines changed

7 files changed

+158
-19
lines changed

targets/linux/rpm/distro/dnf_install.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,18 @@ if [ -x "$import_keys_path" ]; then
183183
"$import_keys_path"
184184
fi
185185
186+
if [ "$cmd" = "tdnf" ] && command -v dnf &>/dev/null; then
187+
# tdnf has a lot of limitations that cause issues (no --forcearch, issues with gpg keys on local file installs)
188+
# We already have dnf, so prefer that.
189+
cmd="dnf"
190+
fi
191+
186192
if [ -n "$force_arch" ]; then
187-
if [ "$cmd" = "tdnf" ]; then
188-
echo "tdnf does not support --forcearch; cross-arch installs must use dnf" >&2
189-
exit 70
190-
fi
191-
install_flags="$install_flags --forcearch=$force_arch"
193+
if [ "$cmd" = "tdnf" ]; then
194+
echo "tdnf does not support --forcearch; cross-arch installs must use dnf" >&2
195+
exit 70
196+
fi
197+
install_flags="$install_flags --forcearch=$force_arch"
192198
fi
193199
194200
$cmd $dnf_sub_cmd $install_flags "${@}"
@@ -276,6 +282,8 @@ func DnfInstall(cfg *dnfInstallConfig, releaseVer string, pkgs []string) llb.Run
276282
return dnfCommand(cfg, releaseVer, "dnf", append([]string{"install"}, pkgs...), nil)
277283
}
278284

285+
// TdnfInstall uses tdnf to install packages
286+
// NOTE: tdnf will be automatically upgraded to dnf to work around tdnf limitations *if* dnf is available
279287
func TdnfInstall(cfg *dnfInstallConfig, releaseVer string, pkgs []string) llb.RunOption {
280288
return dnfCommand(cfg, releaseVer, "tdnf", append([]string{"install"}, pkgs...), nil)
281289
}

targets/linux/rpm/distro/worker.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package distro
33
import (
44
"context"
55
"encoding/json"
6+
"slices"
67

78
"github.com/containerd/platforms"
89
"github.com/moby/buildkit/client/llb"
@@ -142,6 +143,14 @@ func (cfg *Config) workerWithBuildPlatform(sOpt dalec.SourceOpts, buildPlat ocis
142143
}
143144

144145
if samePlatform(targetPlat, buildPlat) {
146+
if slices.Contains(cfg.BuilderPackages, "dnf") {
147+
// Install dnf first since this will be bootstrapped with a different package manager
148+
// This keeps the package cache for the bootstrap mananager separate from the other base packages we use.
149+
targetBase = targetBase.Run(
150+
dalec.WithConstraints(append(opts, llb.Platform(targetPlat))...),
151+
cfg.Install([]string{"dnf"}, installOpts...),
152+
).Root()
153+
}
145154
return targetBase.Run(
146155
dalec.WithConstraints(append(opts, llb.Platform(targetPlat))...),
147156
cfg.Install(cfg.BuilderPackages, installOpts...),

test/signing_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,105 @@ func distroSkipSigningTest(t *testing.T, spec *dalec.Spec, buildTarget string, e
598598
}
599599
}
600600
}
601+
602+
// signRPMs signs all RPM files in the package state using a GPG key.
603+
// The worker state must have rpmsign available (or tdnf/dnf to install it).
604+
// The gpgKey state is expected to have a private key at /private.key
605+
// (as produced by [generateGPGKey]).
606+
// The pkgState is expected to have RPMs under /RPMS/<arch>/*.rpm
607+
// (the standard package target output).
608+
// It returns the modified package state with signed RPMs.
609+
func signRPMs(worker llb.State, gpgKey llb.State, pkgState llb.State) llb.State {
610+
pg := dalec.ProgressGroup("Sign RPMs with GPG key")
611+
612+
scriptDt := `#!/usr/bin/env bash
613+
set -eux -o pipefail
614+
615+
if ! command -v rpmsign &> /dev/null; then
616+
if command -v tdnf &> /dev/null; then
617+
tdnf install -y rpm-sign
618+
elif command -v dnf &> /dev/null; then
619+
dnf install -y rpm-sign
620+
fi
621+
fi
622+
623+
gpg --import < /tmp/gpg/private.key
624+
ID=$(gpg --list-keys --keyid-format LONG | awk '/^pub/{print $2}' | cut -d/ -f2 | head -1)
625+
626+
echo "%_gpg_name $ID" > ~/.rpmmacros
627+
628+
find /tmp/rpms/RPMS -name "*.rpm" -exec rpmsign --addsign {} \;
629+
`
630+
631+
script := llb.Scratch().File(
632+
llb.Mkfile("/script.sh", 0o755, []byte(scriptDt)),
633+
pg,
634+
)
635+
636+
return worker.Run(
637+
llb.AddMount("/tmp/signing", script, llb.Readonly),
638+
llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly),
639+
llb.AddMount("/tmp/rpms", pkgState),
640+
dalec.ShArgs("/tmp/signing/script.sh"),
641+
pg,
642+
).GetMount("/tmp/rpms")
643+
}
644+
645+
// testSignedRPMCustomBaseImage tests that signed RPMs can be installed into
646+
// a container with a custom base image.
647+
//
648+
// This reproduces a bug where the tdnfrepogpgcheck plugin rejects signed RPMs
649+
// installed via "tdnf install /path/to/signed.rpm --installroot=/tmp/rootfs"
650+
// because the @cmdline virtual repo has no gpgkey entry.
651+
//
652+
// The distroImageRef parameter is the image reference for the distro's base
653+
// image (e.g., azlinux.Azlinux3Ref), which is used as the custom base image
654+
// in the spec.
655+
func testSignedRPMCustomBaseImage(ctx context.Context, t *testing.T, targetCfg targetConfig, distroImageRef string) {
656+
t.Run("signed rpm with custom base image", func(t *testing.T) {
657+
t.Parallel()
658+
ctx := startTestSpan(ctx, t)
659+
660+
testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) {
661+
// Get the worker state — we need it to generate GPG keys and sign RPMs.
662+
sr := newSolveRequest(withBuildTarget(targetCfg.Worker), withSpec(ctx, t, nil))
663+
w := reqToState(ctx, client, sr, t)
664+
665+
// Generate a GPG key pair for signing.
666+
gpgKey := generateGPGKey(w, true)
667+
668+
// Create a simple spec and build the RPM package.
669+
spec := newSimpleSpec()
670+
pkgSr := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(targetCfg.Package))
671+
pkgSt := reqToState(ctx, client, pkgSr, t)
672+
673+
// Sign the RPMs on the worker using rpmsign --addsign.
674+
signedPkgSt := signRPMs(w, gpgKey, pkgSt)
675+
676+
// Create a container spec with a custom base image.
677+
// This triggers skipBase=true in BuildContainer, meaning the RPMs
678+
// are installed via "tdnf install /path/to/signed.rpm --installroot=/tmp/rootfs"
679+
// into the custom base image's rootfs.
680+
spec.Image = &dalec.ImageConfig{
681+
Entrypoint: "/usr/bin/foo",
682+
Bases: []dalec.BaseImage{
683+
{
684+
Rootfs: dalec.Source{
685+
DockerImage: &dalec.SourceDockerImage{
686+
Ref: distroImageRef,
687+
},
688+
},
689+
},
690+
},
691+
}
692+
693+
containerSr := newSolveRequest(
694+
withSpec(ctx, t, spec),
695+
withBuildTarget(targetCfg.Container),
696+
withBuildContext(ctx, t, dalec.GenericPkg, signedPkgSt),
697+
)
698+
699+
solveT(ctx, t, client, containerSr)
700+
})
701+
})
702+
}

test/target_almalinux_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package test
22

33
import (
4+
"context"
45
"testing"
56

67
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
@@ -11,7 +12,7 @@ func TestAlmalinux9(t *testing.T) {
1112
t.Parallel()
1213

1314
ctx := startTestSpan(baseCtx, t)
14-
testLinuxDistro(ctx, t, testLinuxConfig{
15+
cfg := testLinuxConfig{
1516
Target: targetConfig{
1617
Key: "almalinux9",
1718
Package: "almalinux9/rpm",
@@ -51,14 +52,16 @@ func TestAlmalinux9(t *testing.T) {
5152
{OS: "linux", Architecture: "arm64"},
5253
},
5354
PackageOutputPath: rpmTargetOutputPath("el9"),
54-
})
55+
}
56+
testLinuxDistro(ctx, t, cfg)
57+
testAlmalinuxExtra(ctx, t, cfg, almalinux.ConfigV9.ImageRef)
5558
}
5659

5760
func TestAlmalinux8(t *testing.T) {
5861
t.Parallel()
5962

6063
ctx := startTestSpan(baseCtx, t)
61-
testLinuxDistro(ctx, t, testLinuxConfig{
64+
cfg := testLinuxConfig{
6265
Target: targetConfig{
6366
Package: "almalinux8/rpm",
6467
Container: "almalinux8/container",
@@ -97,5 +100,11 @@ func TestAlmalinux8(t *testing.T) {
97100
{OS: "linux", Architecture: "arm64"},
98101
},
99102
PackageOutputPath: rpmTargetOutputPath("el8"),
100-
})
103+
}
104+
testLinuxDistro(ctx, t, cfg)
105+
testAlmalinuxExtra(ctx, t, cfg, almalinux.ConfigV8.ImageRef)
106+
}
107+
108+
func testAlmalinuxExtra(ctx context.Context, t *testing.T, cfg testLinuxConfig, distroImageRef string) {
109+
testSignedRPMCustomBaseImage(ctx, t, cfg.Target, distroImageRef)
101110
}

test/target_azlinux_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func TestMariner2(t *testing.T) {
7979
}
8080

8181
testLinuxDistro(ctx, t, cfg)
82-
testAzlinuxExtra(ctx, t, cfg)
82+
testAzlinuxExtra(ctx, t, cfg, azlinux.Mariner2Config.ImageRef)
8383
}
8484

8585
func TestAzlinux3(t *testing.T) {
@@ -122,7 +122,7 @@ func TestAzlinux3(t *testing.T) {
122122
PackageOutputPath: rpmTargetOutputPath("azl3"),
123123
}
124124
testLinuxDistro(ctx, t, cfg)
125-
testAzlinuxExtra(ctx, t, cfg)
125+
testAzlinuxExtra(ctx, t, cfg, azlinux.Azlinux3Config.ImageRef)
126126

127127
t.Run("ca-certs override", func(t *testing.T) {
128128
t.Parallel()
@@ -131,12 +131,14 @@ func TestAzlinux3(t *testing.T) {
131131
})
132132
}
133133

134-
func testAzlinuxExtra(ctx context.Context, t *testing.T, cfg testLinuxConfig) {
134+
func testAzlinuxExtra(ctx context.Context, t *testing.T, cfg testLinuxConfig, distroImageRef string) {
135135
t.Run("base deps", func(t *testing.T) {
136136
t.Parallel()
137137
ctx := startTestSpan(ctx, t)
138138
testAzlinuxBaseDeps(ctx, t, cfg.Target)
139139
})
140+
141+
testSignedRPMCustomBaseImage(ctx, t, cfg.Target, distroImageRef)
140142
}
141143

142144
func testAzlinuxCaCertsOverride(ctx context.Context, t *testing.T, target targetConfig) {
@@ -187,7 +189,7 @@ func signRepoAzLinux(gpgKey llb.State, repoPath string) llb.StateOption {
187189
set -eux -o pipefail
188190
189191
gpg --import < /tmp/gpg/private.key
190-
ID=$(gpg --list-keys --keyid-format LONG | grep -B 2 'test@example.com' | grep 'pub' | awk '{print $2}' | cut -d'/' -f2)
192+
ID=$(gpg --list-keys --keyid-format LONG | awk '/^pub/{print $2}' | cut -d/ -f2 | head -1)
191193
192194
# For tdnf-based distros, only sign repo metadata, not individual packages
193195
# tdnf only checks repo metadata signatures, not package signatures
@@ -233,7 +235,7 @@ if ! command -v rpm-sign &> /dev/null; then
233235
fi
234236
235237
gpg --import < /tmp/gpg/private.key
236-
ID=$(gpg --list-keys --keyid-format LONG | grep -B 2 'test@example.com' | grep 'pub' | awk '{print $2}' | cut -d'/' -f2)
238+
ID=$(gpg --list-keys --keyid-format LONG | awk '/^pub/{print $2}' | cut -d/ -f2 | head -1)
237239
238240
echo "%_gpg_name $ID" > ~/.rpmmacros
239241
find ` + repoPath + `/RPMS -name "*.rpm" -exec rpmsign --addsign {} \;

test/target_rockylinux_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package test
22

33
import (
4+
"context"
45
"testing"
56

67
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
@@ -11,7 +12,7 @@ func TestRockylinux9(t *testing.T) {
1112
t.Parallel()
1213

1314
ctx := startTestSpan(baseCtx, t)
14-
testLinuxDistro(ctx, t, testLinuxConfig{
15+
cfg := testLinuxConfig{
1516
Target: targetConfig{
1617
Key: "rockylinux9",
1718
Package: "rockylinux9/rpm",
@@ -51,14 +52,16 @@ func TestRockylinux9(t *testing.T) {
5152
{OS: "linux", Architecture: "arm64"},
5253
},
5354
PackageOutputPath: rpmTargetOutputPath("el9"),
54-
})
55+
}
56+
testLinuxDistro(ctx, t, cfg)
57+
testRockylinuxExtra(ctx, t, cfg, rockylinux.ConfigV9.ImageRef)
5558
}
5659

5760
func TestRockylinux8(t *testing.T) {
5861
t.Parallel()
5962

6063
ctx := startTestSpan(baseCtx, t)
61-
testLinuxDistro(ctx, t, testLinuxConfig{
64+
cfg := testLinuxConfig{
6265
Target: targetConfig{
6366
Package: "rockylinux8/rpm",
6467
Container: "rockylinux8/container",
@@ -97,5 +100,11 @@ func TestRockylinux8(t *testing.T) {
97100
{OS: "linux", Architecture: "arm64"},
98101
},
99102
PackageOutputPath: rpmTargetOutputPath("el8"),
100-
})
103+
}
104+
testLinuxDistro(ctx, t, cfg)
105+
testRockylinuxExtra(ctx, t, cfg, rockylinux.ConfigV8.ImageRef)
106+
}
107+
108+
func testRockylinuxExtra(ctx context.Context, t *testing.T, cfg testLinuxConfig, distroImageRef string) {
109+
testSignedRPMCustomBaseImage(ctx, t, cfg.Target, distroImageRef)
101110
}

test/target_ubuntu_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func signRepoUbuntu(gpgKey llb.State, repoPath string) llb.StateOption {
123123
llb.AddMount("/tmp/gpg", gpgKey, llb.Readonly),
124124
dalec.ProgressGroup("Importing gpg key")).
125125
Run(
126-
dalec.ShArgs(`ID=$(gpg --list-keys --keyid-format LONG | grep -B 2 'test@example.com' | grep 'pub' | awk '{print $2}' | cut -d'/' -f2) && \
126+
dalec.ShArgs(`ID=$(gpg --list-keys --keyid-format LONG | awk '/^pub/{print $2}' | cut -d/ -f2 | head -1) && \
127127
gpg --list-keys --keyid-format LONG && \
128128
gpg --default-key $ID -abs -o `+repoPath+`/Release.gpg `+repoPath+`/Release && \
129129
gpg --default-key "$ID" --clearsign -o `+repoPath+`/InRelease `+repoPath+`/Release`),

0 commit comments

Comments
 (0)