package manifests import ( "context" "encoding/json" stderrors "errors" "io" "github.com/containers/common/pkg/manifests" "github.com/containers/common/pkg/supplemented" cp "github.com/containers/image/v5/copy" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/image" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/signature" is "github.com/containers/image/v5/storage" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/types" "github.com/containers/storage" "github.com/containers/storage/pkg/lockfile" digest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) const instancesData = "instances.json" // ErrListImageUnknown is returned when we attempt to create an image reference // for a List that has not yet been saved to an image. var ErrListImageUnknown = stderrors.New("unable to determine which image holds the manifest list") type list struct { manifests.List instances map[digest.Digest]string } // List is a manifest list or image index, either created using Create(), or // loaded from local storage using LoadFromImage(). type List interface { manifests.List SaveToImage(store storage.Store, imageID string, names []string, mimeType string) (string, error) Reference(store storage.Store, multiple cp.ImageListSelection, instances []digest.Digest) (types.ImageReference, error) Push(ctx context.Context, dest types.ImageReference, options PushOptions) (reference.Canonical, digest.Digest, error) Add(ctx context.Context, sys *types.SystemContext, ref types.ImageReference, all bool) (digest.Digest, error) } // PushOptions includes various settings which are needed for pushing the // manifest list and its instances. type PushOptions struct { Store storage.Store SystemContext *types.SystemContext // github.com/containers/image/types.SystemContext ImageListSelection cp.ImageListSelection // set to either CopySystemImage, CopyAllImages, or CopySpecificImages Instances []digest.Digest // instances to copy if ImageListSelection == CopySpecificImages ReportWriter io.Writer // will be used to log the writing of the list and any blobs SignBy string // fingerprint of GPG key to use to sign images RemoveSignatures bool // true to discard signatures in images ManifestType string // the format to use when saving the list - possible options are oci, v2s1, and v2s2 } // Create creates a new list containing information about the specified image, // computing its manifest's digest, and retrieving OS and architecture // information from its configuration blob. Returns the new list, and the // instanceDigest for the initial image. func Create() List { return &list{ List: manifests.Create(), instances: make(map[digest.Digest]string), } } // LoadFromImage reads the manifest list or image index, and additional // information about where the various instances that it contains live, from an // image record with the specified ID in local storage. func LoadFromImage(store storage.Store, image string) (string, List, error) { img, err := store.Image(image) if err != nil { return "", nil, errors.Wrapf(err, "error locating image %q for loading manifest list", image) } manifestBytes, err := store.ImageBigData(img.ID, storage.ImageDigestManifestBigDataNamePrefix) if err != nil { return "", nil, errors.Wrapf(err, "error locating image %q for loading manifest list", image) } manifestList, err := manifests.FromBlob(manifestBytes) if err != nil { return "", nil, err } list := &list{ List: manifestList, instances: make(map[digest.Digest]string), } instancesBytes, err := store.ImageBigData(img.ID, instancesData) if err != nil { return "", nil, errors.Wrapf(err, "error locating image %q for loading instance list", image) } if err := json.Unmarshal(instancesBytes, &list.instances); err != nil { return "", nil, errors.Wrapf(err, "error decoding instance list for image %q", image) } list.instances[""] = img.ID return img.ID, list, err } // SaveToImage saves the manifest list or image index as the manifest of an // Image record with the specified names in local storage, generating a random // image ID if none is specified. It also stores information about where the // images whose manifests are included in the list can be found. func (l *list) SaveToImage(store storage.Store, imageID string, names []string, mimeType string) (string, error) { manifestBytes, err := l.List.Serialize(mimeType) if err != nil { return "", err } instancesBytes, err := json.Marshal(&l.instances) if err != nil { return "", err } img, err := store.CreateImage(imageID, names, "", "", &storage.ImageOptions{}) if err == nil || errors.Cause(err) == storage.ErrDuplicateID { created := (err == nil) if created { imageID = img.ID l.instances[""] = img.ID } err := store.SetImageBigData(imageID, storage.ImageDigestManifestBigDataNamePrefix, manifestBytes, manifest.Digest) if err != nil { if created { if _, err2 := store.DeleteImage(img.ID, true); err2 != nil { logrus.Errorf("Deleting image %q after failing to save manifest for it", img.ID) } } return "", errors.Wrapf(err, "saving manifest list to image %q", imageID) } err = store.SetImageBigData(imageID, instancesData, instancesBytes, nil) if err != nil { if created { if _, err2 := store.DeleteImage(img.ID, true); err2 != nil { logrus.Errorf("Deleting image %q after failing to save instance locations for it", img.ID) } } return "", errors.Wrapf(err, "saving instance list to image %q", imageID) } return imageID, nil } return "", errors.Wrapf(err, "error creating image to hold manifest list") } // Reference returns an image reference for the composite image being built // in the list, or an error if the list has never been saved to a local image. func (l *list) Reference(store storage.Store, multiple cp.ImageListSelection, instances []digest.Digest) (types.ImageReference, error) { if l.instances[""] == "" { return nil, errors.Wrap(ErrListImageUnknown, "error building reference to list") } s, err := is.Transport.ParseStoreReference(store, l.instances[""]) if err != nil { return nil, errors.Wrapf(err, "error creating ImageReference from image %q", l.instances[""]) } references := make([]types.ImageReference, 0, len(l.instances)) whichInstances := make([]digest.Digest, 0, len(l.instances)) switch multiple { case cp.CopyAllImages, cp.CopySystemImage: for instance := range l.instances { if instance != "" { whichInstances = append(whichInstances, instance) } } case cp.CopySpecificImages: for instance := range l.instances { for _, allowed := range instances { if instance == allowed { whichInstances = append(whichInstances, instance) } } } } for _, instance := range whichInstances { imageName := l.instances[instance] ref, err := alltransports.ParseImageName(imageName) if err != nil { return nil, errors.Wrapf(err, "error creating ImageReference from image %q", imageName) } references = append(references, ref) } return supplemented.Reference(s, references, multiple, instances), nil } // Push saves the manifest list and whichever blobs are needed to a destination location. func (l *list) Push(ctx context.Context, dest types.ImageReference, options PushOptions) (reference.Canonical, digest.Digest, error) { // Load the system signing policy. pushPolicy, err := signature.DefaultPolicy(options.SystemContext) if err != nil { return nil, "", errors.Wrapf(err, "error obtaining default signature policy") } // Override the settings for local storage to make sure that we can always read the source "image". pushPolicy.Transports[is.Transport.Name()] = storageAllowedPolicyScopes policyContext, err := signature.NewPolicyContext(pushPolicy) if err != nil { return nil, "", errors.Wrapf(err, "error creating new signature policy context") } defer func() { if err2 := policyContext.Destroy(); err2 != nil { logrus.Errorf("Destroying signature policy context: %v", err2) } }() // If we were given a media type that corresponds to a multiple-images // type, reset it to a valid corresponding single-image type, since we // already expect the image library to infer the list type from the // image type that we're telling it to force. singleImageManifestType := options.ManifestType switch singleImageManifestType { case v1.MediaTypeImageIndex: singleImageManifestType = v1.MediaTypeImageManifest case manifest.DockerV2ListMediaType: singleImageManifestType = manifest.DockerV2Schema2MediaType } // Build a source reference for our list and grab bag full of blobs. src, err := l.Reference(options.Store, options.ImageListSelection, options.Instances) if err != nil { return nil, "", err } copyOptions := &cp.Options{ ImageListSelection: options.ImageListSelection, Instances: options.Instances, SourceCtx: options.SystemContext, DestinationCtx: options.SystemContext, ReportWriter: options.ReportWriter, RemoveSignatures: options.RemoveSignatures, SignBy: options.SignBy, ForceManifestMIMEType: singleImageManifestType, } // Copy whatever we were asked to copy. manifestBytes, err := cp.Image(ctx, policyContext, dest, src, copyOptions) if err != nil { return nil, "", err } manifestDigest, err := manifest.Digest(manifestBytes) if err != nil { return nil, "", err } return nil, manifestDigest, nil } // Add adds information about the specified image to the list, computing the // image's manifest's digest, retrieving OS and architecture information from // the image's configuration, and recording the image's reference so that it // can be found at push-time. Returns the instanceDigest for the image. If // the reference points to an image list, either all instances are added (if // "all" is true), or the instance which matches "sys" (if "all" is false) will // be added. func (l *list) Add(ctx context.Context, sys *types.SystemContext, ref types.ImageReference, all bool) (digest.Digest, error) { src, err := ref.NewImageSource(ctx, sys) if err != nil { return "", errors.Wrapf(err, "error setting up to read manifest and configuration from %q", transports.ImageName(ref)) } defer src.Close() type instanceInfo struct { instanceDigest *digest.Digest OS, Architecture, OSVersion, Variant string Features, OSFeatures, Annotations []string Size int64 } var instanceInfos []instanceInfo var manifestDigest digest.Digest primaryManifestBytes, primaryManifestType, err := src.GetManifest(ctx, nil) if err != nil { return "", errors.Wrapf(err, "error reading manifest from %q", transports.ImageName(ref)) } if manifest.MIMETypeIsMultiImage(primaryManifestType) { lists, err := manifests.FromBlob(primaryManifestBytes) if err != nil { return "", errors.Wrapf(err, "error parsing manifest list in %q", transports.ImageName(ref)) } if all { for i, instance := range lists.OCIv1().Manifests { platform := instance.Platform if platform == nil { platform = &v1.Platform{} } instanceDigest := instance.Digest instanceInfo := instanceInfo{ instanceDigest: &instanceDigest, OS: platform.OS, Architecture: platform.Architecture, OSVersion: platform.OSVersion, Variant: platform.Variant, Features: append([]string{}, lists.Docker().Manifests[i].Platform.Features...), OSFeatures: append([]string{}, platform.OSFeatures...), Size: instance.Size, } instanceInfos = append(instanceInfos, instanceInfo) } } else { list, err := manifest.ListFromBlob(primaryManifestBytes, primaryManifestType) if err != nil { return "", errors.Wrapf(err, "error parsing manifest list in %q", transports.ImageName(ref)) } instanceDigest, err := list.ChooseInstance(sys) if err != nil { return "", errors.Wrapf(err, "error selecting image from manifest list in %q", transports.ImageName(ref)) } added := false for i, instance := range lists.OCIv1().Manifests { if instance.Digest != instanceDigest { continue } platform := instance.Platform if platform == nil { platform = &v1.Platform{} } instanceInfo := instanceInfo{ instanceDigest: &instanceDigest, OS: platform.OS, Architecture: platform.Architecture, OSVersion: platform.OSVersion, Variant: platform.Variant, Features: append([]string{}, lists.Docker().Manifests[i].Platform.Features...), OSFeatures: append([]string{}, platform.OSFeatures...), Size: instance.Size, } instanceInfos = append(instanceInfos, instanceInfo) added = true } if !added { instanceInfo := instanceInfo{ instanceDigest: &instanceDigest, } instanceInfos = append(instanceInfos, instanceInfo) } } } else { instanceInfo := instanceInfo{ instanceDigest: nil, } instanceInfos = append(instanceInfos, instanceInfo) } for _, instanceInfo := range instanceInfos { if instanceInfo.OS == "" || instanceInfo.Architecture == "" { img, err := image.FromUnparsedImage(ctx, sys, image.UnparsedInstance(src, instanceInfo.instanceDigest)) if err != nil { return "", errors.Wrapf(err, "error reading configuration blob from %q", transports.ImageName(ref)) } config, err := img.OCIConfig(ctx) if err != nil { return "", errors.Wrapf(err, "error reading info about config blob from %q", transports.ImageName(ref)) } if instanceInfo.OS == "" { instanceInfo.OS = config.OS } if instanceInfo.Architecture == "" { instanceInfo.Architecture = config.Architecture } } manifestBytes, manifestType, err := src.GetManifest(ctx, instanceInfo.instanceDigest) if err != nil { return "", errors.Wrapf(err, "error reading manifest from %q, instance %q", transports.ImageName(ref), instanceInfo.instanceDigest) } if instanceInfo.instanceDigest == nil { manifestDigest, err = manifest.Digest(manifestBytes) if err != nil { return "", errors.Wrapf(err, "error computing digest of manifest from %q", transports.ImageName(ref)) } instanceInfo.instanceDigest = &manifestDigest instanceInfo.Size = int64(len(manifestBytes)) } else { if manifestDigest == "" { manifestDigest = *instanceInfo.instanceDigest } } err = l.List.AddInstance(*instanceInfo.instanceDigest, instanceInfo.Size, manifestType, instanceInfo.OS, instanceInfo.Architecture, instanceInfo.OSVersion, instanceInfo.OSFeatures, instanceInfo.Variant, instanceInfo.Features, instanceInfo.Annotations) if err != nil { return "", errors.Wrapf(err, "error adding instance with digest %q", *instanceInfo.instanceDigest) } if _, ok := l.instances[*instanceInfo.instanceDigest]; !ok { l.instances[*instanceInfo.instanceDigest] = transports.ImageName(ref) } } return manifestDigest, nil } // Remove filters out any instances in the list which match the specified digest. func (l *list) Remove(instanceDigest digest.Digest) error { err := l.List.Remove(instanceDigest) if err == nil { if _, needToDelete := l.instances[instanceDigest]; needToDelete { delete(l.instances, instanceDigest) } } return err } // LockerForImage returns a Locker for a given image record. It's recommended // that processes which use LoadFromImage() to load a list from an image and // then use that list's SaveToImage() method to save a modified version of the // list to that image record use this lock to avoid accidentally wiping out // changes that another process is also attempting to make. func LockerForImage(store storage.Store, image string) (lockfile.Locker, error) { img, err := store.Image(image) if err != nil { return nil, errors.Wrapf(err, "locating image %q for locating lock", image) } d := digest.NewDigestFromEncoded(digest.Canonical, img.ID) if err := d.Validate(); err != nil { return nil, errors.Wrapf(err, "coercing image ID for %q into a digest", image) } return store.GetDigestLock(d) }