diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index 73f1a5f0..ffb1652b 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -163,6 +163,20 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, pat return err } + if solveOpt.SourcePolicy != nil { + switch { + case strings.Contains(solveOpt.SourcePolicy.Rules[0].Updates.Identifier, "redhat"): + err = errors.New("RedHat is not supported via source policies due to BusyBox not being in the RHEL repos\n" + + "Please use a different RPM-based image") + return err + + case strings.Contains(solveOpt.SourcePolicy.Rules[0].Updates.Identifier, "rockylinux"): + err = errors.New("RockyLinux is not supported via source policies due to BusyBox not being in the RockyLinux repos\n" + + "Please use a different RPM-based image") + return err + } + } + buildChannel := make(chan *client.SolveStatus) eg, ctx := errgroup.WithContext(ctx) eg.Go(func() error { diff --git a/pkg/pkgmgr/rpm.go b/pkg/pkgmgr/rpm.go index 5c7439db..a376afa1 100644 --- a/pkg/pkgmgr/rpm.go +++ b/pkg/pkgmgr/rpm.go @@ -34,7 +34,6 @@ const ( rpmManifest2 = "container-manifest-2" rpmManifestWildcard = "container-manifest-*" - installToolsCmd = "tdnf install busybox cpio dnf-utils -y" resultQueryFormat = "%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n" ) @@ -255,14 +254,21 @@ func (rm *rpmManager) probeRPMStatus(ctx context.Context, toolImage string) erro llb.ResolveModeDefault, ) + // List all packages installed in the tooling image + toolsListed := toolingBase.Run(llb.Shlex(`sh -c 'ls /usr/bin > applications.txt'`)).Root() + installToolsCmd, err := rm.generateToolInstallCmd(ctx, &toolsListed) + if err != nil { + return err + } + + packageManagers := []string{"tdnf", "dnf", "microdnf", "yum", "rpm"} + toolsInstalled := toolingBase.Run(llb.Shlex(installToolsCmd), llb.WithProxy(utils.GetProxy())).Root() toolsApplied := rm.config.ImageState.File(llb.Copy(toolsInstalled, "/usr/sbin/busybox", "/usr/sbin/busybox")) mkFolders := toolsApplied. File(llb.Mkdir(resultsPath, 0o744, llb.WithParents(true))). File(llb.Mkdir(inputPath, 0o744, llb.WithParents(true))) - toolList := []string{"dnf", "microdnf", "rpm", "yum"} - rpmDBList := []string{ filepath.Join(rpmLibPath, rpmBDB), filepath.Join(rpmLibPath, rpmNDB), @@ -274,7 +280,7 @@ func (rm *rpmManager) probeRPMStatus(ctx context.Context, toolImage string) erro toolListPath := filepath.Join(inputPath, "tool_list") dbListPath := filepath.Join(inputPath, "rpm_db_list") - probed := buildkit.WithArrayFile(&mkFolders, toolListPath, toolList) + probed := buildkit.WithArrayFile(&mkFolders, toolListPath, packageManagers) probed = buildkit.WithArrayFile(&probed, dbListPath, rpmDBList) outState := probed.Run( llb.AddEnv("TOOL_LIST_PATH", toolListPath), @@ -361,6 +367,45 @@ func (rm *rpmManager) probeRPMStatus(ctx context.Context, toolImage string) erro return nil } +func (rm *rpmManager) generateToolInstallCmd(ctx context.Context, toolsListed *llb.State) (string, error) { + applicationsList, err := buildkit.ExtractFileFromState(ctx, rm.config.Client, toolsListed, "/applications.txt") + if err != nil { + return "", err + } + + // packageManagersInstalled is the package manager(s) available within the tooling image + // RPM must be excluded from this list as it cannot connect to RPM repos + var packageManagersInstalled []string + packageManagerList := []string{"tdnf", "dnf", "microdnf", "yum"} + + for _, packageManager := range packageManagerList { + if strings.Contains(string(applicationsList), packageManager) { + packageManagersInstalled = append(packageManagersInstalled, packageManager) + } + } + + // missingTools indicates which tools, if any, need to be installed within the tooling image + var missingTools []string + requiredToolingList := []string{"busybox", "dnf-utils", "cpio"} + + for _, tool := range requiredToolingList { + isMissingTool := !strings.Contains(string(applicationsList), tool) + if isMissingTool { + missingTools = append(missingTools, tool) + } + + if tool == "cpio" && !isMissingTool && strings.Contains(string(applicationsList), "rpm2cpio") { + missingTools = append(missingTools, "cpio") + } + } + + // A tooling image could contain multiple package managers + // Choose the first one detected to use in the installation command + installCmd := fmt.Sprintf("%s install %s -y", packageManagersInstalled[0], strings.Join(missingTools, " ")) + + return installCmd, nil +} + func parseManifestFile(file string) (map[string]string, error) { // split into lines file = strings.TrimSuffix(file, "\n") @@ -496,6 +541,13 @@ func (rm *rpmManager) unpackAndMergeUpdates(ctx context.Context, updates unversi llb.ResolveModeDefault, ) + // List all packages installed in the tooling image + toolsListed := toolingBase.Run(llb.Shlex(`sh -c 'ls /usr/bin > applications.txt'`)).Root() + installToolsCmd, err := rm.generateToolInstallCmd(ctx, &toolsListed) + if err != nil { + return nil, nil, err + } + // Install busybox. This should reuse the layer cached from probeRPMStatus. toolsInstalled := toolingBase.Run(llb.Shlex(installToolsCmd), llb.WithProxy(utils.GetProxy())).Root() busyboxCopied := toolsInstalled.Dir(downloadPath).Run(llb.Shlex("cp /usr/sbin/busybox .")).Root() diff --git a/pkg/pkgmgr/rpm_test.go b/pkg/pkgmgr/rpm_test.go index de5c9f27..f088b445 100644 --- a/pkg/pkgmgr/rpm_test.go +++ b/pkg/pkgmgr/rpm_test.go @@ -590,6 +590,8 @@ func Test_installUpdates_RPM(t *testing.T) { } func Test_unpackAndMergeUpdates_RPM(t *testing.T) { + // Due to the generateToolInstallCmd function, we need to pass in a package manager as well + // Without a package manager passed in, these tests all fail tests := []struct { name string updates unversioned.UpdatePackages @@ -602,7 +604,7 @@ func Test_unpackAndMergeUpdates_RPM(t *testing.T) { { name: "Successful update with specific packages", mockSetup: func(mr *mocks.MockReference) { - mr.On("ReadFile", mock.Anything, mock.Anything).Return([]byte("package1\t1.2.3\tx86_64\npackage2\t2.3.4\tx86_64"), nil) + mr.On("ReadFile", mock.Anything, mock.Anything).Return([]byte("package1\t1.2.3\tx86_64\npackage2\t2.3.4\tx86_64\ntdnf"), nil) }, updates: unversioned.UpdatePackages{ {Name: "package1", FixedVersion: "1.2.3"}, @@ -611,23 +613,23 @@ func Test_unpackAndMergeUpdates_RPM(t *testing.T) { toolImage: "test-tool-image:latest", ignoreErrors: false, expectedError: false, - expectedResult: []byte("package1\t1.2.3\tx86_64\npackage2\t2.3.4\tx86_64"), + expectedResult: []byte("package1\t1.2.3\tx86_64\npackage2\t2.3.4\tx86_64\ntdnf"), }, { name: "Successful update all packages", mockSetup: func(mr *mocks.MockReference) { - mr.On("ReadFile", mock.Anything, mock.Anything).Return([]byte(nil), nil) + mr.On("ReadFile", mock.Anything, mock.Anything).Return([]byte("tdnf"), nil) }, updates: nil, toolImage: "test-tool-image:latest", ignoreErrors: false, - expectedResult: nil, + expectedResult: []byte("tdnf"), expectedError: false, }, { name: "Ignore errors during update", mockSetup: func(mr *mocks.MockReference) { - mr.On("ReadFile", mock.Anything, mock.Anything).Return([]byte("package1\t1.0.1\n"), nil) + mr.On("ReadFile", mock.Anything, mock.Anything).Return([]byte("package1\t1.0.1\ntdnf"), nil) }, updates: unversioned.UpdatePackages{ {Name: "package1", FixedVersion: "2.0.0"}, @@ -635,7 +637,7 @@ func Test_unpackAndMergeUpdates_RPM(t *testing.T) { toolImage: "test-tool-image:latest", ignoreErrors: true, expectedError: false, - expectedResult: []byte("package1\t1.0.1\n"), + expectedResult: []byte("package1\t1.0.1\ntdnf"), }, }