Skip to content
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package main

import (
"errors"
"os"
"strings"

"github.com/project-copacetic/copacetic/pkg/patch"
"github.com/project-copacetic/copacetic/pkg/types"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -49,7 +51,12 @@ func initConfig() {
func main() {
cobra.OnInitialize(initConfig)
rootCmd := newRootCmd()

if err := rootCmd.Execute(); err != nil {
if errors.Is(err, types.ErrNoUpdatesFound) {
log.Info("Image is already up-to-date. No patch was applied.")
os.Exit(0)
}
os.Exit(1)
}
}
29 changes: 26 additions & 3 deletions pkg/patch/multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import (
"text/tabwriter"

"github.com/distribution/reference"
"github.com/project-copacetic/copacetic/pkg/common"
"github.com/project-copacetic/copacetic/pkg/types"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"

"github.com/project-copacetic/copacetic/pkg/buildkit"
"github.com/project-copacetic/copacetic/pkg/common"
"github.com/project-copacetic/copacetic/pkg/types"
)

// patchMultiPlatformImage patches a multi-platform image across all discovered platforms.
Expand Down Expand Up @@ -196,6 +196,17 @@ func patchMultiPlatformImage(
mu.Lock()
defer mu.Unlock()
if err != nil {
if errors.Is(err, types.ErrNoUpdatesFound) {
patchResults = append(patchResults, *res)
summaryMap[platformKey] = &types.MultiPlatformSummary{
Platform: platformKey,
Status: "Up-to-date",
Ref: res.OriginalRef.String() + " (original)",
Message: "Image is already up-to-date",
}
return nil
}

status := "Error"
if ignoreError {
status = "Ignored"
Expand All @@ -210,7 +221,8 @@ func patchMultiPlatformImage(
return err
}
return nil
} else if res == nil {
}
if res == nil {
summaryMap[platformKey] = &types.MultiPlatformSummary{
Platform: platformKey,
Status: "Error",
Expand Down Expand Up @@ -320,5 +332,16 @@ func patchMultiPlatformImage(
w.Flush()
log.Info("\nMulti-arch patch summary:\n" + b.String())

anyPatchesApplied := false
for _, summary := range summaryMap {
if summary.Status == "Patched" {
anyPatchesApplied = true
break
}
}
if !anyPatchesApplied && len(summaryMap) > 0 {
return types.ErrNoUpdatesFound
}

return nil
}
18 changes: 18 additions & 0 deletions pkg/patch/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package patch

import (
"context"
"errors"
"fmt"
"io"
"maps"
Expand Down Expand Up @@ -213,6 +214,10 @@ func patchSingleArchImage(

// Wait for completion
if err := eg.Wait(); err != nil {
if errors.Is(err, types.ErrNoUpdatesFound) {
res, _ := createOriginalImageResult(imageName, &targetPlatform, image)
return res, types.ErrNoUpdatesFound
}
return nil, err
}

Expand Down Expand Up @@ -534,3 +539,16 @@ func parsePkgTypes(pkgTypesStr string) ([]string, error) {

return validTypes, nil
}

func createOriginalImageResult(imageName reference.Named, targetPlatform *types.PatchPlatform, originalImageRef string) (*types.PatchResult, error) {
originalDesc, err := getPlatformDescriptorFromManifest(originalImageRef, targetPlatform)
if err != nil {
log.Warnf("Could not get original descriptor for up-to-date platform %s/%s: %v", targetPlatform.OS, targetPlatform.Architecture, err)
}

return &types.PatchResult{
OriginalRef: imageName,
PatchedRef: imageName,
PatchedDesc: originalDesc,
}, nil
}
12 changes: 10 additions & 2 deletions pkg/pkgmgr/apk.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
apkVer "github.com/knqyf263/go-apk-version"
"github.com/moby/buildkit/client/llb"
"github.com/project-copacetic/copacetic/pkg/buildkit"
"github.com/project-copacetic/copacetic/pkg/types"
"github.com/project-copacetic/copacetic/pkg/types/unversioned"
"github.com/project-copacetic/copacetic/pkg/utils"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -167,8 +168,15 @@ func (am *apkManager) upgradePackages(ctx context.Context, updates unversioned.U

// If updating all packages, check for upgrades before proceeding with patch
if updates == nil {
checkUpgradable := `sh -c "apk list 2>/dev/null | grep -q "upgradable" || exit 1"`
apkUpdated = apkUpdated.Run(llb.Shlex(checkUpgradable)).Root()
const updatesAvailableMarker = "/updates.txt"
checkUpgradable := fmt.Sprintf(`sh -c 'if apk list 2>/dev/null | grep -q "upgradable"; then touch %s; fi'`, updatesAvailableMarker)
stateWithCheck := apkUpdated.Run(llb.Shlex(checkUpgradable)).Root()

_, err := buildkit.ExtractFileFromState(ctx, am.config.Client, &stateWithCheck, updatesAvailableMarker)
if err != nil {
log.Info("No upgradable packages found for this image.")
return nil, nil, types.ErrNoUpdatesFound
}
}

var apkInstalled llb.State
Expand Down
61 changes: 37 additions & 24 deletions pkg/pkgmgr/dpkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/moby/buildkit/client/llb"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/project-copacetic/copacetic/pkg/buildkit"
"github.com/project-copacetic/copacetic/pkg/types"
"github.com/project-copacetic/copacetic/pkg/types/unversioned"
"github.com/project-copacetic/copacetic/pkg/utils"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -344,8 +345,18 @@ func (dm *dpkgManager) installUpdates(ctx context.Context, updates unversioned.U
llb.IgnoreCache,
).Root()

checkUpgradable := `sh -c "apt-get -s upgrade 2>/dev/null | grep -q "^Inst" || exit 1"`
aptGetUpdated = aptGetUpdated.Run(llb.Shlex(checkUpgradable)).Root()
// Only check for upgradable packages when updating all (no specific updates list).
if updates == nil {
const updatesAvailableMarker = "/updates.txt"
checkUpgradable := fmt.Sprintf(`sh -c 'if apt-get -s upgrade 2>/dev/null | grep -q "^Inst"; then touch %s; fi'`, updatesAvailableMarker)
aptGetUpdated = aptGetUpdated.Run(llb.Shlex(checkUpgradable)).Root()

_, err := buildkit.ExtractFileFromState(ctx, dm.config.Client, &aptGetUpdated, updatesAvailableMarker)
if err != nil {
log.Info("No upgradable packages found for this image.")
return nil, nil, types.ErrNoUpdatesFound
}
}

// detect held packages and log them
checkHeldCmd := `sh -c "apt-mark showhold | tee /held.txt"`
Expand Down Expand Up @@ -462,29 +473,31 @@ func (dm *dpkgManager) unpackAndMergeUpdates(ctx context.Context, updates unvers
llb.AddEnv("PACKAGES_PRESENT", string(jsonPackageData)),
llb.Args([]string{
`bash`, `-c`, `
json_str=$PACKAGES_PRESENT
update_packages=""

while IFS=':' read -r package version; do
pkg_name=$(echo "$package" | sed 's/^"\(.*\)"$/\1/')
pkg_version=$(echo "$version" | sed 's/^"\(.*\)"$/\1/')
latest_version=$(apt show $pkg_name 2>/dev/null | awk -F ': ' '/Version:/{print $2}')

if [ "$latest_version" != "$pkg_version" ]; then
update_packages="$update_packages $pkg_name"
fi
done <<< "$(echo "$json_str" | tr -d '{}\n' | tr ',' '\n')"

if [ -z "$update_packages" ]; then
echo "No packages to update"
exit 1
fi

mkdir /var/cache/apt/archives
cd /var/cache/apt/archives
echo "$update_packages" > packages.txt
`,
json_str=$PACKAGES_PRESENT
update_packages=""

while IFS=':' read -r package version; do
pkg_name=$(echo "$package" | sed 's/^"\(.*\)"$/\1/')
pkg_version=$(echo "$version" | sed 's/^"\(.*\)"$/\1/')
latest_version=$(apt show $pkg_name 2>/dev/null | awk -F ': ' '/Version:/{print $2}')

if [ "$latest_version" != "$pkg_version" ]; then
update_packages="$update_packages $pkg_name"
fi
done <<< "$(echo "$json_str" | tr -d '{}\n' | tr ',' '\n')"

if [ -n "$update_packages" ]; then
mkdir -p /var/cache/apt/archives
cd /var/cache/apt/archives
echo "$update_packages" > packages.txt
touch /updates.txt
fi
`,
})).Root()
if _, err := buildkit.ExtractFileFromState(ctx, dm.config.Client, &updated, "/updates.txt"); err != nil {
log.Info("No upgradable packages found for this image (distroless path).")
return nil, nil, types.ErrNoUpdatesFound
}
}

// Replace status file in tooling image with new status file with relevant pacakges from image to be patched.
Expand Down
61 changes: 35 additions & 26 deletions pkg/pkgmgr/rpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/moby/buildkit/client/llb"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/project-copacetic/copacetic/pkg/buildkit"
"github.com/project-copacetic/copacetic/pkg/types"
"github.com/project-copacetic/copacetic/pkg/types/unversioned"
"github.com/project-copacetic/copacetic/pkg/utils"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -489,25 +490,31 @@ func (rm *rpmManager) installUpdates(ctx context.Context, updates unversioned.Up
if dnfTooling == "" {
dnfTooling = rm.rpmTools["dnf"]
}
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'`
if !rm.checkForUpgrades(ctx, dnfTooling, checkUpdateTemplate) {
return nil, nil, fmt.Errorf("no patchable packages found")
if updates == nil {
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'`
if !rm.checkForUpgrades(ctx, dnfTooling, checkUpdateTemplate) {
return nil, nil, types.ErrNoUpdatesFound
}
}

const dnfInstallTemplate = `sh -c '%[1]s upgrade --refresh %[2]s -y && %[1]s clean all'`
installCmd = fmt.Sprintf(dnfInstallTemplate, dnfTooling, pkgs)
case rm.rpmTools["yum"] != "":
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'`
if !rm.checkForUpgrades(ctx, rm.rpmTools["yum"], checkUpdateTemplate) {
return nil, nil, fmt.Errorf("no patchable packages found")
if updates == nil {
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'`
if !rm.checkForUpgrades(ctx, rm.rpmTools["yum"], checkUpdateTemplate) {
return nil, nil, types.ErrNoUpdatesFound
}
}

const yumInstallTemplate = `sh -c '%[1]s upgrade %[2]s -y && %[1]s clean all'`
installCmd = fmt.Sprintf(yumInstallTemplate, rm.rpmTools["yum"], pkgs)
case rm.rpmTools["microdnf"] != "":
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;"`
if !rm.checkForUpgrades(ctx, rm.rpmTools["microdnf"], checkUpdateTemplate) {
return nil, nil, fmt.Errorf("no patchable packages found")
if updates == nil {
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;"`
if !rm.checkForUpgrades(ctx, rm.rpmTools["microdnf"], checkUpdateTemplate) {
return nil, nil, types.ErrNoUpdatesFound
}
}

const microdnfInstallTemplate = `sh -c '%[1]s update %[2]s -y && %[1]s clean all'`
Expand Down Expand Up @@ -617,28 +624,30 @@ func (rm *rpmManager) unpackAndMergeUpdates(ctx context.Context, updates unversi
llb.AddEnv("PACKAGES_PRESENT", string(jsonPackageData)),
llb.Args([]string{
`bash`, `-c`, `
json_str=$PACKAGES_PRESENT
update_packages=""

while IFS=':' read -r package version; do
pkg_name=$(echo "$package" | sed 's/^"\(.*\)"$/\1/')
json_str=$PACKAGES_PRESENT
update_packages=""

pkg_version=$(echo "$version" | sed 's/^"\(.*\)"$/\1/')
latest_version=$(yum list available $pkg_name 2>/dev/null | grep $pkg_name | tail -n 1 | tr -s ' ' | cut -d ' ' -f 2)
while IFS=':' read -r package version; do
pkg_name=$(echo "$package" | sed 's/^"\(.*\)"$/\1/')

if [ "$latest_version" != "$pkg_version" ]; then
update_packages="$update_packages $pkg_name"
fi
done <<< "$(echo "$json_str" | tr -d '{}\n' | tr ',' '\n')"
pkg_version=$(echo "$version" | sed 's/^"\(.*\)"$/\1/')
latest_version=$(yum list available $pkg_name 2>/dev/null | grep $pkg_name | tail -n 1 | tr -s ' ' | cut -d ' ' -f 2)

if [ -z "$update_packages" ]; then
echo "No packages to update"
exit 1
fi
if [ "$latest_version" != "$pkg_version" ]; then
update_packages="$update_packages $pkg_name"
fi
done <<< "$(echo "$json_str" | tr -d '{}\n' | tr ',' '\n')"

echo "$update_packages" > packages.txt
`,
if [ -n "$update_packages" ]; then
echo "$update_packages" > packages.txt
touch /updates.txt
fi
`,
})).Root()
if _, err := buildkit.ExtractFileFromState(ctx, rm.config.Client, &busyboxCopied, "/updates.txt"); err != nil {
log.Info("No upgradable packages found for this image (RPM distroless path).")
return nil, nil, types.ErrNoUpdatesFound
}
}

// Create a new state for tooling image with all the packages from the image we are trying to patch
Expand Down
6 changes: 6 additions & 0 deletions pkg/types/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package types

import "errors"

// ErrNoUpdatesFound indicates that no package updates are available for the image.
var ErrNoUpdatesFound = errors.New("no package updates found for image")
Loading