-
Notifications
You must be signed in to change notification settings - Fork 18.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce a new `ImageConvert` operation to the image service (only supported by the containerd image store) to convert multi-platform image indexes. The intent behind adding a new operation is to make the reduction of a multi-platform image to a single-platform image explicit to the user and not force operation like push to create a new image index with a different image digest if user wants to operate only on a subset of the original multi-platform image. Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
- Loading branch information
Showing
10 changed files
with
380 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package client // import "github.com/docker/docker/client" | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/url" | ||
|
||
"github.com/containerd/containerd/platforms" | ||
"github.com/distribution/reference" | ||
"github.com/docker/docker/api/types/image" | ||
"github.com/docker/docker/errdefs" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// ImageList converts image. | ||
func (cli *Client) ImageConvert(ctx context.Context, src string, dst reference.NamedTagged, options image.ConvertOptions) error { | ||
query := url.Values{} | ||
|
||
if options.OnlyAvailablePlatforms && len(options.Platforms) > 0 { | ||
return errdefs.InvalidParameter(errors.New("specifying both explicit platform list and only-available-platforms is not allowed")) | ||
} | ||
|
||
if options.OnlyAvailablePlatforms { | ||
query.Set("only-available-platforms", "1") | ||
} | ||
|
||
if len(options.Platforms) > 0 { | ||
for _, p := range options.Platforms { | ||
query.Add("platforms", platforms.Format(p)) | ||
} | ||
} | ||
|
||
if options.NoAttestations { | ||
query.Set("no-attestations", "1") | ||
} | ||
|
||
query.Set("from", src) | ||
query.Set("to", dst.String()) | ||
|
||
serverResp, err := cli.post(ctx, "/images/convert", query, nil, nil) | ||
ensureReaderClosed(serverResp) | ||
|
||
if serverResp.statusCode != http.StatusCreated { | ||
return errdefs.NotFound(errors.Wrapf(err, "failed to convert image %v", src)) | ||
} | ||
|
||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package containerd | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"reflect" | ||
"time" | ||
|
||
"github.com/containerd/containerd/content" | ||
cerrdefs "github.com/containerd/containerd/errdefs" | ||
"github.com/containerd/containerd/images" | ||
"github.com/containerd/containerd/platforms" | ||
"github.com/containerd/log" | ||
"github.com/distribution/reference" | ||
imagetypes "github.com/docker/docker/api/types/image" | ||
"github.com/docker/docker/errdefs" | ||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
) | ||
|
||
func (i *ImageService) ImageConvert(ctx context.Context, src string, dst reference.NamedTagged, opts imagetypes.ConvertOptions) error { | ||
log.G(ctx).Debugf("converting: %+v", opts) | ||
|
||
srcImg, err := i.resolveImage(ctx, src) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if _, err := i.images.Get(ctx, dst.String()); !cerrdefs.IsNotFound(err) { | ||
err := i.images.Delete(ctx, dst.String(), images.SynchronousDelete()) | ||
if err != nil { | ||
return errdefs.System(fmt.Errorf("failed to delete existing image: %w", err)) | ||
} | ||
} | ||
|
||
if opts.OnlyAvailablePlatforms && len(opts.Platforms) > 0 { | ||
return errdefs.InvalidParameter(errors.New("specifying both explicit platform list and only-available-platforms is not allowed")) | ||
} | ||
|
||
srcMediaType := srcImg.Target.MediaType | ||
if !images.IsIndexType(srcMediaType) { | ||
return errdefs.InvalidParameter(errors.New("cannot convert non-index image")) | ||
} | ||
|
||
oldIndex, info, err := readIndex(ctx, i.content, srcImg.Target) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
newImg := srcImg | ||
newImg.Name = dst.String() | ||
|
||
n, err := i.convertManifests(ctx, srcImg, opts) | ||
if err != nil { | ||
return err | ||
} | ||
if n != nil { | ||
newIndex := oldIndex | ||
newIndex.Manifests = n | ||
target, err := storeJson(ctx, i.content, newIndex.MediaType, newIndex, info.Labels) | ||
if err != nil { | ||
return errdefs.System(fmt.Errorf("failed to write modified image target: %w", err)) | ||
} | ||
|
||
newImg.Target = target | ||
newImg.CreatedAt = time.Now() | ||
newImg.UpdatedAt = newImg.CreatedAt | ||
} | ||
|
||
if _, err := i.images.Create(ctx, newImg); err != nil { | ||
return errdefs.System(fmt.Errorf("failed to create image: %w", err)) | ||
} | ||
return nil | ||
} | ||
|
||
func (i *ImageService) convertManifests(ctx context.Context, srcImg images.Image, opts imagetypes.ConvertOptions) ([]ocispec.Descriptor, error) { | ||
changed := false | ||
pm := platforms.All | ||
if len(opts.Platforms) > 0 { | ||
pm = platforms.Any(opts.Platforms...) | ||
} | ||
|
||
var newManifests []ocispec.Descriptor | ||
walker := i.walkReachableImageManifests | ||
if opts.OnlyAvailablePlatforms { | ||
walker = i.walkImageManifests | ||
changed = true | ||
} | ||
|
||
err := walker(ctx, srcImg, func(m *ImageManifest) error { | ||
if opts.NoAttestations && m.IsAttestation() { | ||
return nil | ||
} | ||
|
||
mtarget := m.Target() | ||
mplatform, err := m.ImagePlatform(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
if !pm.Match(mplatform) { | ||
changed = true | ||
log.G(ctx).WithFields(log.Fields{ | ||
"platform": mplatform, | ||
"digest": mtarget.Digest, | ||
}).Debugf("skipping manifest %s due to platform mismatch", mtarget.Digest) | ||
return images.ErrSkipDesc | ||
} | ||
|
||
newManifests = append(newManifests, mtarget) | ||
|
||
return nil | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if changed { | ||
return newManifests, nil | ||
} | ||
|
||
return nil, nil | ||
} | ||
|
||
func diffObj(old, new any) map[string]any { | ||
oldBytes, _ := json.Marshal(old) | ||
newBytes, _ := json.Marshal(new) | ||
|
||
oldMap := make(map[string]any) | ||
newMap := make(map[string]any) | ||
|
||
_ = json.Unmarshal(oldBytes, &newMap) | ||
_ = json.Unmarshal(newBytes, &oldMap) | ||
|
||
diff := map[string]any{} | ||
for k, v := range oldMap { | ||
if reflect.DeepEqual(newMap[k], v) { | ||
diff[k] = v | ||
} | ||
} | ||
for k, v := range newMap { | ||
if reflect.DeepEqual(oldMap[k], v) { | ||
diff[k] = v | ||
} | ||
} | ||
|
||
return diff | ||
} | ||
|
||
func readIndex(ctx context.Context, store content.InfoReaderProvider, desc ocispec.Descriptor) (ocispec.Index, content.Info, error) { | ||
info, err := store.Info(ctx, desc.Digest) | ||
if err != nil { | ||
return ocispec.Index{}, content.Info{}, err | ||
} | ||
|
||
p, err := content.ReadBlob(ctx, store, desc) | ||
if err != nil { | ||
return ocispec.Index{}, info, err | ||
} | ||
|
||
var idx ocispec.Index | ||
if err := json.Unmarshal(p, &idx); err != nil { | ||
return ocispec.Index{}, info, err | ||
} | ||
|
||
return idx, info, nil | ||
} |
Oops, something went wrong.