mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
Merge pull request #49586 from vvoland/image-inspect-platform
image/inspect: Add platform selection
This commit is contained in:
@@ -341,8 +341,22 @@ func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWrite
|
||||
manifests = httputils.BoolValue(r, "manifests")
|
||||
}
|
||||
|
||||
var platform *ocispec.Platform
|
||||
if r.Form.Get("platform") != "" && versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.49") {
|
||||
p, err := httputils.DecodePlatform(r.Form.Get("platform"))
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
}
|
||||
platform = p
|
||||
}
|
||||
|
||||
if manifests && platform != nil {
|
||||
return errdefs.InvalidParameter(errors.New("conflicting options: manifests and platform options cannot both be set"))
|
||||
}
|
||||
|
||||
imageInspect, err := ir.backend.ImageInspect(ctx, vars["name"], backend.ImageInspectOpts{
|
||||
Manifests: manifests,
|
||||
Platform: platform,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -153,6 +153,7 @@ type GetImageOpts struct {
|
||||
// ImageInspectOpts holds parameters to inspect an image.
|
||||
type ImageInspectOpts struct {
|
||||
Manifests bool
|
||||
Platform *ocispec.Platform
|
||||
}
|
||||
|
||||
// CommitConfig is the configuration for creating an image as part of a build.
|
||||
|
||||
@@ -128,11 +128,12 @@ type InspectResponse struct {
|
||||
// compatibility.
|
||||
Descriptor *ocispec.Descriptor `json:"Descriptor,omitempty"`
|
||||
|
||||
// Manifests is a list of image manifests available in this image. It
|
||||
// Manifests is a list of image manifests available in this image. It
|
||||
// provides a more detailed view of the platform-specific image manifests or
|
||||
// other image-attached data like build attestations.
|
||||
//
|
||||
// Only available if the daemon provides a multi-platform image store.
|
||||
// Only available if the daemon provides a multi-platform image store, the client
|
||||
// requests manifests AND does not request a specific platform.
|
||||
//
|
||||
// WARNING: This is experimental and may change at any time without any backward
|
||||
// compatibility.
|
||||
|
||||
@@ -106,6 +106,11 @@ type LoadOptions struct {
|
||||
type InspectOptions struct {
|
||||
// Manifests returns the image manifests.
|
||||
Manifests bool
|
||||
|
||||
// Platform selects the specific platform of a multi-platform image to inspect.
|
||||
//
|
||||
// This option is only available for API version 1.49 and up.
|
||||
Platform *ocispec.Platform
|
||||
}
|
||||
|
||||
// SaveOptions holds parameters to save images.
|
||||
|
||||
@@ -32,6 +32,17 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts
|
||||
query.Set("manifests", "1")
|
||||
}
|
||||
|
||||
if opts.apiOptions.Platform != nil {
|
||||
if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil {
|
||||
return image.InspectResponse{}, err
|
||||
}
|
||||
platform, err := encodePlatform(opts.apiOptions.Platform)
|
||||
if err != nil {
|
||||
return image.InspectResponse{}, err
|
||||
}
|
||||
query.Set("platform", platform)
|
||||
}
|
||||
|
||||
resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil)
|
||||
defer ensureReaderClosed(resp)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// ImageInspectOption is a type representing functional options for the image inspect operation.
|
||||
@@ -36,6 +37,17 @@ func ImageInspectWithManifests(manifests bool) ImageInspectOption {
|
||||
})
|
||||
}
|
||||
|
||||
// ImageInspectWithPlatform sets platform API option for the image inspect operation.
|
||||
// This option is only available for API version 1.49 and up.
|
||||
// With this option set, the image inspect operation will return information for the
|
||||
// specified platform variant of the multi-platform image.
|
||||
func ImageInspectWithPlatform(platform *ocispec.Platform) ImageInspectOption {
|
||||
return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
|
||||
clientOpts.apiOptions.Platform = platform
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ImageInspectWithAPIOpts sets the API options for the image inspect operation.
|
||||
func ImageInspectWithAPIOpts(opts image.InspectOptions) ImageInspectOption {
|
||||
return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/errdefs"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@@ -79,3 +80,47 @@ func TestImageInspect(t *testing.T) {
|
||||
t.Fatalf("expected `%v`, got %v", expectedTags, imageInspect.RepoTags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageInspectWithPlatform(t *testing.T) {
|
||||
expectedURL := "/images/image_id/json"
|
||||
requestedPlatform := &ocispec.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "arm64",
|
||||
}
|
||||
|
||||
expectedPlatform, err := encodePlatform(requestedPlatform)
|
||||
assert.NilError(t, err)
|
||||
|
||||
client := &Client{
|
||||
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
||||
if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
||||
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
||||
}
|
||||
|
||||
// Check if platform parameter is passed correctly
|
||||
platform := req.URL.Query().Get("platform")
|
||||
if platform != expectedPlatform {
|
||||
return nil, fmt.Errorf("Expected platform '%s', got '%s'", expectedPlatform, platform)
|
||||
}
|
||||
|
||||
content, err := json.Marshal(image.InspectResponse{
|
||||
ID: "image_id",
|
||||
Architecture: "arm64",
|
||||
Os: "linux",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(content)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
imageInspect, err := client.ImageInspect(context.Background(), "image_id", ImageInspectWithPlatform(requestedPlatform))
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, imageInspect.ID, "image_id")
|
||||
assert.Equal(t, imageInspect.Architecture, "arm64")
|
||||
assert.Equal(t, imageInspect.Os, "linux")
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ import (
|
||||
)
|
||||
|
||||
func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, opts backend.ImageInspectOpts) (*imagetypes.InspectResponse, error) {
|
||||
// TODO: Pass in opts
|
||||
var requestedPlatform *ocispec.Platform
|
||||
requestedPlatform := opts.Platform
|
||||
|
||||
c8dImg, err := i.resolveImage(ctx, refOrID)
|
||||
if err != nil {
|
||||
@@ -60,7 +59,6 @@ func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, opts ba
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//nolint:govet // TODO: requestedPlatform is always nil, but should be passed by the caller
|
||||
if multi.Best == nil && requestedPlatform != nil {
|
||||
return nil, &errPlatformNotFound{
|
||||
imageRef: refOrID,
|
||||
@@ -87,6 +85,10 @@ func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, opts ba
|
||||
|
||||
repoTags, repoDigests := collectRepoTagsAndDigests(ctx, tagged)
|
||||
|
||||
if requestedPlatform != nil {
|
||||
target = multi.Best.Target()
|
||||
}
|
||||
|
||||
resp := &imagetypes.InspectResponse{
|
||||
ID: target.Digest.String(),
|
||||
RepoTags: repoTags,
|
||||
|
||||
@@ -61,4 +61,38 @@ func TestImageInspect(t *testing.T) {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inspect image with platform parameter", func(t *testing.T) {
|
||||
ctx := logtest.WithT(ctx, t)
|
||||
service := fakeImageService(t, ctx, cs)
|
||||
|
||||
multiPlatformImage := toContainerdImage(t, func(dir string) (*ocispec.Index, error) {
|
||||
idx, _, err := specialimage.MultiPlatform(dir, "multiplatform:latest", []ocispec.Platform{
|
||||
{OS: "linux", Architecture: "amd64"},
|
||||
{OS: "linux", Architecture: "arm64"},
|
||||
})
|
||||
return idx, err
|
||||
})
|
||||
|
||||
_, err := service.images.Create(ctx, multiPlatformImage)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Test with amd64 platform
|
||||
amd64Platform := &ocispec.Platform{OS: "linux", Architecture: "amd64"}
|
||||
inspectAmd64, err := service.ImageInspect(ctx, multiPlatformImage.Name, backend.ImageInspectOpts{
|
||||
Platform: amd64Platform,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, inspectAmd64.Architecture, "amd64")
|
||||
assert.Equal(t, inspectAmd64.Os, "linux")
|
||||
|
||||
// Test with arm64 platform
|
||||
arm64Platform := &ocispec.Platform{OS: "linux", Architecture: "arm64"}
|
||||
inspectArm64, err := service.ImageInspect(ctx, multiPlatformImage.Name, backend.ImageInspectOpts{
|
||||
Platform: arm64Platform,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, inspectArm64.Architecture, "arm64")
|
||||
assert.Equal(t, inspectArm64.Os, "linux")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"github.com/docker/docker/layer"
|
||||
)
|
||||
|
||||
func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, _ backend.ImageInspectOpts) (*imagetypes.InspectResponse, error) {
|
||||
img, err := i.GetImage(ctx, refOrID, backend.GetImageOpts{})
|
||||
func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, opts backend.ImageInspectOpts) (*imagetypes.InspectResponse, error) {
|
||||
img, err := i.GetImage(ctx, refOrID, backend.GetImageOpts{Platform: opts.Platform})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ keywords: "API, Docker, rcli, REST, documentation"
|
||||
|
||||
[Docker Engine API v1.49](https://docs.docker.com/reference/api/engine/version/v1.49/) documentation
|
||||
|
||||
* `GET /images/{name}/json` now supports a `platform` parameter (JSON
|
||||
encoded OCI Platform type) allowing to specify a platform of the multi-platform
|
||||
image to inspect.
|
||||
This option is mutually exclusive with the `manifests` option.
|
||||
|
||||
## v1.48 API changes
|
||||
|
||||
[Docker Engine API v1.48](https://docs.docker.com/reference/api/engine/version/v1.48/) documentation
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/internal/testutils/specialimage"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/skip"
|
||||
@@ -80,3 +81,123 @@ func TestImageInspectDescriptor(t *testing.T) {
|
||||
assert.Check(t, inspect.Descriptor.Digest.String() == inspect.ID)
|
||||
assert.Check(t, inspect.Descriptor.Size > 0)
|
||||
}
|
||||
|
||||
func TestImageInspectWithPlatform(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "The test image is a Linux image")
|
||||
ctx := setupTest(t)
|
||||
|
||||
apiClient := testEnv.APIClient()
|
||||
|
||||
nativePlatform := ocispec.Platform{
|
||||
OS: testEnv.DaemonInfo.OSType,
|
||||
Architecture: testEnv.DaemonInfo.Architecture,
|
||||
}
|
||||
|
||||
// Create a platform that does not match the host platform
|
||||
differentOS := "linux"
|
||||
if nativePlatform.OS == "linux" {
|
||||
differentOS = "windows"
|
||||
}
|
||||
differentPlatform := ocispec.Platform{
|
||||
OS: differentOS,
|
||||
Architecture: "amd64",
|
||||
}
|
||||
|
||||
imageID := specialimage.Load(ctx, t, apiClient, func(dir string) (*ocispec.Index, error) {
|
||||
i, descs, err := specialimage.MultiPlatform(dir, "multiplatform:latest", []ocispec.Platform{nativePlatform, differentPlatform})
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = specialimage.LegacyManifest(dir, "multiplatform:latest", descs[0])
|
||||
assert.NilError(t, err)
|
||||
|
||||
return i, err
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
requestedPlatform *ocispec.Platform
|
||||
expectedPlatform *ocispec.Platform
|
||||
expectedError string
|
||||
withManifests bool
|
||||
snapshotterOnly bool
|
||||
graphdriverOnly bool
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
requestedPlatform: nil,
|
||||
expectedPlatform: &nativePlatform,
|
||||
},
|
||||
{
|
||||
name: "snapshotter/with-manifests",
|
||||
requestedPlatform: nil,
|
||||
expectedPlatform: &nativePlatform,
|
||||
snapshotterOnly: true,
|
||||
withManifests: true,
|
||||
},
|
||||
{
|
||||
name: "native",
|
||||
requestedPlatform: &nativePlatform,
|
||||
expectedPlatform: &nativePlatform,
|
||||
},
|
||||
{
|
||||
name: "different",
|
||||
requestedPlatform: &differentPlatform,
|
||||
expectedPlatform: &differentPlatform,
|
||||
snapshotterOnly: true,
|
||||
},
|
||||
{
|
||||
name: "different not supported on graphdriver",
|
||||
requestedPlatform: &differentPlatform,
|
||||
graphdriverOnly: true,
|
||||
// image with reference multiplatform:latest was found but its platform (linux/aarch64) does not match the specified platform (windows/amd64)
|
||||
expectedError: "image with reference multiplatform:latest was found but its platform",
|
||||
},
|
||||
} {
|
||||
if tc.snapshotterOnly && !testEnv.UsingSnapshotter() {
|
||||
continue
|
||||
}
|
||||
if tc.graphdriverOnly && testEnv.UsingSnapshotter() {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var opts []client.ImageInspectOption
|
||||
if tc.requestedPlatform != nil {
|
||||
opts = append(opts, client.ImageInspectWithPlatform(tc.requestedPlatform))
|
||||
}
|
||||
if tc.withManifests {
|
||||
opts = append(opts, client.ImageInspectWithManifests(true))
|
||||
}
|
||||
inspect, err := apiClient.ImageInspect(ctx, imageID, opts...)
|
||||
if tc.expectedError != "" {
|
||||
assert.Assert(t, is.ErrorContains(err, tc.expectedError))
|
||||
return
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Check(t, is.Equal(inspect.Architecture, tc.expectedPlatform.Architecture))
|
||||
assert.Check(t, is.Equal(inspect.Os, tc.expectedPlatform.OS))
|
||||
|
||||
if testEnv.UsingSnapshotter() {
|
||||
assert.Assert(t, inspect.Descriptor != nil)
|
||||
if tc.requestedPlatform != nil {
|
||||
if assert.Check(t, inspect.Descriptor.Platform != nil) {
|
||||
assert.Check(t, is.DeepEqual(*inspect.Descriptor.Platform, *tc.expectedPlatform))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Check(t, inspect.Descriptor == nil)
|
||||
}
|
||||
|
||||
if tc.withManifests {
|
||||
t.Run("has manifests", func(t *testing.T) {
|
||||
assert.Check(t, is.Len(inspect.Manifests, 2))
|
||||
})
|
||||
} else {
|
||||
t.Run("has no manifests", func(t *testing.T) {
|
||||
assert.Check(t, is.Nil(inspect.Manifests))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func MultiPlatform(dir string, imageRef string, imagePlatforms []ocispec.Platfor
|
||||
|
||||
for _, platform := range imagePlatforms {
|
||||
ps := platforms.Format(platform)
|
||||
manifestDesc, err := oneLayerPlatformManifest(dir, platform, FileInLayer{Path: "bash", Content: []byte("layer-" + ps)})
|
||||
manifestDesc, _, err := oneLayerPlatformManifest(dir, platform, FileInLayer{Path: "bash", Content: []byte("layer-" + ps)})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func PartialMultiPlatform(dir string, imageRef string, opts PartialOpts) (*ocisp
|
||||
|
||||
for _, platform := range opts.Stored {
|
||||
ps := platforms.Format(platform)
|
||||
manifestDesc, err := oneLayerPlatformManifest(dir, platform, FileInLayer{Path: "bash", Content: []byte("layer-" + ps)})
|
||||
manifestDesc, _, err := oneLayerPlatformManifest(dir, platform, FileInLayer{Path: "bash", Content: []byte("layer-" + ps)})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package specialimage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
@@ -75,3 +78,32 @@ func blobPaths(descriptors []ocispec.Descriptor) []string {
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func readJson(path string, v any) error {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(content, v)
|
||||
}
|
||||
|
||||
func LegacyManifest(dir string, imageRef string, mfstDesc ocispec.Descriptor) error {
|
||||
legacyManifests := []manifestItem{}
|
||||
|
||||
var mfst ocispec.Manifest
|
||||
if err := readJson(filepath.Join(dir, blobPath(mfstDesc)), &mfst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacyManifests = append(legacyManifests, manifestItem{
|
||||
Config: blobPath(mfst.Config),
|
||||
RepoTags: []string{imageRef},
|
||||
Layers: blobPaths(mfst.Layers),
|
||||
})
|
||||
|
||||
if err := writeJson(legacyManifests, filepath.Join(dir, "manifest.json")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ func TwoPlatform(dir string) (*ocispec.Index, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest1Desc, err := oneLayerPlatformManifest(dir, platforms.MustParse("linux/amd64"), FileInLayer{Path: "bash", Content: []byte("layer1")})
|
||||
manifest1Desc, _, err := oneLayerPlatformManifest(dir, platforms.MustParse("linux/amd64"), FileInLayer{Path: "bash", Content: []byte("layer1")})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest2Desc, err := oneLayerPlatformManifest(dir, platforms.MustParse("linux/arm64"), FileInLayer{Path: "bash", Content: []byte("layer2")})
|
||||
manifest2Desc, _, err := oneLayerPlatformManifest(dir, platforms.MustParse("linux/arm64"), FileInLayer{Path: "bash", Content: []byte("layer2")})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,13 +40,13 @@ type FileInLayer struct {
|
||||
Content []byte
|
||||
}
|
||||
|
||||
func oneLayerPlatformManifest(dir string, platform ocispec.Platform, f FileInLayer) (ocispec.Descriptor, error) {
|
||||
func oneLayerPlatformManifest(dir string, platform ocispec.Platform, f FileInLayer) (ocispec.Descriptor, manifestItem, error) {
|
||||
layerDesc, err := writeLayerWithOneFile(dir, f.Path, f.Content)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
return ocispec.Descriptor{}, manifestItem{}, err
|
||||
}
|
||||
|
||||
configDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{
|
||||
img := ocispec.Image{
|
||||
Platform: platform,
|
||||
Config: ocispec.ImageConfig{
|
||||
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
|
||||
@@ -55,9 +55,11 @@ func oneLayerPlatformManifest(dir string, platform ocispec.Platform, f FileInLay
|
||||
Type: "layers",
|
||||
DiffIDs: []digest.Digest{layerDesc.Digest},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
configDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, img)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
return ocispec.Descriptor{}, manifestItem{}, err
|
||||
}
|
||||
|
||||
manifestDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageManifest, ocispec.Manifest{
|
||||
@@ -66,11 +68,14 @@ func oneLayerPlatformManifest(dir string, platform ocispec.Platform, f FileInLay
|
||||
Layers: []ocispec.Descriptor{layerDesc},
|
||||
})
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
return ocispec.Descriptor{}, manifestItem{}, err
|
||||
}
|
||||
manifestDesc.Platform = &platform
|
||||
|
||||
return manifestDesc, nil
|
||||
return manifestDesc, manifestItem{
|
||||
Config: blobPath(configDesc),
|
||||
Layers: []string{blobPath(layerDesc)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func multiPlatformImage(dir string, ref reference.Named, target ocispec.Index) (*ocispec.Index, error) {
|
||||
|
||||
Reference in New Issue
Block a user