Skip to content

Commit 15ced60

Browse files
feat: support update all for multi platform patching (#1141)
Signed-off-by: ashnamehrotra <ashnamehrotra@gmail.com> Co-authored-by: Sertaç Özercan <852750+sozercan@users.noreply.github.com>
1 parent c990ce8 commit 15ced60

File tree

8 files changed

+315
-68
lines changed

8 files changed

+315
-68
lines changed

integration/singlearch/fixtures/test-images.json

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"digest": "sha256:42d3e6bc186572245aded5a0be381012adba6d89355fa9486dd81b0c634695b5",
66
"distro": "Alpine",
77
"description": "Valid apk/db, apk present",
8-
"ignoreErrors": false
8+
"ignoreErrors": false,
9+
"isManifestList": true
910
},
1011
{
1112
"image": "docker.io/grafana/grafana",
@@ -14,15 +15,17 @@
1415
"digest": "sha256:42d3e6bc186572245aded5a0be381012adba6d89355fa9486dd81b0c634695b5",
1516
"distro": "Alpine",
1617
"description": "Valid apk/db, apk present, locally tagged with fully-qualified name",
17-
"ignoreErrors": false
18+
"ignoreErrors": false,
19+
"isManifestList": false
1820
},
1921
{
2022
"image": "docker.io/library/nginx",
2123
"tag": "1.21.6",
2224
"digest": "sha256:2bcabc23b45489fb0885d69a06ba1d648aeda973fae7bb981bafbb884165e514",
2325
"distro": "Debian",
2426
"description": "Valid dpkg/status, apt present",
25-
"ignoreErrors": false
27+
"ignoreErrors": false,
28+
"isManifestList": true
2629
},
2730
{
2831
"image": "docker.io/library/nginx",
@@ -31,39 +34,44 @@
3134
"localName": "local/image:tag",
3235
"distro": "Debian",
3336
"description": "Valid dpkg/status, apt present, locally tagged with repo and image name",
34-
"ignoreErrors": false
37+
"ignoreErrors": false,
38+
"isManifestList": false
3539
},
3640
{
3741
"image": "registry.k8s.io/kube-proxy",
3842
"tag": "v1.23.4",
3943
"digest": "sha256:30116c7218264d95623d3918a50da703675755cae866cd4c324586611fcd50ea",
4044
"distro": "Debian",
4145
"description": "Valid dpkg/status, apt present, custom network config",
42-
"ignoreErrors": false
46+
"ignoreErrors": false,
47+
"isManifestList": true
4348
},
4449
{
4550
"image": "registry.k8s.io/kube-proxy",
4651
"tag": "v1.27.2",
4752
"digest": "sha256:1e4f13f5f5c215813fb9c9c6f56da1c0354363f2a69bd12732658f79d585864f",
4853
"distro": "Custom Google Distroless",
4954
"description": "Custom dpkg/status.d with text names, no apt, libssl1.1",
50-
"ignoreErrors": false
55+
"ignoreErrors": false,
56+
"isManifestList": true
5157
},
5258
{
5359
"image": "registry.k8s.io/kube-proxy",
5460
"tag": "v1.26.1",
5561
"digest": "sha256:1e4f13f5f5c215813fb9c9c6f56da1c0354363f2a69bd12732658f79d585864f",
5662
"distro": "Custom Google Distroless",
5763
"description": "Custom dpkg/status.d with text names, with both libssl1.1 and libssl1",
58-
"ignoreErrors": false
64+
"ignoreErrors": false,
65+
"isManifestList": true
5966
},
6067
{
6168
"image": "docker.io/fluent/fluent-bit",
6269
"tag": "1.8.4",
6370
"digest": "sha256:2d80c13c2e7e06aa6a2e54a1825c6adbb3829c8a133ff617a0a61790bd61c53d",
6471
"distro": "Google Distroless",
6572
"description": "Custom dpkg/status.d with base64 names",
66-
"ignoreErrors": false
73+
"ignoreErrors": false,
74+
"isManifestList": true
6775
},
6876
{
6977
"image": "docker.io/fluent/fluent-bit",
@@ -72,133 +80,150 @@
7280
"localName": "localimage:tag",
7381
"distro": "Google Distroless",
7482
"description": "Custom dpkg/status.d with base64 names, locally tagged with image name only",
75-
"ignoreErrors": false
83+
"ignoreErrors": false,
84+
"isManifestList": false
7685
},
7786
{
7887
"image": "docker.io/openpolicyagent/opa",
7988
"tag": "0.46.0",
8089
"digest": "sha256:c4b11c9b86eaba41276ae682bb6875332316242010b7523efe30f365ad0c3cb8",
8190
"distro": "Google Distroless",
8291
"description": "Custom dpkg/status.d with text names, no apt, libssl1",
83-
"ignoreErrors": false
92+
"ignoreErrors": false,
93+
"isManifestList": false
8494
},
8595
{
8696
"image": "quay.io/calico/cni",
8797
"tag": "v3.15.1",
8898
"digest": "sha256:a925b445c2688fc9c149b20ea04faabd40610d3304a6efda68e5dada7a41b813",
8999
"distro": "Redhat",
90100
"description": "Valid rpm DB, microdnf & rpm present",
91-
"ignoreErrors": false
101+
"ignoreErrors": false,
102+
"isManifestList": false
92103
},
93104
{
94105
"image": "mcr.microsoft.com/cbl-mariner/base/core",
95106
"tag": "2.0.20240112",
96107
"digest": "sha256:60323975ec3aabe1840920a65237950a54c5fef6ffc811a5d26bb6bd130f1cc3",
97108
"distro": "Mariner",
98109
"description": "Valid rpm DB, tdnf, yum & rpm present",
99-
"ignoreErrors": false
110+
"ignoreErrors": false,
111+
"isManifestList": true
100112
},
101113
{
102114
"image": "mcr.microsoft.com/cbl-mariner/base/core",
103115
"tag": "2.0.20240112-arm64",
104116
"digest": "sha256:c85680df0ddccfd5bf0cd60ff7d0c07b0ea783bcee9ce5dc748b68c0d36e280a",
105117
"distro": "Mariner",
106118
"description": "Valid rpm DB, tdnf, yum & rpm present, arm64 cross-arch",
107-
"ignoreErrors": false
119+
"ignoreErrors": false,
120+
"isManifestList": false
108121
},
109122
{
110123
"image": "mcr.microsoft.com/cbl-mariner/distroless/base",
111124
"tag": "2.0.20220527",
112125
"digest": "sha256:f550c5428df17b145851ad75983aca6d613ad4b51ca7983b2a83e67d0ac91a5d",
113126
"distro": "Mariner Distroless",
114127
"description": "Custom rpmmanifest files, no yum/tdnf/dnf/microdnf/rpm",
115-
"ignoreErrors": false
128+
"ignoreErrors": false,
129+
"isManifestList": true
116130
},
117131
{
118132
"image": "mcr.microsoft.com/azurelinux/base/core",
119133
"tag": "3.0.20240727",
120134
"digest": "sha256:02004412d6133fba772fd88dd45ea99b61258722bfc796c156937df4a5d75c6c",
121135
"distro": "Azure Linux",
122136
"description": "Valid rpm DB, tdnf & rpm present, no dnf or yum",
123-
"ignoreErrors": false
137+
"ignoreErrors": false,
138+
"isManifestList": true
124139
},
125140
{
126141
"image": "mcr.microsoft.com/azurelinux/base/core",
127142
"tag": "3.0.20240727-arm64",
128143
"digest": "sha256:5975d2ba45e7d256d4eb4e2b3df3aefbaddf25f14fa300fa126fb93b9f082d33",
129144
"distro": "Azure Linux",
130145
"description": "Valid rpm DB, tdnf & rpm present, no dnf or yum, arm64 cross-arch",
131-
"ignoreErrors": false
146+
"ignoreErrors": false,
147+
"isManifestList": false
132148
},
133149
{
134150
"image": "mcr.microsoft.com/azurelinux/distroless/base",
135151
"tag": "3.0.20240727",
136152
"digest": "sha256:50c24841324cdb36a268bb1288dd6f8bd5bcf19055c24f6aaa750a740a8be62d",
137153
"distro": "Azure Linux Distroless",
138154
"description": "Custom rpmmanifest files, no yum/tdnf/dnf/microdnf/rpm",
139-
"ignoreErrors": false
155+
"ignoreErrors": false,
156+
"isManifestList": true
140157
},
141158
{
142159
"image": "docker.io/library/amazonlinux",
143160
"tag": "2.0.20210326.0",
144161
"digest": "sha256:06380711d6a8ac0b6989f7e2a4419e560796791d9c7c843753a719c73552dc30",
145162
"distro": "Amazon Linux",
146163
"description": "Valid rpm DB, yum present",
147-
"ignoreErrors": false
164+
"ignoreErrors": false,
165+
"isManifestList": true
148166
},
149167
{
150168
"image": "docker.io/library/oraclelinux",
151169
"tag": "7.9",
152170
"digest": "sha256:ba39a0daabd2df95ed5f374d016e87513f8e579ecc5a1599d7cf94679a281a34",
153171
"distro": "Oracle Linux 7.9",
154172
"description": "Valid rpm DB, yum present",
155-
"ignoreErrors": false
173+
"ignoreErrors": false,
174+
"isManifestList": false
156175
},
157176
{
158177
"image": "docker.io/library/oraclelinux",
159178
"tag": "8.9",
160179
"digest": "sha256:67c889172b07b1f4067050abf4bcf7fce2febd280664df261fe17fa82501a498",
161180
"distro": "Oracle Linux 8.9",
162181
"description": "Valid rpm DB, yum present",
163-
"ignoreErrors": true
182+
"ignoreErrors": true,
183+
"isManifestList": true
164184
},
165185
{
166186
"image": "docker.io/library/rockylinux",
167187
"tag": "8.9.20231119",
168188
"digest": "sha256:69cecc7163282ad83e27b739fe8473b7c56e280e83827dcda60e5d37102457f1",
169189
"distro": "Rocky Linux",
170190
"description": "Valid rpm DB, yum present",
171-
"ignoreErrors": false
191+
"ignoreErrors": false,
192+
"isManifestList": false
172193
},
173194
{
174195
"image": "docker.io/redhat/ubi9-minimal",
175196
"tag": "9.4-949",
176197
"digest": "sha256:9607229894026ebecade4623038fce35bb75bf9371aca7b08ca11b08d103d2ab",
177198
"distro": "Redhat",
178-
"description": "Valid microdnf, no yum/dnf/rpm"
199+
"description": "Valid microdnf, no yum/dnf/rpm",
200+
"isManifestList": false
179201
},
180202
{
181203
"image": "docker.io/almalinux",
182204
"tag": "9.4-20240923",
183205
"digest": "sha256:1c718a4cd7bab3bdb069ddbbd1eb593a390e6932d51d0048a2f6556303bafba7",
184206
"distro": "AlmaLinux",
185207
"description": "Valid rpm DB, microdnf & rpm present",
186-
"ignoreErrors": false
208+
"ignoreErrors": false,
209+
"isManifestList": true
187210
},
188211
{
189212
"image": "ghcr.io/project-copacetic/copacetic/test/openssl",
190213
"tag": "test-rpm",
191214
"digest": "sha256:a3ce5460b21cefef782ffd5f8b751c18d6b15b96e81da26248bfab032b4db742",
192215
"distro": "Mariner Distroless",
193216
"description": "Custom rpmmanifest files, no yum/tdnf/dnf/microdnf/rpm, custom openssl.cnf",
194-
"ignoreErrors": false
217+
"ignoreErrors": false,
218+
"isManifestList": false
195219
},
196220
{
197221
"image": "ghcr.io/project-copacetic/copacetic/test/openssl",
198222
"tag": "test-debian",
199223
"digest": "sha256:453ce8b732afa551d2e8772d131d8b060b765a4532b33c1fc1e7a90569fb2fbb",
200224
"distro": "Google Distroless",
201225
"description": "Custom dpkg/status.d with text names, no apt, libssl1, custom openssl.cnf",
202-
"ignoreErrors": false
226+
"ignoreErrors": false,
227+
"isManifestList": false
203228
}
204229
]

integration/singlearch/patch_test.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ import (
2424
var testImages []byte
2525

2626
type testImage struct {
27-
Image string `json:"image"`
28-
Tag string `json:"tag"`
29-
LocalName string `json:"localName,omitempty"`
30-
Distro string `json:"distro"`
31-
Digest digest.Digest `json:"digest"`
32-
Description string `json:"description"`
33-
IgnoreErrors bool `json:"ignoreErrors"`
27+
Image string `json:"image"`
28+
Tag string `json:"tag"`
29+
LocalName string `json:"localName,omitempty"`
30+
Distro string `json:"distro"`
31+
Digest digest.Digest `json:"digest"`
32+
Description string `json:"description"`
33+
IgnoreErrors bool `json:"ignoreErrors"`
34+
IsManifestList bool `json:"isManifestList"`
3435
}
3536

3637
func TestPatch(t *testing.T) {
@@ -93,7 +94,6 @@ func TestPatch(t *testing.T) {
9394
require.NoError(t, err, err)
9495

9596
tagPatched := img.Tag + "-patched"
96-
patchedRef := fmt.Sprintf("%s:%s", r.Name(), tagPatched)
9797

9898
patchedMediaType, err := utils.GetMediaType(imageRef, imageloader.Docker)
9999
require.NoError(t, err)
@@ -107,6 +107,14 @@ func TestPatch(t *testing.T) {
107107
t.Log("patching image")
108108
patch(t, ref, tagPatched, dir, img.IgnoreErrors, reportFile)
109109

110+
// For no-report tests with manifest images, Copa creates platform-specific tags like "-patched-amd64"
111+
// The scanning should look for the tag that Copa actually created
112+
scanTag := tagPatched
113+
if !reportFile && img.IsManifestList {
114+
scanTag += "-amd64"
115+
}
116+
patchedRef := fmt.Sprintf("%s:%s", r.Name(), scanTag)
117+
110118
switch {
111119
case strings.Contains(img.Image, "oracle"):
112120
t.Log("Oracle image detected. Skipping Trivy scan.")
@@ -176,6 +184,11 @@ func patch(t *testing.T, ref, patchedTag, path string, ignoreErrors bool, report
176184
reportPath = "-r=" + path + "/scan.json"
177185
}
178186

187+
var platformFlag string
188+
if !reportFile {
189+
platformFlag = "--platform=linux/amd64"
190+
}
191+
179192
//#nosec G204
180193
cmd := exec.Command(
181194
copaPath,
@@ -186,6 +199,7 @@ func patch(t *testing.T, ref, patchedTag, path string, ignoreErrors bool, report
186199
"-s="+scannerPlugin,
187200
"--timeout=30m",
188201
addrFl,
202+
platformFlag,
189203
"--ignore-errors="+strconv.FormatBool(ignoreErrors),
190204
"--output="+path+"/vex.json",
191205
"--debug",

pkg/buildkit/buildkit.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ func DiscoverPlatformsFromReport(reportDir, scanner string) ([]types.PatchPlatfo
133133
Architecture: report.Metadata.Config.Arch,
134134
Variant: report.Metadata.Config.Variant,
135135
},
136-
ReportFile: filePath,
136+
ReportFile: filePath,
137+
ShouldPreserve: false, // This platform has a report, so it should be patched
137138
}
138139

139140
if platform.Architecture == arm64 && platform.Variant == "v8" {
@@ -198,6 +199,8 @@ func DiscoverPlatformsFromReference(manifestRef string) ([]types.PatchPlatform,
198199
OSVersion: m.Platform.OSVersion,
199200
OSFeatures: m.Platform.OSFeatures,
200201
},
202+
ReportFile: "", // No report file for platforms discovered from reference
203+
ShouldPreserve: false, // Default to false, will be set appropriately later
201204
}
202205
if m.Platform.Architecture == arm64 && m.Platform.Variant == "v8" {
203206
// some scanners may not add v8 to arm64 reports, so we
@@ -256,11 +259,13 @@ func DiscoverPlatforms(manifestRef, reportDir, scanner string) ([]types.PatchPla
256259
if rp, ok := reportSet[PlatformKey(pl.Platform)]; ok {
257260
// Platform has a report - will be patched
258261
pl.ReportFile = rp
262+
pl.ShouldPreserve = false
259263
platforms = append(platforms, pl)
260264
} else {
261265
// Platform has no report - preserve original without patching
262266
log.Debugf("No report found for platform %s, preserving original", PlatformKey(pl.Platform))
263267
pl.ReportFile = ""
268+
pl.ShouldPreserve = true
264269
platforms = append(platforms, pl)
265270
}
266271
}

pkg/patch/cmd.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type patchArgs struct {
2828
output string
2929
bkOpts buildkit.Opts
3030
push bool
31+
platform []string
3132
loader string
3233
}
3334

@@ -57,6 +58,7 @@ func NewPatchCmd() *cobra.Command {
5758
ua.loader,
5859
ua.ignoreError,
5960
ua.push,
61+
ua.platform,
6062
bkopts)
6163
},
6264
}
@@ -76,6 +78,10 @@ func NewPatchCmd() *cobra.Command {
7678
flags.StringVarP(&ua.format, "format", "f", "openvex", "Output format, defaults to 'openvex'")
7779
flags.StringVarP(&ua.output, "output", "o", "", "Output file path")
7880
flags.BoolVarP(&ua.push, "push", "p", false, "Push patched image to destination registry")
81+
flags.StringSliceVar(&ua.platform, "platform", nil,
82+
"Target platform(s) for multi-arch images when no report directory is provided (e.g., linux/amd64,linux/arm64). "+
83+
"Valid platforms: linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6. "+
84+
"If platform flag is used, only specified platforms are patched and the rest are preserved. If not specified, all platforms present in the image are patched.")
7985
flags.StringVarP(&ua.loader, "loader", "l", "", "Loader to use for loading images. Options: 'docker', 'podman', or empty for auto-detection based on buildkit address")
8086

8187
if err := patchCmd.MarkFlagRequired("image"); err != nil {

0 commit comments

Comments
 (0)