|
1 | 1 | package copy |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "os" |
5 | 6 | "path/filepath" |
6 | 7 | "testing" |
7 | 8 |
|
8 | 9 | digest "github.com/opencontainers/go-digest" |
9 | 10 | "github.com/stretchr/testify/assert" |
10 | 11 | "github.com/stretchr/testify/require" |
| 12 | + "go.podman.io/image/v5/directory" |
11 | 13 | internalManifest "go.podman.io/image/v5/internal/manifest" |
12 | 14 | "go.podman.io/image/v5/pkg/compression" |
13 | 15 | ) |
14 | 16 |
|
| 17 | +const ( |
| 18 | + // Test manifest files (relative to ../internal/manifest/testdata/) |
| 19 | + ociManifestFile = "ociv1.manifest.json" |
| 20 | + ociIndexZstdFile = "oci1.index.zstd-selection.json" |
| 21 | +) |
| 22 | + |
15 | 23 | // Test `instanceOpCopy` cases. |
16 | 24 | func TestPrepareCopyInstancesforInstanceCopyCopy(t *testing.T) { |
17 | | - validManifest, err := os.ReadFile(filepath.Join("..", "internal", "manifest", "testdata", "oci1.index.zstd-selection.json")) |
| 25 | + validManifest, err := os.ReadFile(filepath.Join("..", "internal", "manifest", "testdata", ociIndexZstdFile)) |
18 | 26 | require.NoError(t, err) |
19 | 27 | list, err := internalManifest.ListFromBlob(validManifest, internalManifest.GuessMIMEType(validManifest)) |
20 | 28 | require.NoError(t, err) |
@@ -77,7 +85,7 @@ func TestPrepareCopyInstancesforInstanceCopyCopy(t *testing.T) { |
77 | 85 |
|
78 | 86 | // Test `instanceOpClone` cases. |
79 | 87 | func TestPrepareCopyInstancesforInstanceCopyClone(t *testing.T) { |
80 | | - validManifest, err := os.ReadFile(filepath.Join("..", "internal", "manifest", "testdata", "oci1.index.zstd-selection.json")) |
| 88 | + validManifest, err := os.ReadFile(filepath.Join("..", "internal", "manifest", "testdata", ociIndexZstdFile)) |
81 | 89 | require.NoError(t, err) |
82 | 90 | list, err := internalManifest.ListFromBlob(validManifest, internalManifest.GuessMIMEType(validManifest)) |
83 | 91 | require.NoError(t, err) |
@@ -194,3 +202,189 @@ func convertInstanceCopyToSimplerInstanceCopy(copies []instanceOp) []simplerInst |
194 | 202 | } |
195 | 203 | return res |
196 | 204 | } |
| 205 | + |
| 206 | +// TestStripOnlyListSignaturesValidation tests the validation logic for StripOnlyListSignatures |
| 207 | +// by actually calling copy.Image() with various option combinations. |
| 208 | +func TestStripOnlyListSignaturesValidation(t *testing.T) { |
| 209 | + tests := []struct { |
| 210 | + name string |
| 211 | + manifestFile string // Relative to testdata directory |
| 212 | + options *Options |
| 213 | + expectedError string |
| 214 | + }{ |
| 215 | + { |
| 216 | + name: "Invalid: StripOnlyListSignatures with single image (not manifest list)", |
| 217 | + manifestFile: ociManifestFile, |
| 218 | + options: &Options{ |
| 219 | + ImageListSelection: CopySpecificImages, |
| 220 | + SparseManifestListAction: StripSparseManifestList, |
| 221 | + StripOnlyListSignatures: true, |
| 222 | + }, |
| 223 | + expectedError: "StripOnlyListSignatures can only be used with manifest lists, not single images", |
| 224 | + }, |
| 225 | + { |
| 226 | + name: "Invalid: StripOnlyListSignatures with CopySystemImage", |
| 227 | + manifestFile: ociIndexZstdFile, |
| 228 | + options: &Options{ |
| 229 | + ImageListSelection: CopySystemImage, |
| 230 | + SparseManifestListAction: StripSparseManifestList, |
| 231 | + StripOnlyListSignatures: true, |
| 232 | + }, |
| 233 | + expectedError: "StripOnlyListSignatures can only be used with CopySpecificImages and SparseManifestListAction=StripSparseManifestList, not with CopySystemImage", |
| 234 | + }, |
| 235 | + { |
| 236 | + name: "Invalid: StripOnlyListSignatures with CopyAllImages", |
| 237 | + manifestFile: ociIndexZstdFile, |
| 238 | + options: &Options{ |
| 239 | + ImageListSelection: CopyAllImages, |
| 240 | + SparseManifestListAction: StripSparseManifestList, |
| 241 | + StripOnlyListSignatures: true, |
| 242 | + }, |
| 243 | + expectedError: "StripOnlyListSignatures can only be used with CopySpecificImages, not CopyAllImages", |
| 244 | + }, |
| 245 | + { |
| 246 | + name: "Invalid: StripOnlyListSignatures without StripSparseManifestList", |
| 247 | + manifestFile: ociIndexZstdFile, |
| 248 | + options: &Options{ |
| 249 | + ImageListSelection: CopySpecificImages, |
| 250 | + SparseManifestListAction: KeepSparseManifestList, |
| 251 | + StripOnlyListSignatures: true, |
| 252 | + }, |
| 253 | + expectedError: "StripOnlyListSignatures requires SparseManifestListAction=StripSparseManifestList", |
| 254 | + }, |
| 255 | + } |
| 256 | + |
| 257 | + for _, tt := range tests { |
| 258 | + t.Run(tt.name, func(t *testing.T) { |
| 259 | + // Load the appropriate manifest for this test case |
| 260 | + manifest, err := os.ReadFile(filepath.Join("..", "internal", "manifest", "testdata", tt.manifestFile)) |
| 261 | + require.NoError(t, err) |
| 262 | + |
| 263 | + // Set up source directory with the manifest |
| 264 | + srcDir := t.TempDir() |
| 265 | + srcManifestPath := filepath.Join(srcDir, "manifest.json") |
| 266 | + require.NoError(t, os.WriteFile(srcManifestPath, manifest, 0644)) |
| 267 | + |
| 268 | + // Set up destination directory |
| 269 | + destDir := t.TempDir() |
| 270 | + |
| 271 | + // Create source and destination references |
| 272 | + // Note: We use directory transport for simplicity, even though copy.Image |
| 273 | + // will fail later in the process. The validation we're testing happens |
| 274 | + // early in copy.Image() before it tries to actually copy data. |
| 275 | + srcRef, err := directory.NewReference(srcDir) |
| 276 | + require.NoError(t, err) |
| 277 | + destRef, err := directory.NewReference(destDir) |
| 278 | + require.NoError(t, err) |
| 279 | + |
| 280 | + // Call the real copy.Image() function |
| 281 | + _, err = Image(context.Background(), nil, destRef, srcRef, tt.options) |
| 282 | + |
| 283 | + // Verify the error matches expectations (all test cases in this function are invalid) |
| 284 | + require.Error(t, err, "Expected validation error from copy.Image()") |
| 285 | + assert.Equal(t, tt.expectedError, err.Error()) |
| 286 | + }) |
| 287 | + } |
| 288 | +} |
| 289 | + |
| 290 | +// TestStripSparseManifestListRequiresSignatureHandling tests that when using |
| 291 | +// StripSparseManifestList with a signed manifest list, the user must explicitly |
| 292 | +// choose how to handle signatures via RemoveSignatures or StripOnlyListSignatures. |
| 293 | +func TestStripSparseManifestListRequiresSignatureHandling(t *testing.T) { |
| 294 | + // Load a manifest list |
| 295 | + manifest, err := os.ReadFile(filepath.Join("..", "internal", "manifest", "testdata", ociIndexZstdFile)) |
| 296 | + require.NoError(t, err) |
| 297 | + |
| 298 | + tests := []struct { |
| 299 | + name string |
| 300 | + options *Options |
| 301 | + addSignature bool |
| 302 | + expectedError string |
| 303 | + }{ |
| 304 | + { |
| 305 | + name: "Valid: StripSparseManifestList with signed manifest + RemoveSignatures", |
| 306 | + options: &Options{ |
| 307 | + ImageListSelection: CopySpecificImages, |
| 308 | + Instances: []digest.Digest{digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, |
| 309 | + SparseManifestListAction: StripSparseManifestList, |
| 310 | + RemoveSignatures: true, |
| 311 | + }, |
| 312 | + addSignature: true, |
| 313 | + expectedError: "", |
| 314 | + }, |
| 315 | + { |
| 316 | + name: "Valid: StripSparseManifestList with signed manifest + StripOnlyListSignatures", |
| 317 | + options: &Options{ |
| 318 | + ImageListSelection: CopySpecificImages, |
| 319 | + Instances: []digest.Digest{digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, |
| 320 | + SparseManifestListAction: StripSparseManifestList, |
| 321 | + StripOnlyListSignatures: true, |
| 322 | + }, |
| 323 | + addSignature: true, |
| 324 | + expectedError: "", |
| 325 | + }, |
| 326 | + { |
| 327 | + name: "Invalid: StripSparseManifestList with signed manifest without signature handling", |
| 328 | + options: &Options{ |
| 329 | + ImageListSelection: CopySpecificImages, |
| 330 | + Instances: []digest.Digest{digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, |
| 331 | + SparseManifestListAction: StripSparseManifestList, |
| 332 | + }, |
| 333 | + addSignature: true, |
| 334 | + expectedError: "SparseManifestListAction.StripSparseManifestList will modify the signed manifest list; use RemoveSignatures to remove all signatures, or StripOnlyListSignatures to strip only the list signature while preserving per-instance signatures", |
| 335 | + }, |
| 336 | + { |
| 337 | + name: "Valid: StripSparseManifestList with unsigned manifest (no signature handling needed)", |
| 338 | + options: &Options{ |
| 339 | + ImageListSelection: CopySpecificImages, |
| 340 | + Instances: []digest.Digest{digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, |
| 341 | + SparseManifestListAction: StripSparseManifestList, |
| 342 | + }, |
| 343 | + addSignature: false, |
| 344 | + expectedError: "", |
| 345 | + }, |
| 346 | + } |
| 347 | + |
| 348 | + for _, tt := range tests { |
| 349 | + t.Run(tt.name, func(t *testing.T) { |
| 350 | + // Set up source directory with the manifest |
| 351 | + srcDir := t.TempDir() |
| 352 | + srcManifestPath := filepath.Join(srcDir, "manifest.json") |
| 353 | + require.NoError(t, os.WriteFile(srcManifestPath, manifest, 0644)) |
| 354 | + |
| 355 | + // Add a signature file if requested |
| 356 | + if tt.addSignature { |
| 357 | + // For directory transport, signatures are stored as "signature-1", "signature-2", etc. |
| 358 | + // Copy an existing signature file from testdata |
| 359 | + existingSignature, err := os.ReadFile(filepath.Join("..", "internal", "signature", "testdata", "simple.signature")) |
| 360 | + require.NoError(t, err) |
| 361 | + signaturePath := filepath.Join(srcDir, "signature-1") |
| 362 | + require.NoError(t, os.WriteFile(signaturePath, existingSignature, 0644)) |
| 363 | + } |
| 364 | + |
| 365 | + // Set up destination directory |
| 366 | + destDir := t.TempDir() |
| 367 | + |
| 368 | + // Create source and destination references |
| 369 | + srcRef, err := directory.NewReference(srcDir) |
| 370 | + require.NoError(t, err) |
| 371 | + destRef, err := directory.NewReference(destDir) |
| 372 | + require.NoError(t, err) |
| 373 | + |
| 374 | + // Call the real copy.Image() function |
| 375 | + _, err = Image(context.Background(), nil, destRef, srcRef, tt.options) |
| 376 | + |
| 377 | + // Verify the error matches expectations |
| 378 | + if tt.expectedError != "" { |
| 379 | + require.Error(t, err, "Expected validation error from copy.Image()") |
| 380 | + assert.Equal(t, tt.expectedError, err.Error()) |
| 381 | + } else { |
| 382 | + // Note: The copy may fail for other reasons (missing blobs, etc.) |
| 383 | + // but should not fail with the signature handling error |
| 384 | + if err != nil { |
| 385 | + assert.NotContains(t, err.Error(), "will modify the signed manifest list") |
| 386 | + } |
| 387 | + } |
| 388 | + }) |
| 389 | + } |
| 390 | +} |
0 commit comments