mirror of
https://github.com/containers/skopeo.git
synced 2026-05-18 13:03:16 +00:00
Merge pull request #2874 from aguidirh/add-platform-based-filtering
RUN-4630: copy: add platform-based filtering via --multi-arch flag
This commit is contained in:
@@ -78,7 +78,7 @@ See skopeo(1) section "IMAGE NAMES" for the expected format
|
||||
flags.StringSliceVar(&opts.additionalTags, "additional-tag", []string{}, "additional tags (supports docker-archive)")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress output information when copying images")
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Copy all images if SOURCE-IMAGE is a list")
|
||||
flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, or index-only)`)
|
||||
flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, index-only, or comma-separated platform list like linux/amd64,linux/arm64)`)
|
||||
flags.StringVar(&opts.signIdentity, "sign-identity", "", "Identity of signed image, must be a fully specified docker reference. Defaults to the target docker reference.")
|
||||
flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digest of the pushed image to the specified file")
|
||||
flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", []string{}, "*Experimental* key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)")
|
||||
@@ -89,23 +89,37 @@ See skopeo(1) section "IMAGE NAMES" for the expected format
|
||||
}
|
||||
|
||||
// parseMultiArch parses the list processing selection
|
||||
// It returns the copy.ImageListSelection to use with image.Copy option
|
||||
func parseMultiArch(multiArch string) (copy.ImageListSelection, error) {
|
||||
// It returns the copy.ImageListSelection to use with image.Copy option,
|
||||
// and optionally a list of platform filters if specific platforms were requested
|
||||
func parseMultiArch(multiArch string) (copy.ImageListSelection, []copy.InstancePlatformFilter, error) {
|
||||
switch multiArch {
|
||||
case "system":
|
||||
return copy.CopySystemImage, nil
|
||||
return copy.CopySystemImage, nil, nil
|
||||
case "all":
|
||||
return copy.CopyAllImages, nil
|
||||
return copy.CopyAllImages, nil, nil
|
||||
// There is no CopyNoImages value in copy.ImageListSelection, but because we
|
||||
// don't provide an option to select a set of images to copy, we can use
|
||||
// CopySpecificImages.
|
||||
case "index-only":
|
||||
return copy.CopySpecificImages, nil
|
||||
// We don't expose CopySpecificImages other than index-only above, because
|
||||
// we currently don't provide an option to choose the images to copy. That
|
||||
// could be added in the future.
|
||||
return copy.CopySpecificImages, nil, nil
|
||||
default:
|
||||
return copy.CopySystemImage, fmt.Errorf("unknown multi-arch option %q. Choose one of the supported options: 'system', 'all', or 'index-only'", multiArch)
|
||||
if !strings.Contains(multiArch, "/") {
|
||||
return copy.CopySystemImage, nil, fmt.Errorf("unknown multi-arch option %q. Choose one of the supported options: 'system', 'all', 'index-only', or a comma-separated platform list like 'linux/amd64,linux/arm64'", multiArch)
|
||||
}
|
||||
// Parse comma-separated platform list
|
||||
var platforms []copy.InstancePlatformFilter
|
||||
for platform := range strings.SplitSeq(multiArch, ",") {
|
||||
platform = strings.TrimSpace(platform)
|
||||
parts := strings.Split(platform, "/")
|
||||
if len(parts) != 2 {
|
||||
return copy.CopySystemImage, nil, fmt.Errorf("invalid platform format %q in --multi-arch, expected OS/Architecture (e.g., linux/amd64)", platform)
|
||||
}
|
||||
platforms = append(platforms, copy.InstancePlatformFilter{
|
||||
OS: parts[0],
|
||||
Architecture: parts[1],
|
||||
})
|
||||
}
|
||||
return copy.CopySpecificImages, platforms, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,11 +182,12 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
}
|
||||
|
||||
imageListSelection := copy.CopySystemImage
|
||||
var instancePlatforms []copy.InstancePlatformFilter
|
||||
if opts.multiArch.Present() && opts.all {
|
||||
return fmt.Errorf("Cannot use --all and --multi-arch flags together")
|
||||
}
|
||||
if opts.multiArch.Present() {
|
||||
imageListSelection, err = parseMultiArch(opts.multiArch.Value())
|
||||
imageListSelection, instancePlatforms, err = parseMultiArch(opts.multiArch.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -236,6 +251,7 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) {
|
||||
copyOpts.SourceCtx = sourceCtx
|
||||
copyOpts.DestinationCtx = destinationCtx
|
||||
copyOpts.ImageListSelection = imageListSelection
|
||||
copyOpts.InstancePlatforms = instancePlatforms
|
||||
copyOpts.OciDecryptConfig = decConfig
|
||||
copyOpts.OciEncryptLayers = encLayers
|
||||
copyOpts.OciEncryptConfig = encConfig
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.podman.io/image/v5/copy"
|
||||
)
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
// Invalid command-line arguments
|
||||
@@ -16,3 +22,95 @@ func TestCopy(t *testing.T) {
|
||||
// FIXME: Much more test coverage
|
||||
// Actual feature tests exist in integration and systemtest
|
||||
}
|
||||
|
||||
func TestParseMultiArch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedSelection copy.ImageListSelection
|
||||
expectedPlatforms []copy.InstancePlatformFilter
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "system option",
|
||||
input: "system",
|
||||
expectedSelection: copy.CopySystemImage,
|
||||
expectedPlatforms: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "all option",
|
||||
input: "all",
|
||||
expectedSelection: copy.CopyAllImages,
|
||||
expectedPlatforms: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "index-only option",
|
||||
input: "index-only",
|
||||
expectedSelection: copy.CopySpecificImages,
|
||||
expectedPlatforms: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "single platform",
|
||||
input: "linux/amd64",
|
||||
expectedSelection: copy.CopySpecificImages,
|
||||
expectedPlatforms: []copy.InstancePlatformFilter{
|
||||
{OS: "linux", Architecture: "amd64"},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "multiple platforms",
|
||||
input: "linux/amd64,linux/arm64",
|
||||
expectedSelection: copy.CopySpecificImages,
|
||||
expectedPlatforms: []copy.InstancePlatformFilter{
|
||||
{OS: "linux", Architecture: "amd64"},
|
||||
{OS: "linux", Architecture: "arm64"},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "platforms with whitespace",
|
||||
input: "linux/amd64, linux/arm64 , windows/amd64",
|
||||
expectedSelection: copy.CopySpecificImages,
|
||||
expectedPlatforms: []copy.InstancePlatformFilter{
|
||||
{OS: "linux", Architecture: "amd64"},
|
||||
{OS: "linux", Architecture: "arm64"},
|
||||
{OS: "windows", Architecture: "amd64"},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid option",
|
||||
input: "invalid",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid platform format - no slash",
|
||||
input: "linux-amd64",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid platform format - too many parts",
|
||||
input: "linux/amd64/extra",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selection, platforms, err := parseMultiArch(tt.input)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedSelection, selection)
|
||||
assert.Equal(t, tt.expectedPlatforms, platforms)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,11 @@ Options:
|
||||
- system: Copy only the image that matches the system architecture
|
||||
- all: Copy the full multi-architecture image
|
||||
- index-only: Copy only the index
|
||||
- _platform-list_: Copy only specific platforms (comma-separated list of OS/Architecture pairs, e.g., `linux/amd64,linux/arm64`)
|
||||
|
||||
The index-only option usually fails unless the referenced per-architecture images are already present in the destination, or the target registry supports sparse indexes.
|
||||
The index-only option and platform-list both create sparse manifest lists, which usually fail unless the referenced per-architecture images are already present in the destination, or the target registry supports sparse indexes.
|
||||
|
||||
When specifying a platform list, all compression variants and other variations for each platform are copied.
|
||||
|
||||
**--quiet**, **-q**
|
||||
|
||||
@@ -267,6 +270,11 @@ To copy and sign an image:
|
||||
$ skopeo copy --sign-by dev@example.com containers-storage:example/busybox:streaming docker://example/busybox:gold
|
||||
```
|
||||
|
||||
To copy only specific platforms from a multi-architecture image (creates a sparse manifest list):
|
||||
```console
|
||||
$ skopeo copy --multi-arch=linux/amd64,linux/arm64 docker://quay.io/skopeo/stable:latest docker://registry.example.com/skopeo:latest
|
||||
```
|
||||
|
||||
To encrypt an image:
|
||||
```console
|
||||
$ skopeo copy docker://docker.io/library/nginx:1.17.8 oci:local_nginx:1.17.8
|
||||
|
||||
@@ -187,6 +187,23 @@ func (s *copySuite) TestCopyNoneWithManifestList() {
|
||||
assert.Equal(t, "manifest.json\nversion\n", out)
|
||||
}
|
||||
|
||||
func (s *copySuite) TestCopyWithPlatformList() {
|
||||
t := s.T()
|
||||
dir1 := t.TempDir()
|
||||
assertSkopeoSucceeds(t, "", "copy", "--retry-times", "3", "--multi-arch=linux/amd64,linux/arm64", knownListImage, "dir:"+dir1)
|
||||
|
||||
manifestPath := filepath.Join(dir1, "manifest.json")
|
||||
readManifest, err := os.ReadFile(manifestPath)
|
||||
require.NoError(t, err)
|
||||
mimeType := manifest.GuessMIMEType(readManifest)
|
||||
assert.Equal(t, "application/vnd.docker.distribution.manifest.list.v2+json", mimeType)
|
||||
|
||||
// Verify that we copied exactly 2 platform manifests (manifest list + 2 platforms = 3 manifest files)
|
||||
manifestFiles, err := filepath.Glob(filepath.Join(dir1, "*manifest.json"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, len(manifestFiles), "Expected manifest list + 2 platform manifests")
|
||||
}
|
||||
|
||||
func (s *copySuite) TestCopyWithManifestListConverge() {
|
||||
t := s.T()
|
||||
oci1 := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user