825 lines
27 KiB
Go
825 lines
27 KiB
Go
//go:build !remote
|
|
|
|
package libimage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/containers/common/libimage/define"
|
|
"github.com/containers/common/libimage/manifests"
|
|
manifesterrors "github.com/containers/common/pkg/manifests"
|
|
"github.com/containers/common/pkg/supplemented"
|
|
imageCopy "github.com/containers/image/v5/copy"
|
|
"github.com/containers/image/v5/docker"
|
|
"github.com/containers/image/v5/image"
|
|
"github.com/containers/image/v5/manifest"
|
|
"github.com/containers/image/v5/oci/layout"
|
|
"github.com/containers/image/v5/signature"
|
|
"github.com/containers/image/v5/transports/alltransports"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/storage"
|
|
structcopier "github.com/jinzhu/copier"
|
|
"github.com/opencontainers/go-digest"
|
|
imgspec "github.com/opencontainers/image-spec/specs-go"
|
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// NOTE: the abstractions and APIs here are a first step to further merge
|
|
// `libimage/manifests` into `libimage`.
|
|
|
|
// ErrNotAManifestList indicates that an image was found in the local
|
|
// containers storage but it is not a manifest list as requested.
|
|
var ErrNotAManifestList = errors.New("image is not a manifest list")
|
|
|
|
// ManifestList represents a manifest list (Docker) or an image index (OCI) in
|
|
// the local containers storage.
|
|
type ManifestList struct {
|
|
// NOTE: the *List* suffix is intentional as the term "manifest" is
|
|
// used ambiguously across the ecosystem. It may refer to the (JSON)
|
|
// manifest of an ordinary image OR to a manifest *list* (Docker) or to
|
|
// image index (OCI).
|
|
// It's a bit more work when typing but without ambiguity.
|
|
|
|
// The underlying image in the containers storage.
|
|
image *Image
|
|
|
|
// The underlying manifest list.
|
|
list manifests.List
|
|
}
|
|
|
|
// ID returns the ID of the manifest list.
|
|
func (m *ManifestList) ID() string {
|
|
return m.image.ID()
|
|
}
|
|
|
|
// CreateManifestList creates a new empty manifest list with the specified
|
|
// name.
|
|
func (r *Runtime) CreateManifestList(name string) (*ManifestList, error) {
|
|
normalized, err := NormalizeName(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list := manifests.Create()
|
|
listID, err := list.SaveToImage(r.store, "", []string{normalized.String()}, manifest.DockerV2ListMediaType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mList, err := r.LookupManifestList(listID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return mList, nil
|
|
}
|
|
|
|
// LookupManifestList looks up a manifest list with the specified name in the
|
|
// containers storage.
|
|
func (r *Runtime) LookupManifestList(name string) (*ManifestList, error) {
|
|
image, list, err := r.lookupManifestList(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ManifestList{image: image, list: list}, nil
|
|
}
|
|
|
|
func (r *Runtime) lookupManifestList(name string) (*Image, manifests.List, error) {
|
|
lookupOptions := &LookupImageOptions{
|
|
lookupManifest: true,
|
|
}
|
|
image, _, err := r.LookupImage(name, lookupOptions)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := image.reload(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
list, err := image.getManifestList()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return image, list, nil
|
|
}
|
|
|
|
// ConvertToManifestList converts the image into a manifest list if it is not
|
|
// already also a list. An error is returned if the conversion fails.
|
|
func (i *Image) ConvertToManifestList(ctx context.Context) (*ManifestList, error) {
|
|
// If we don't need to do anything, don't do anything.
|
|
if list, err := i.ToManifestList(); err == nil || !errors.Is(err, ErrNotAManifestList) {
|
|
return list, err
|
|
}
|
|
|
|
// Determine which type we prefer for the new manifest list or image index.
|
|
_, imageManifestType, err := i.Manifest(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading the image's manifest: %w", err)
|
|
}
|
|
var preferredListType string
|
|
switch imageManifestType {
|
|
case manifest.DockerV2Schema2MediaType,
|
|
manifest.DockerV2Schema1SignedMediaType,
|
|
manifest.DockerV2Schema1MediaType,
|
|
manifest.DockerV2ListMediaType:
|
|
preferredListType = manifest.DockerV2ListMediaType
|
|
case imgspecv1.MediaTypeImageManifest, imgspecv1.MediaTypeImageIndex:
|
|
preferredListType = imgspecv1.MediaTypeImageIndex
|
|
default:
|
|
preferredListType = ""
|
|
}
|
|
|
|
// Create a list and add the image's manifest to it. Use OCI format
|
|
// for now. If we need to convert it to Docker format, we'll do that
|
|
// while copying it.
|
|
list := manifests.Create()
|
|
if _, err := list.Add(ctx, &i.runtime.systemContext, i.storageReference, false); err != nil {
|
|
return nil, fmt.Errorf("generating new image index: %w", err)
|
|
}
|
|
listBytes, err := list.Serialize(imgspecv1.MediaTypeImageIndex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("serializing image index: %w", err)
|
|
}
|
|
listDigest, err := manifest.Digest(listBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("digesting image index: %w", err)
|
|
}
|
|
|
|
// Build an OCI layout containing the image index as the only item.
|
|
tmp, err := os.MkdirTemp("", "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("serializing initial list: %w", err)
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
|
|
// Drop our image index in there.
|
|
if err := os.Mkdir(filepath.Join(tmp, imgspecv1.ImageBlobsDir), 0o755); err != nil {
|
|
return nil, fmt.Errorf("creating directory for blobs: %w", err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(tmp, imgspecv1.ImageBlobsDir, listDigest.Algorithm().String()), 0o755); err != nil {
|
|
return nil, fmt.Errorf("creating directory for %s blobs: %w", listDigest.Algorithm().String(), err)
|
|
}
|
|
listFile := filepath.Join(tmp, imgspecv1.ImageBlobsDir, listDigest.Algorithm().String(), listDigest.Encoded())
|
|
if err := os.WriteFile(listFile, listBytes, 0o644); err != nil {
|
|
return nil, fmt.Errorf("writing image index for OCI layout: %w", err)
|
|
}
|
|
|
|
// Build the index for the layout.
|
|
index := imgspecv1.Index{
|
|
Versioned: imgspec.Versioned{
|
|
SchemaVersion: 2,
|
|
},
|
|
MediaType: imgspecv1.MediaTypeImageIndex,
|
|
Manifests: []imgspecv1.Descriptor{{
|
|
MediaType: imgspecv1.MediaTypeImageIndex,
|
|
Digest: listDigest,
|
|
Size: int64(len(listBytes)),
|
|
}},
|
|
}
|
|
indexBytes, err := json.Marshal(&index)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encoding image index for OCI layout: %w", err)
|
|
}
|
|
|
|
// Write the index for the layout.
|
|
indexFile := filepath.Join(tmp, imgspecv1.ImageIndexFile)
|
|
if err := os.WriteFile(indexFile, indexBytes, 0o644); err != nil {
|
|
return nil, fmt.Errorf("writing top-level index for OCI layout: %w", err)
|
|
}
|
|
|
|
// Write the "why yes, this is an OCI layout" file.
|
|
layoutFile := filepath.Join(tmp, imgspecv1.ImageLayoutFile)
|
|
layoutBytes, err := json.Marshal(imgspecv1.ImageLayout{Version: imgspecv1.ImageLayoutVersion})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encoding image layout structure for OCI layout: %w", err)
|
|
}
|
|
if err := os.WriteFile(layoutFile, layoutBytes, 0o644); err != nil {
|
|
return nil, fmt.Errorf("writing oci-layout file: %w", err)
|
|
}
|
|
|
|
// Build an OCI layout reference to use as a source.
|
|
tmpRef, err := layout.NewReference(tmp, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating reference to directory: %w", err)
|
|
}
|
|
bundle := supplemented.Reference(tmpRef, []types.ImageReference{i.storageReference}, imageCopy.CopySystemImage, nil)
|
|
|
|
// Build a policy that ensures we don't prevent ourselves from reading
|
|
// this reference.
|
|
signaturePolicy, err := signature.DefaultPolicy(&i.runtime.systemContext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("obtaining default signature policy: %w", err)
|
|
}
|
|
acceptAnything := signature.PolicyTransportScopes{
|
|
"": []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()},
|
|
}
|
|
signaturePolicy.Transports[i.storageReference.Transport().Name()] = acceptAnything
|
|
signaturePolicy.Transports[tmpRef.Transport().Name()] = acceptAnything
|
|
policyContext, err := signature.NewPolicyContext(signaturePolicy)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating new signature policy context: %w", err)
|
|
}
|
|
defer func() {
|
|
if err2 := policyContext.Destroy(); err2 != nil {
|
|
logrus.Errorf("Destroying signature policy context: %v", err2)
|
|
}
|
|
}()
|
|
|
|
// Copy from the OCI layout into the same image record, so that it gets
|
|
// both its own manifest and the image index.
|
|
copyOptions := imageCopy.Options{
|
|
ForceManifestMIMEType: imageManifestType,
|
|
}
|
|
if _, err := imageCopy.Image(ctx, policyContext, i.storageReference, bundle, ©Options); err != nil {
|
|
return nil, fmt.Errorf("writing updates to image: %w", err)
|
|
}
|
|
|
|
// Now explicitly write the list's manifest to the image as its "main"
|
|
// manifest.
|
|
if _, err := list.SaveToImage(i.runtime.store, i.ID(), i.storageImage.Names, preferredListType); err != nil {
|
|
return nil, fmt.Errorf("saving image index: %w", err)
|
|
}
|
|
|
|
// Reload the record.
|
|
if err = i.reload(); err != nil {
|
|
return nil, fmt.Errorf("reloading image record: %w", err)
|
|
}
|
|
mList, err := i.runtime.LookupManifestList(i.storageImage.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("looking up new manifest list: %w", err)
|
|
}
|
|
|
|
return mList, nil
|
|
}
|
|
|
|
// ToManifestList converts the image into a manifest list. An error is thrown
|
|
// if the image is not a manifest list.
|
|
func (i *Image) ToManifestList() (*ManifestList, error) {
|
|
list, err := i.getManifestList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ManifestList{image: i, list: list}, nil
|
|
}
|
|
|
|
// LookupInstance looks up an instance of the manifest list matching the
|
|
// specified platform. The local machine's platform is used if left empty.
|
|
func (m *ManifestList) LookupInstance(ctx context.Context, architecture, os, variant string) (*Image, error) {
|
|
sys := m.image.runtime.systemContextCopy()
|
|
if architecture != "" {
|
|
sys.ArchitectureChoice = architecture
|
|
}
|
|
if os != "" {
|
|
sys.OSChoice = os
|
|
}
|
|
if architecture != "" {
|
|
sys.VariantChoice = variant
|
|
}
|
|
|
|
// Now look at the *manifest* and select a matching instance.
|
|
rawManifest, manifestType, err := m.image.Manifest(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
list, err := manifest.ListFromBlob(rawManifest, manifestType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
instanceDigest, err := list.ChooseInstance(sys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allImages, err := m.image.runtime.ListImages(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, image := range allImages {
|
|
for _, imageDigest := range append(image.Digests(), image.Digest()) {
|
|
if imageDigest == instanceDigest {
|
|
return image, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("could not find image instance %s of manifest list %s in local containers storage: %w", instanceDigest, m.ID(), storage.ErrImageUnknown)
|
|
}
|
|
|
|
// Saves the specified manifest list and reloads it from storage with the new ID.
|
|
func (m *ManifestList) saveAndReload() error {
|
|
newID, err := m.list.SaveToImage(m.image.runtime.store, m.image.ID(), nil, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make sure to reload the image from the containers storage to fetch
|
|
// the latest data (e.g., new or delete digests).
|
|
if err := m.image.reload(); err != nil {
|
|
return err
|
|
}
|
|
image, list, err := m.image.runtime.lookupManifestList(newID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.image = image
|
|
m.list = list
|
|
return nil
|
|
}
|
|
|
|
// Reload the image and list instances from storage
|
|
func (m *ManifestList) reload() error {
|
|
listID := m.ID()
|
|
if err := m.image.reload(); err != nil {
|
|
return err
|
|
}
|
|
image, list, err := m.image.runtime.lookupManifestList(listID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.image = image
|
|
m.list = list
|
|
return nil
|
|
}
|
|
|
|
// getManifestList is a helper to obtain a manifest list
|
|
func (i *Image) getManifestList() (manifests.List, error) {
|
|
_, list, err := manifests.LoadFromImage(i.runtime.store, i.ID())
|
|
if errors.Is(err, manifesterrors.ErrManifestTypeNotSupported) {
|
|
err = fmt.Errorf("%s: %w", err.Error(), ErrNotAManifestList)
|
|
}
|
|
return list, err
|
|
}
|
|
|
|
// IsManifestList returns true if the image is a manifest list (Docker) or an
|
|
// image index (OCI). This information may be critical to make certain
|
|
// execution paths more robust (e.g., suppress certain errors).
|
|
func (i *Image) IsManifestList(ctx context.Context) (bool, error) {
|
|
// FIXME: due to `ImageDigestBigDataKey` we'll always check the
|
|
// _last-written_ manifest which is causing issues for multi-arch image
|
|
// pulls.
|
|
//
|
|
// See https://github.com/containers/common/pull/1505#discussion_r1242677279.
|
|
ref, err := i.StorageReference()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
imgSrc, err := ref.NewImageSource(ctx, i.runtime.systemContextCopy())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer imgSrc.Close()
|
|
_, manifestType, err := image.UnparsedInstance(imgSrc, nil).Manifest(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return manifest.MIMETypeIsMultiImage(manifestType), nil
|
|
}
|
|
|
|
// Inspect returns a dockerized version of the manifest list.
|
|
func (m *ManifestList) Inspect() (*define.ManifestListData, error) {
|
|
inspectList := define.ManifestListData{}
|
|
// Copy the fields from the Docker-format version of the list.
|
|
dockerFormat := m.list.Docker()
|
|
err := structcopier.Copy(&inspectList, &dockerFormat)
|
|
if err != nil {
|
|
return &inspectList, err
|
|
}
|
|
// Get OCI-specific fields from the OCIv1-format version of the list
|
|
// and copy them to the inspect data.
|
|
ociFormat := m.list.OCIv1()
|
|
inspectList.ArtifactType = ociFormat.ArtifactType
|
|
inspectList.Annotations = ociFormat.Annotations
|
|
for i, manifest := range ociFormat.Manifests {
|
|
inspectList.Manifests[i].Annotations = manifest.Annotations
|
|
inspectList.Manifests[i].ArtifactType = manifest.ArtifactType
|
|
inspectList.Manifests[i].URLs = slices.Clone(manifest.URLs)
|
|
inspectList.Manifests[i].Data = manifest.Data
|
|
inspectList.Manifests[i].Files, err = m.list.Files(manifest.Digest)
|
|
if err != nil {
|
|
return &inspectList, err
|
|
}
|
|
}
|
|
if ociFormat.Subject != nil {
|
|
platform := ociFormat.Subject.Platform
|
|
if platform == nil {
|
|
platform = &imgspecv1.Platform{}
|
|
}
|
|
osFeatures := slices.Clone(platform.OSFeatures)
|
|
inspectList.Subject = &define.ManifestListDescriptor{
|
|
Platform: manifest.Schema2PlatformSpec{
|
|
OS: platform.OS,
|
|
Architecture: platform.Architecture,
|
|
OSVersion: platform.OSVersion,
|
|
Variant: platform.Variant,
|
|
OSFeatures: osFeatures,
|
|
},
|
|
Schema2Descriptor: manifest.Schema2Descriptor{
|
|
MediaType: ociFormat.Subject.MediaType,
|
|
Digest: ociFormat.Subject.Digest,
|
|
Size: ociFormat.Subject.Size,
|
|
URLs: ociFormat.Subject.URLs,
|
|
},
|
|
Annotations: ociFormat.Subject.Annotations,
|
|
ArtifactType: ociFormat.Subject.ArtifactType,
|
|
Data: ociFormat.Subject.Data,
|
|
}
|
|
}
|
|
// Set MediaType to mirror the value we'd use when saving the list
|
|
// using defaults, instead of forcing it to one or the other by
|
|
// using the value from one version or the other that we explicitly
|
|
// requested above.
|
|
serialized, err := m.list.Serialize("")
|
|
if err != nil {
|
|
return &inspectList, err
|
|
}
|
|
var typed struct {
|
|
MediaType string `json:"mediaType,omitempty"`
|
|
}
|
|
if err := json.Unmarshal(serialized, &typed); err != nil {
|
|
return &inspectList, err
|
|
}
|
|
if typed.MediaType != "" {
|
|
inspectList.MediaType = typed.MediaType
|
|
}
|
|
return &inspectList, nil
|
|
}
|
|
|
|
// Options for adding an image or artifact to a manifest list.
|
|
type ManifestListAddOptions struct {
|
|
// Add all images to the list if the to-be-added image itself is a
|
|
// manifest list.
|
|
All bool `json:"all"`
|
|
// containers-auth.json(5) file to use when authenticating against
|
|
// container registries.
|
|
AuthFilePath string
|
|
// Path to the certificates directory.
|
|
CertDirPath string
|
|
// Allow contacting registries over HTTP, or HTTPS with failed TLS
|
|
// verification. Note that this does not affect other TLS connections.
|
|
InsecureSkipTLSVerify types.OptionalBool
|
|
// Username to use when authenticating at a container registry.
|
|
Username string
|
|
// Password to use when authenticating at a container registry.
|
|
Password string
|
|
}
|
|
|
|
func (m *ManifestList) parseNameToExtantReference(ctx context.Context, sys *types.SystemContext, name string, manifestList bool, what string) (types.ImageReference, error) {
|
|
ref, err := alltransports.ParseImageName(name)
|
|
if err != nil {
|
|
withDocker := fmt.Sprintf("%s://%s", docker.Transport.Name(), name)
|
|
ref, err = alltransports.ParseImageName(withDocker)
|
|
if err == nil {
|
|
var src types.ImageSource
|
|
src, err = ref.NewImageSource(ctx, sys)
|
|
if err == nil {
|
|
src.Close()
|
|
}
|
|
}
|
|
if err != nil {
|
|
image, _, lookupErr := m.image.runtime.LookupImage(name, &LookupImageOptions{ManifestList: manifestList})
|
|
if lookupErr != nil {
|
|
return nil, fmt.Errorf("locating %s: %q: %w; %q: %w", what, withDocker, err, name, lookupErr)
|
|
}
|
|
ref, err = image.storageReference, nil
|
|
}
|
|
}
|
|
return ref, err
|
|
}
|
|
|
|
// Add adds one or more manifests to the manifest list and returns the digest
|
|
// of the added instance.
|
|
func (m *ManifestList) Add(ctx context.Context, name string, options *ManifestListAddOptions) (digest.Digest, error) {
|
|
if options == nil {
|
|
options = &ManifestListAddOptions{}
|
|
}
|
|
|
|
// Now massage in the copy-related options into the system context.
|
|
systemContext := m.image.runtime.systemContextCopy()
|
|
if options.AuthFilePath != "" {
|
|
systemContext.AuthFilePath = options.AuthFilePath
|
|
}
|
|
if options.CertDirPath != "" {
|
|
systemContext.DockerCertPath = options.CertDirPath
|
|
}
|
|
if options.InsecureSkipTLSVerify != types.OptionalBoolUndefined {
|
|
systemContext.DockerInsecureSkipTLSVerify = options.InsecureSkipTLSVerify
|
|
systemContext.OCIInsecureSkipTLSVerify = options.InsecureSkipTLSVerify == types.OptionalBoolTrue
|
|
systemContext.DockerDaemonInsecureSkipTLSVerify = options.InsecureSkipTLSVerify == types.OptionalBoolTrue
|
|
}
|
|
if options.Username != "" {
|
|
systemContext.DockerAuthConfig = &types.DockerAuthConfig{
|
|
Username: options.Username,
|
|
Password: options.Password,
|
|
}
|
|
}
|
|
|
|
ref, err := m.parseNameToExtantReference(ctx, systemContext, name, false, "image to add to manifest list")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
locker, err := manifests.LockerForImage(m.image.runtime.store, m.ID())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
locker.Lock()
|
|
defer locker.Unlock()
|
|
// Make sure to reload the image from the containers storage to fetch
|
|
// the latest data (e.g., new or delete digests).
|
|
if err := m.reload(); err != nil {
|
|
return "", err
|
|
}
|
|
newDigest, err := m.list.Add(ctx, systemContext, ref, options.All)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Write the changes to disk.
|
|
if err := m.saveAndReload(); err != nil {
|
|
return "", err
|
|
}
|
|
return newDigest, nil
|
|
}
|
|
|
|
// Options for creating an artifact manifest for one or more files and adding
|
|
// the artifact manifest to a manifest list.
|
|
type ManifestListAddArtifactOptions struct {
|
|
// The artifactType to set in the artifact manifest.
|
|
Type *string `json:"artifact_type"`
|
|
// The mediaType to set in the config.MediaType field in the artifact manifest.
|
|
ConfigType string `json:"artifact_config_type"`
|
|
// Content to point to from the config field in the artifact manifest.
|
|
Config string `json:"artifact_config"`
|
|
// The mediaType to set in the layer descriptors in the artifact manifest.
|
|
LayerType string `json:"artifact_layer_type"`
|
|
// Whether or not to suppress the org.opencontainers.image.title annotation in layer descriptors.
|
|
ExcludeTitles bool `json:"exclude_layer_titles"`
|
|
// Annotations to set in the artifact manifest.
|
|
Annotations map[string]string `json:"annotations"`
|
|
// Subject to set in the artifact manifest.
|
|
Subject string `json:"subject"`
|
|
}
|
|
|
|
// Add adds one or more manifests to the manifest list and returns the digest
|
|
// of the added instance.
|
|
func (m *ManifestList) AddArtifact(ctx context.Context, options *ManifestListAddArtifactOptions, files ...string) (digest.Digest, error) {
|
|
if options == nil {
|
|
options = &ManifestListAddArtifactOptions{}
|
|
}
|
|
opts := manifests.AddArtifactOptions{
|
|
ManifestArtifactType: options.Type,
|
|
Annotations: maps.Clone(options.Annotations),
|
|
ExcludeTitles: options.ExcludeTitles,
|
|
}
|
|
if options.ConfigType != "" {
|
|
opts.ConfigDescriptor = &imgspecv1.Descriptor{
|
|
MediaType: options.ConfigType,
|
|
Digest: imgspecv1.DescriptorEmptyJSON.Digest,
|
|
Size: imgspecv1.DescriptorEmptyJSON.Size,
|
|
Data: slices.Clone(imgspecv1.DescriptorEmptyJSON.Data),
|
|
}
|
|
}
|
|
if options.Config != "" {
|
|
if opts.ConfigDescriptor == nil {
|
|
opts.ConfigDescriptor = &imgspecv1.Descriptor{
|
|
MediaType: imgspecv1.MediaTypeImageConfig,
|
|
}
|
|
}
|
|
opts.ConfigDescriptor.Digest = digest.FromString(options.Config)
|
|
opts.ConfigDescriptor.Size = int64(len(options.Config))
|
|
opts.ConfigDescriptor.Data = slices.Clone([]byte(options.Config))
|
|
}
|
|
if opts.ConfigDescriptor == nil {
|
|
empty := imgspecv1.DescriptorEmptyJSON
|
|
opts.ConfigDescriptor = &empty
|
|
}
|
|
if options.LayerType != "" {
|
|
opts.LayerMediaType = &options.LayerType
|
|
}
|
|
if options.Subject != "" {
|
|
ref, err := m.parseNameToExtantReference(ctx, nil, options.Subject, true, "subject for artifact manifest")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
opts.SubjectReference = ref
|
|
}
|
|
|
|
// Lock the image record where this list lives.
|
|
locker, err := manifests.LockerForImage(m.image.runtime.store, m.ID())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
locker.Lock()
|
|
defer locker.Unlock()
|
|
|
|
systemContext := m.image.runtime.systemContextCopy()
|
|
|
|
// Make sure to reload the image from the containers storage to fetch
|
|
// the latest data (e.g., new or delete digests).
|
|
if err := m.reload(); err != nil {
|
|
return "", err
|
|
}
|
|
newDigest, err := m.list.AddArtifact(ctx, systemContext, opts, files...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Write the changes to disk.
|
|
if err := m.saveAndReload(); err != nil {
|
|
return "", err
|
|
}
|
|
return newDigest, nil
|
|
}
|
|
|
|
// Options for annotating a manifest list.
|
|
type ManifestListAnnotateOptions struct {
|
|
// Add the specified annotations to the added image. Empty values are ignored.
|
|
Annotations map[string]string
|
|
// Add the specified architecture to the added image. Empty values are ignored.
|
|
Architecture string
|
|
// Add the specified features to the added image. Empty values are ignored.
|
|
Features []string
|
|
// Add the specified OS to the added image. Empty values are ignored.
|
|
OS string
|
|
// Add the specified OS features to the added image. Empty values are ignored.
|
|
OSFeatures []string
|
|
// Add the specified OS version to the added image. Empty values are ignored.
|
|
OSVersion string
|
|
// Add the specified variant to the added image. Empty values are ignored unless Architecture is set to a non-empty value.
|
|
Variant string
|
|
// Add the specified annotations to the index itself. Empty values are ignored.
|
|
IndexAnnotations map[string]string
|
|
// Set the subject to which the index refers. Empty values are ignored.
|
|
Subject string
|
|
}
|
|
|
|
// Annotate an image instance specified by `d` in the manifest list.
|
|
func (m *ManifestList) AnnotateInstance(d digest.Digest, options *ManifestListAnnotateOptions) error {
|
|
ctx := context.Background()
|
|
|
|
if options == nil {
|
|
return nil
|
|
}
|
|
|
|
if len(options.OS) > 0 {
|
|
if err := m.list.SetOS(d, options.OS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(options.OSVersion) > 0 {
|
|
if err := m.list.SetOSVersion(d, options.OSVersion); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(options.Features) > 0 {
|
|
if err := m.list.SetFeatures(d, options.Features); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(options.OSFeatures) > 0 {
|
|
if err := m.list.SetOSFeatures(d, options.OSFeatures); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(options.Architecture) > 0 {
|
|
if err := m.list.SetArchitecture(d, options.Architecture); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(options.Architecture) != 0 || len(options.Variant) > 0 {
|
|
if err := m.list.SetVariant(d, options.Variant); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(options.Annotations) > 0 {
|
|
if err := m.list.SetAnnotations(&d, options.Annotations); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(options.IndexAnnotations) > 0 {
|
|
if err := m.list.SetAnnotations(nil, options.IndexAnnotations); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if options.Subject != "" {
|
|
ref, err := m.parseNameToExtantReference(ctx, nil, options.Subject, true, "subject for image index")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
src, err := ref.NewImageSource(ctx, &m.image.runtime.systemContext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
subjectManifestBytes, subjectManifestType, err := image.UnparsedInstance(src, nil).Manifest(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subjectManifestDigest, err := manifest.Digest(subjectManifestBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var subjectArtifactType string
|
|
if !manifest.MIMETypeIsMultiImage(subjectManifestType) {
|
|
var subjectManifest imgspecv1.Manifest
|
|
if json.Unmarshal(subjectManifestBytes, &subjectManifest) == nil {
|
|
subjectArtifactType = subjectManifest.ArtifactType
|
|
}
|
|
}
|
|
descriptor := &imgspecv1.Descriptor{
|
|
MediaType: subjectManifestType,
|
|
ArtifactType: subjectArtifactType,
|
|
Digest: subjectManifestDigest,
|
|
Size: int64(len(subjectManifestBytes)),
|
|
}
|
|
if err := m.list.SetSubject(descriptor); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Write the changes to disk.
|
|
return m.saveAndReload()
|
|
}
|
|
|
|
// RemoveInstance removes the instance specified by `d` from the manifest list.
|
|
// Returns the new ID of the image.
|
|
func (m *ManifestList) RemoveInstance(d digest.Digest) error {
|
|
if err := m.list.Remove(d); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write the changes to disk.
|
|
return m.saveAndReload()
|
|
}
|
|
|
|
// ManifestListPushOptions allow for customizing pushing a manifest list.
|
|
type ManifestListPushOptions struct {
|
|
CopyOptions
|
|
|
|
// For tweaking the list selection.
|
|
ImageListSelection imageCopy.ImageListSelection
|
|
// Use when selecting only specific imags.
|
|
Instances []digest.Digest
|
|
// Add existing instances with requested compression algorithms to manifest list
|
|
AddCompression []string
|
|
}
|
|
|
|
// Push pushes a manifest to the specified destination.
|
|
func (m *ManifestList) Push(ctx context.Context, destination string, options *ManifestListPushOptions) (digest.Digest, error) {
|
|
if options == nil {
|
|
options = &ManifestListPushOptions{}
|
|
}
|
|
|
|
dest, err := alltransports.ParseImageName(destination)
|
|
if err != nil {
|
|
oldErr := err
|
|
dest, err = alltransports.ParseImageName("docker://" + destination)
|
|
if err != nil {
|
|
return "", oldErr
|
|
}
|
|
}
|
|
|
|
if m.image.runtime.eventChannel != nil {
|
|
defer m.image.runtime.writeEvent(&Event{ID: m.ID(), Name: destination, Time: time.Now(), Type: EventTypeImagePush})
|
|
}
|
|
|
|
// NOTE: we're using the logic in copier to create a proper
|
|
// types.SystemContext. This prevents us from having an error prone
|
|
// code duplicate here.
|
|
copier, err := m.image.runtime.newCopier(&options.CopyOptions)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer copier.Close()
|
|
|
|
pushOptions := manifests.PushOptions{
|
|
AddCompression: options.AddCompression,
|
|
Store: m.image.runtime.store,
|
|
SystemContext: copier.systemContext,
|
|
ImageListSelection: options.ImageListSelection,
|
|
Instances: options.Instances,
|
|
ReportWriter: options.Writer,
|
|
Signers: options.Signers,
|
|
SignBy: options.SignBy,
|
|
SignPassphrase: options.SignPassphrase,
|
|
SignBySigstorePrivateKeyFile: options.SignBySigstorePrivateKeyFile,
|
|
SignSigstorePrivateKeyPassphrase: options.SignSigstorePrivateKeyPassphrase,
|
|
RemoveSignatures: options.RemoveSignatures,
|
|
ManifestType: options.ManifestMIMEType,
|
|
MaxRetries: options.MaxRetries,
|
|
RetryDelay: options.RetryDelay,
|
|
ForceCompressionFormat: options.ForceCompressionFormat,
|
|
}
|
|
|
|
_, d, err := m.list.Push(ctx, dest, pushOptions)
|
|
return d, err
|
|
}
|