Skip to content

Commit 0c86ad6

Browse files
amanycodesrobert-croninashnamehrotra
authored
fix: Non Zero Exit code for no upgradable packages causes build fail (#1274)
Signed-off-by: amanycodes <amanycodes@gmail.com> Signed-off-by: Aman <95525722+amanycodes@users.noreply.github.com> Co-authored-by: Robbie Cronin <robert.owen.cronin@gmail.com> Co-authored-by: Ashna Mehrotra <ashnamehrotra@gmail.com>
1 parent 599e0ac commit 0c86ad6

File tree

7 files changed

+144
-58
lines changed

7 files changed

+144
-58
lines changed

main.go

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

33
import (
4+
"errors"
45
"os"
56
"strings"
67

78
"github.com/project-copacetic/copacetic/pkg/generate"
89
"github.com/project-copacetic/copacetic/pkg/patch"
10+
"github.com/project-copacetic/copacetic/pkg/types"
911
log "github.com/sirupsen/logrus"
1012
"github.com/spf13/cobra"
1113
"github.com/spf13/viper"
@@ -51,7 +53,12 @@ func initConfig() {
5153
func main() {
5254
cobra.OnInitialize(initConfig)
5355
rootCmd := newRootCmd()
56+
5457
if err := rootCmd.Execute(); err != nil {
58+
if errors.Is(err, types.ErrNoUpdatesFound) {
59+
log.Info("Image is already up-to-date. No patch was applied.")
60+
os.Exit(0)
61+
}
5562
os.Exit(1)
5663
}
5764
}

pkg/patch/multi.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@ package patch
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"runtime"
78
"strings"
89
"sync"
910
"text/tabwriter"
1011

1112
"github.com/distribution/reference"
13+
"github.com/project-copacetic/copacetic/pkg/common"
14+
"github.com/project-copacetic/copacetic/pkg/types"
1215
log "github.com/sirupsen/logrus"
1316
"golang.org/x/sync/errgroup"
1417

1518
"github.com/project-copacetic/copacetic/pkg/buildkit"
16-
"github.com/project-copacetic/copacetic/pkg/common"
17-
"github.com/project-copacetic/copacetic/pkg/types"
1819
)
1920

2021
// patchMultiPlatformImage patches a multi-platform image across all discovered platforms.
@@ -207,16 +208,32 @@ func patchMultiPlatformImage(
207208
mu.Lock()
208209
defer mu.Unlock()
209210
if err != nil {
211+
if errors.Is(err, types.ErrNoUpdatesFound) {
212+
patchResults = append(patchResults, *res)
213+
summaryMap[platformKey] = &types.MultiPlatformSummary{
214+
Platform: platformKey,
215+
Status: "Up-to-date",
216+
Ref: res.OriginalRef.String() + " (original)",
217+
Message: "Image is already up-to-date",
218+
}
219+
return nil
220+
}
221+
222+
status := "Error"
223+
if ignoreError {
224+
status = "Ignored"
225+
}
210226
summaryMap[platformKey] = &types.MultiPlatformSummary{
211227
Platform: platformKey,
212-
Status: "Error",
228+
Status: status,
213229
Ref: "",
214230
Message: err.Error(),
215231
}
216232
hasErrors = true
217233
// Continue processing other platforms
218234
return nil
219-
} else if res == nil {
235+
}
236+
if res == nil {
220237
summaryMap[platformKey] = &types.MultiPlatformSummary{
221238
Platform: platformKey,
222239
Status: "Error",
@@ -338,6 +355,16 @@ func patchMultiPlatformImage(
338355
w.Flush()
339356
log.Info("\nMulti-arch patch summary:\n" + b.String())
340357

358+
anyPatchesApplied := false
359+
for _, summary := range summaryMap {
360+
if summary.Status == "Patched" {
361+
anyPatchesApplied = true
362+
break
363+
}
364+
}
365+
if !anyPatchesApplied && len(summaryMap) > 0 {
366+
return types.ErrNoUpdatesFound
367+
}
341368
// Create OCI layout if requested and not pushing to registry
342369
if opts.OCIDir != "" && !opts.Push {
343370
if err := buildkit.CreateOCILayoutFromResults(opts.OCIDir, patchResults, platforms); err != nil {

pkg/patch/single.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package patch
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io"
78
"maps"
@@ -224,6 +225,10 @@ func patchSingleArchImage(
224225

225226
// Wait for completion
226227
if err := eg.Wait(); err != nil {
228+
if errors.Is(err, types.ErrNoUpdatesFound) {
229+
res, _ := createOriginalImageResult(imageName, &targetPlatform, image)
230+
return res, types.ErrNoUpdatesFound
231+
}
227232
return nil, err
228233
}
229234

@@ -578,3 +583,16 @@ func parsePkgTypes(pkgTypesStr string) ([]string, error) {
578583

579584
return validTypes, nil
580585
}
586+
587+
func createOriginalImageResult(imageName reference.Named, targetPlatform *types.PatchPlatform, originalImageRef string) (*types.PatchResult, error) {
588+
originalDesc, err := getPlatformDescriptorFromManifest(originalImageRef, targetPlatform)
589+
if err != nil {
590+
log.Warnf("Could not get original descriptor for up-to-date platform %s/%s: %v", targetPlatform.OS, targetPlatform.Architecture, err)
591+
}
592+
593+
return &types.PatchResult{
594+
OriginalRef: imageName,
595+
PatchedRef: imageName,
596+
PatchedDesc: originalDesc,
597+
}, nil
598+
}

pkg/pkgmgr/apk.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
apkVer "github.com/knqyf263/go-apk-version"
1313
"github.com/moby/buildkit/client/llb"
1414
"github.com/project-copacetic/copacetic/pkg/buildkit"
15+
"github.com/project-copacetic/copacetic/pkg/types"
1516
"github.com/project-copacetic/copacetic/pkg/types/unversioned"
1617
"github.com/project-copacetic/copacetic/pkg/utils"
1718
log "github.com/sirupsen/logrus"
@@ -168,9 +169,15 @@ func (am *apkManager) upgradePackages(ctx context.Context, updates unversioned.U
168169

169170
// If updating all packages, check for upgrades before proceeding with patch
170171
if updates == nil {
171-
checkUpgradable := `sh -c "apk list 2>/dev/null | grep -q "upgradable" || exit 1"`
172-
apkUpdated = apkUpdated.Run(llb.Shlex(checkUpgradable),
173-
llb.WithCustomName("Checking for available updates")).Root()
172+
const updatesAvailableMarker = "/updates.txt"
173+
checkUpgradable := fmt.Sprintf(`sh -c 'if apk list 2>/dev/null | grep -q "upgradable"; then touch %s; fi'`, updatesAvailableMarker)
174+
stateWithCheck := apkUpdated.Run(llb.Shlex(checkUpgradable)).Root()
175+
176+
_, err := buildkit.ExtractFileFromState(ctx, am.config.Client, &stateWithCheck, updatesAvailableMarker)
177+
if err != nil {
178+
log.Info("No upgradable packages found for this image.")
179+
return nil, nil, types.ErrNoUpdatesFound
180+
}
174181
}
175182

176183
var apkInstalled llb.State

pkg/pkgmgr/dpkg.go

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/moby/buildkit/client/llb"
1818
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
1919
"github.com/project-copacetic/copacetic/pkg/buildkit"
20+
"github.com/project-copacetic/copacetic/pkg/types"
2021
"github.com/project-copacetic/copacetic/pkg/types/unversioned"
2122
"github.com/project-copacetic/copacetic/pkg/utils"
2223
log "github.com/sirupsen/logrus"
@@ -345,9 +346,18 @@ func (dm *dpkgManager) installUpdates(ctx context.Context, updates unversioned.U
345346
llb.WithCustomName("Updating package database"),
346347
).Root()
347348

348-
checkUpgradable := `sh -c "apt-get -s upgrade 2>/dev/null | grep -q "^Inst" || exit 1"`
349-
aptGetUpdated = aptGetUpdated.Run(llb.Shlex(checkUpgradable),
350-
llb.WithCustomName("Checking for available updates")).Root()
349+
// Only check for upgradable packages when updating all (no specific updates list).
350+
if updates == nil {
351+
const updatesAvailableMarker = "/updates.txt"
352+
checkUpgradable := fmt.Sprintf(`sh -c 'if apt-get -s upgrade 2>/dev/null | grep -q "^Inst"; then touch %s; fi'`, updatesAvailableMarker)
353+
aptGetUpdated = aptGetUpdated.Run(llb.Shlex(checkUpgradable)).Root()
354+
355+
_, err := buildkit.ExtractFileFromState(ctx, dm.config.Client, &aptGetUpdated, updatesAvailableMarker)
356+
if err != nil {
357+
log.Info("No upgradable packages found for this image.")
358+
return nil, nil, types.ErrNoUpdatesFound
359+
}
360+
}
351361

352362
// detect held packages and log them
353363
checkHeldCmd := `sh -c "apt-mark showhold | tee /held.txt"`
@@ -464,29 +474,31 @@ func (dm *dpkgManager) unpackAndMergeUpdates(ctx context.Context, updates unvers
464474
llb.AddEnv("PACKAGES_PRESENT", string(jsonPackageData)),
465475
llb.Args([]string{
466476
`bash`, `-c`, `
467-
json_str=$PACKAGES_PRESENT
468-
update_packages=""
469-
470-
while IFS=':' read -r package version; do
471-
pkg_name=$(echo "$package" | sed 's/^"\(.*\)"$/\1/')
472-
pkg_version=$(echo "$version" | sed 's/^"\(.*\)"$/\1/')
473-
latest_version=$(apt show $pkg_name 2>/dev/null | awk -F ': ' '/Version:/{print $2}')
474-
475-
if [ "$latest_version" != "$pkg_version" ]; then
476-
update_packages="$update_packages $pkg_name"
477-
fi
478-
done <<< "$(echo "$json_str" | tr -d '{}\n' | tr ',' '\n')"
479-
480-
if [ -z "$update_packages" ]; then
481-
echo "No packages to update"
482-
exit 1
483-
fi
484-
485-
mkdir /var/cache/apt/archives
486-
cd /var/cache/apt/archives
487-
echo "$update_packages" > packages.txt
488-
`,
477+
json_str=$PACKAGES_PRESENT
478+
update_packages=""
479+
480+
while IFS=':' read -r package version; do
481+
pkg_name=$(echo "$package" | sed 's/^"\(.*\)"$/\1/')
482+
pkg_version=$(echo "$version" | sed 's/^"\(.*\)"$/\1/')
483+
latest_version=$(apt show $pkg_name 2>/dev/null | awk -F ': ' '/Version:/{print $2}')
484+
485+
if [ "$latest_version" != "$pkg_version" ]; then
486+
update_packages="$update_packages $pkg_name"
487+
fi
488+
done <<< "$(echo "$json_str" | tr -d '{}\n' | tr ',' '\n')"
489+
490+
if [ -n "$update_packages" ]; then
491+
mkdir -p /var/cache/apt/archives
492+
cd /var/cache/apt/archives
493+
echo "$update_packages" > packages.txt
494+
touch /updates.txt
495+
fi
496+
`,
489497
})).Root()
498+
if _, err := buildkit.ExtractFileFromState(ctx, dm.config.Client, &updated, "/updates.txt"); err != nil {
499+
log.Info("No upgradable packages found for this image (distroless path).")
500+
return nil, nil, types.ErrNoUpdatesFound
501+
}
490502
}
491503

492504
// Replace status file in tooling image with new status file with relevant pacakges from image to be patched.

pkg/pkgmgr/rpm.go

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/moby/buildkit/client/llb"
1818
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
1919
"github.com/project-copacetic/copacetic/pkg/buildkit"
20+
"github.com/project-copacetic/copacetic/pkg/types"
2021
"github.com/project-copacetic/copacetic/pkg/types/unversioned"
2122
"github.com/project-copacetic/copacetic/pkg/utils"
2223
log "github.com/sirupsen/logrus"
@@ -489,25 +490,31 @@ func (rm *rpmManager) installUpdates(ctx context.Context, updates unversioned.Up
489490
if dnfTooling == "" {
490491
dnfTooling = rm.rpmTools["dnf"]
491492
}
492-
checkUpdateTemplate := `sh -c '%[1]s clean all && %[1]s makecache --refresh -y; if [ "$(%[1]s -q check-update | wc -l)" -ne 0 ]; then echo >> /updates.txt; fi'`
493-
if !rm.checkForUpgrades(ctx, dnfTooling, checkUpdateTemplate) {
494-
return nil, nil, fmt.Errorf("no patchable packages found")
493+
if updates == nil {
494+
checkUpdateTemplate := `sh -c '%[1]s clean all && %[1]s makecache --refresh -y; if [ "$(%[1]s -q check-update | wc -l)" -ne 0 ]; then echo >> /updates.txt; fi'`
495+
if !rm.checkForUpgrades(ctx, dnfTooling, checkUpdateTemplate) {
496+
return nil, nil, types.ErrNoUpdatesFound
497+
}
495498
}
496499

497500
const dnfInstallTemplate = `sh -c '%[1]s upgrade --refresh %[2]s -y && %[1]s clean all'`
498501
installCmd = fmt.Sprintf(dnfInstallTemplate, dnfTooling, pkgs)
499502
case rm.rpmTools["yum"] != "":
500-
checkUpdateTemplate := `sh -c '%[1]s clean all && %[1]s makecache fast; if [ "$(%[1]s -q check-update | wc -l)" -ne 0 ]; then echo >> /updates.txt; fi'`
501-
if !rm.checkForUpgrades(ctx, rm.rpmTools["yum"], checkUpdateTemplate) {
502-
return nil, nil, fmt.Errorf("no patchable packages found")
503+
if updates == nil {
504+
checkUpdateTemplate := `sh -c '%[1]s clean all && %[1]s makecache fast; if [ "$(%[1]s -q check-update | wc -l)" -ne 0 ]; then echo >> /updates.txt; fi'`
505+
if !rm.checkForUpgrades(ctx, rm.rpmTools["yum"], checkUpdateTemplate) {
506+
return nil, nil, types.ErrNoUpdatesFound
507+
}
503508
}
504509

505510
const yumInstallTemplate = `sh -c '%[1]s upgrade %[2]s -y && %[1]s clean all'`
506511
installCmd = fmt.Sprintf(yumInstallTemplate, rm.rpmTools["yum"], pkgs)
507512
case rm.rpmTools["microdnf"] != "":
508-
checkUpdateTemplate := `sh -c "%[1]s install dnf -y; dnf clean all && dnf makecache --refresh -y; dnf check-update -y; if [ $? -ne 0 ]; then echo >> /updates.txt; fi;"`
509-
if !rm.checkForUpgrades(ctx, rm.rpmTools["microdnf"], checkUpdateTemplate) {
510-
return nil, nil, fmt.Errorf("no patchable packages found")
513+
if updates == nil {
514+
checkUpdateTemplate := `sh -c "%[1]s install dnf -y; dnf clean all && dnf makecache --refresh -y; dnf check-update -y; if [ $? -ne 0 ]; then echo >> /updates.txt; fi;"`
515+
if !rm.checkForUpgrades(ctx, rm.rpmTools["microdnf"], checkUpdateTemplate) {
516+
return nil, nil, types.ErrNoUpdatesFound
517+
}
511518
}
512519

513520
const microdnfInstallTemplate = `sh -c '%[1]s update %[2]s -y && %[1]s clean all'`
@@ -619,28 +626,30 @@ func (rm *rpmManager) unpackAndMergeUpdates(ctx context.Context, updates unversi
619626
llb.AddEnv("PACKAGES_PRESENT", string(jsonPackageData)),
620627
llb.Args([]string{
621628
`bash`, `-c`, `
622-
json_str=$PACKAGES_PRESENT
623-
update_packages=""
624-
625-
while IFS=':' read -r package version; do
626-
pkg_name=$(echo "$package" | sed 's/^"\(.*\)"$/\1/')
629+
json_str=$PACKAGES_PRESENT
630+
update_packages=""
627631
628-
pkg_version=$(echo "$version" | sed 's/^"\(.*\)"$/\1/')
629-
latest_version=$(yum list available $pkg_name 2>/dev/null | grep $pkg_name | tail -n 1 | tr -s ' ' | cut -d ' ' -f 2)
632+
while IFS=':' read -r package version; do
633+
pkg_name=$(echo "$package" | sed 's/^"\(.*\)"$/\1/')
630634
631-
if [ "$latest_version" != "$pkg_version" ]; then
632-
update_packages="$update_packages $pkg_name"
633-
fi
634-
done <<< "$(echo "$json_str" | tr -d '{}\n' | tr ',' '\n')"
635+
pkg_version=$(echo "$version" | sed 's/^"\(.*\)"$/\1/')
636+
latest_version=$(yum list available $pkg_name 2>/dev/null | grep $pkg_name | tail -n 1 | tr -s ' ' | cut -d ' ' -f 2)
635637
636-
if [ -z "$update_packages" ]; then
637-
echo "No packages to update"
638-
exit 1
639-
fi
638+
if [ "$latest_version" != "$pkg_version" ]; then
639+
update_packages="$update_packages $pkg_name"
640+
fi
641+
done <<< "$(echo "$json_str" | tr -d '{}\n' | tr ',' '\n')"
640642
641-
echo "$update_packages" > packages.txt
642-
`,
643+
if [ -n "$update_packages" ]; then
644+
echo "$update_packages" > packages.txt
645+
touch /updates.txt
646+
fi
647+
`,
643648
})).Root()
649+
if _, err := buildkit.ExtractFileFromState(ctx, rm.config.Client, &busyboxCopied, "/updates.txt"); err != nil {
650+
log.Info("No upgradable packages found for this image (RPM distroless path).")
651+
return nil, nil, types.ErrNoUpdatesFound
652+
}
644653
}
645654

646655
// Create a new state for tooling image with all the packages from the image we are trying to patch

pkg/types/errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package types
2+
3+
import "errors"
4+
5+
// ErrNoUpdatesFound indicates that no package updates are available for the image.
6+
var ErrNoUpdatesFound = errors.New("no package updates found for image")

0 commit comments

Comments
 (0)