From 0f7d54b0260c1be992ee3b9cee359ef3a9e8bd21 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Thu, 22 Apr 2021 08:01:12 +0200 Subject: migrate Podman to containers/common/libimage Migrate the Podman code base over to `common/libimage` which replaces `libpod/image` and a lot of glue code entirely. Note that I tried to leave bread crumbs for changed tests. Miscellaneous changes: * Some errors yield different messages which required to alter some tests. * I fixed some pre-existing issues in the code. Others were marked as `//TODO`s to prevent the PR from exploding. * The `NamesHistory` of an image is returned as is from the storage. Previously, we did some filtering which I think is undesirable. Instead we should return the data as stored in the storage. * Touched handlers use the ABI interfaces where possible. * Local image resolution: previously Podman would match "foo" on "myfoo". This behaviour has been changed and Podman will now only match on repository boundaries such that "foo" would match "my/foo" but not "myfoo". I consider the old behaviour to be a bug, at the very least an exotic corner case. * Futhermore, "foo:none" does *not* resolve to a local image "foo" without tag anymore. It's a hill I am (almost) willing to die on. * `image prune` prints the IDs of pruned images. Previously, in some cases, the names were printed instead. The API clearly states ID, so we should stick to it. * Compat endpoint image removal with _force_ deletes the entire not only the specified tag. Signed-off-by: Valentin Rothberg --- libpod/image/config.go | 14 - libpod/image/df.go | 126 -- libpod/image/docker_registry_options.go | 75 - libpod/image/errors.go | 16 - libpod/image/filters.go | 196 --- libpod/image/image.go | 1858 --------------------- libpod/image/image_test.go | 318 ---- libpod/image/layer_tree.go | 239 --- libpod/image/manifests.go | 209 --- libpod/image/parts.go | 104 -- libpod/image/parts_test.go | 123 -- libpod/image/prune.go | 164 -- libpod/image/pull.go | 437 ----- libpod/image/pull_test.go | 394 ----- libpod/image/search.go | 318 ---- libpod/image/signing_options.go | 10 - libpod/image/testdata/docker-name-only.tar.xz | Bin 1024 -> 0 bytes libpod/image/testdata/docker-registry-name.tar.xz | Bin 1028 -> 0 bytes libpod/image/testdata/docker-two-images.tar.xz | Bin 1416 -> 0 bytes libpod/image/testdata/docker-two-names.tar.xz | Bin 1040 -> 0 bytes libpod/image/testdata/docker-unnamed.tar.xz | Bin 968 -> 0 bytes libpod/image/testdata/oci-name-only.tar.gz | Bin 975 -> 0 bytes libpod/image/testdata/oci-non-docker-name.tar.gz | Bin 991 -> 0 bytes libpod/image/testdata/oci-registry-name.tar.gz | Bin 979 -> 0 bytes libpod/image/testdata/oci-unnamed.tar.gz | Bin 928 -> 0 bytes libpod/image/testdata/registries.conf | 4 - libpod/image/tree.go | 138 -- libpod/image/utils.go | 182 -- 28 files changed, 4925 deletions(-) delete mode 100644 libpod/image/config.go delete mode 100644 libpod/image/df.go delete mode 100644 libpod/image/docker_registry_options.go delete mode 100644 libpod/image/errors.go delete mode 100644 libpod/image/filters.go delete mode 100644 libpod/image/image.go delete mode 100644 libpod/image/image_test.go delete mode 100644 libpod/image/layer_tree.go delete mode 100644 libpod/image/manifests.go delete mode 100644 libpod/image/parts.go delete mode 100644 libpod/image/parts_test.go delete mode 100644 libpod/image/prune.go delete mode 100644 libpod/image/pull.go delete mode 100644 libpod/image/pull_test.go delete mode 100644 libpod/image/search.go delete mode 100644 libpod/image/signing_options.go delete mode 100644 libpod/image/testdata/docker-name-only.tar.xz delete mode 100644 libpod/image/testdata/docker-registry-name.tar.xz delete mode 100644 libpod/image/testdata/docker-two-images.tar.xz delete mode 100644 libpod/image/testdata/docker-two-names.tar.xz delete mode 100644 libpod/image/testdata/docker-unnamed.tar.xz delete mode 100644 libpod/image/testdata/oci-name-only.tar.gz delete mode 100644 libpod/image/testdata/oci-non-docker-name.tar.gz delete mode 100644 libpod/image/testdata/oci-registry-name.tar.gz delete mode 100644 libpod/image/testdata/oci-unnamed.tar.gz delete mode 100644 libpod/image/testdata/registries.conf delete mode 100644 libpod/image/tree.go delete mode 100644 libpod/image/utils.go (limited to 'libpod/image') diff --git a/libpod/image/config.go b/libpod/image/config.go deleted file mode 100644 index efd83d343..000000000 --- a/libpod/image/config.go +++ /dev/null @@ -1,14 +0,0 @@ -package image - -const ( - // LatestTag describes the tag used to refer to the latest version - // of an image - LatestTag = "latest" -) - -// ImageDeleteResponse is the response for removing an image from storage and containers -// what was untagged vs actually removed -type ImageDeleteResponse struct { //nolint - Untagged []string `json:"untagged"` - Deleted string `json:"deleted"` -} diff --git a/libpod/image/df.go b/libpod/image/df.go deleted file mode 100644 index 231d28df4..000000000 --- a/libpod/image/df.go +++ /dev/null @@ -1,126 +0,0 @@ -package image - -import ( - "context" - "time" - - "github.com/containers/image/v5/docker/reference" -) - -// DiskUsageStat gives disk-usage statistics for a specific image. -type DiskUsageStat struct { - // ID of the image. - ID string - // Repository of the first recorded name of the image. - Repository string - // Tag of the first recorded name of the image. - Tag string - // Created is the creation time of the image. - Created time.Time - // SharedSize is the amount of space shared with another image. - SharedSize uint64 - // UniqueSize is the amount of space used only by this image. - UniqueSize uint64 - // Size is the total size of the image (i.e., the sum of the shared and - // unique size). - Size uint64 - // Number of containers using the image. - Containers int -} - -// DiskUsage returns disk-usage statistics for the specified slice of images. -func (ir *Runtime) DiskUsage(ctx context.Context, images []*Image) ([]DiskUsageStat, error) { - stats := make([]DiskUsageStat, len(images)) - - // Build a layerTree to quickly compute (and cache!) parent/child - // relations. - tree, err := ir.layerTree() - if err != nil { - return nil, err - } - - // Calculate the stats for each image. - for i, img := range images { - stat, err := diskUsageForImage(ctx, img, tree) - if err != nil { - return nil, err - } - stats[i] = *stat - } - - return stats, nil -} - -// diskUsageForImage returns the disk-usage statistics for the specified image. -func diskUsageForImage(ctx context.Context, image *Image, tree *layerTree) (*DiskUsageStat, error) { - stat := DiskUsageStat{ - ID: image.ID(), - Created: image.Created(), - } - - // Repository and tag. - var name, repository, tag string - for _, n := range image.Names() { - if len(n) > 0 { - name = n - break - } - } - if len(name) > 0 { - named, err := reference.ParseNormalizedNamed(name) - if err != nil { - return nil, err - } - repository = named.Name() - if tagged, isTagged := named.(reference.NamedTagged); isTagged { - tag = tagged.Tag() - } - } else { - repository = "" - tag = "" - } - stat.Repository = repository - stat.Tag = tag - - // Shared, unique and total size. - parent, err := tree.parent(ctx, image) - if err != nil { - return nil, err - } - childIDs, err := tree.children(ctx, image, false) - if err != nil { - return nil, err - } - // Optimistically set unique size to the full size of the image. - size, err := image.Size(ctx) - if err != nil { - return nil, err - } - stat.UniqueSize = *size - - if len(childIDs) > 0 { - // If we have children, we share everything. - stat.SharedSize = stat.UniqueSize - stat.UniqueSize = 0 - } else if parent != nil { - // If we have no children but a parent, remove the parent - // (shared) size from the unique one. - size, err := parent.Size(ctx) - if err != nil { - return nil, err - } - stat.UniqueSize -= *size - stat.SharedSize = *size - } - - stat.Size = stat.SharedSize + stat.UniqueSize - - // Number of containers using the image. - containers, err := image.Containers() - if err != nil { - return nil, err - } - stat.Containers = len(containers) - - return &stat, nil -} diff --git a/libpod/image/docker_registry_options.go b/libpod/image/docker_registry_options.go deleted file mode 100644 index d95234e3d..000000000 --- a/libpod/image/docker_registry_options.go +++ /dev/null @@ -1,75 +0,0 @@ -package image - -import ( - "fmt" - - "github.com/containers/buildah/pkg/parse" - "github.com/containers/image/v5/docker/reference" - "github.com/containers/image/v5/types" - podmanVersion "github.com/containers/podman/v3/version" -) - -// DockerRegistryOptions encapsulates settings that affect how we connect or -// authenticate to a remote registry. -type DockerRegistryOptions struct { - // DockerRegistryCreds is the user name and password to supply in case - // we need to pull an image from a registry, and it requires us to - // authenticate. - DockerRegistryCreds *types.DockerAuthConfig - // DockerCertPath is the location of a directory containing CA - // certificates which will be used to verify the registry's certificate - // (all files with names ending in ".crt"), and possibly client - // certificates and private keys (pairs of files with the same name, - // except for ".cert" and ".key" suffixes). - DockerCertPath string - // DockerInsecureSkipTLSVerify turns off verification of TLS - // certificates and allows connecting to registries without encryption - // - or forces it on even if registries.conf has the registry configured as insecure. - DockerInsecureSkipTLSVerify types.OptionalBool - // If not "", overrides the use of platform.GOOS when choosing an image or verifying OS match. - OSChoice string - // If not "", overrides the use of platform.GOARCH when choosing an image or verifying architecture match. - ArchitectureChoice string - // If not "", overrides_VARIANT_ instead of the running architecture variant for choosing images. - VariantChoice string - // RegistriesConfPath can be used to override the default path of registries.conf. - RegistriesConfPath string -} - -// GetSystemContext constructs a new system context from a parent context. the values in the DockerRegistryOptions, and other parameters. -func (o DockerRegistryOptions) GetSystemContext(parent *types.SystemContext, additionalDockerArchiveTags []reference.NamedTagged) *types.SystemContext { - sc := &types.SystemContext{ - DockerAuthConfig: o.DockerRegistryCreds, - DockerCertPath: o.DockerCertPath, - DockerInsecureSkipTLSVerify: o.DockerInsecureSkipTLSVerify, - DockerArchiveAdditionalTags: additionalDockerArchiveTags, - OSChoice: o.OSChoice, - ArchitectureChoice: o.ArchitectureChoice, - VariantChoice: o.VariantChoice, - BigFilesTemporaryDir: parse.GetTempDir(), - } - if parent != nil { - sc.SignaturePolicyPath = parent.SignaturePolicyPath - sc.AuthFilePath = parent.AuthFilePath - sc.DirForceCompress = parent.DirForceCompress - sc.DockerRegistryUserAgent = parent.DockerRegistryUserAgent - sc.OSChoice = parent.OSChoice - sc.ArchitectureChoice = parent.ArchitectureChoice - sc.BlobInfoCacheDir = parent.BlobInfoCacheDir - } - return sc -} - -// GetSystemContext Constructs a new containers/image/types.SystemContext{} struct from the given signaturePolicy path -func GetSystemContext(signaturePolicyPath, authFilePath string, forceCompress bool) *types.SystemContext { - sc := &types.SystemContext{} - if signaturePolicyPath != "" { - sc.SignaturePolicyPath = signaturePolicyPath - } - sc.AuthFilePath = authFilePath - sc.DirForceCompress = forceCompress - sc.DockerRegistryUserAgent = fmt.Sprintf("libpod/%s", podmanVersion.Version) - sc.BigFilesTemporaryDir = parse.GetTempDir() - - return sc -} diff --git a/libpod/image/errors.go b/libpod/image/errors.go deleted file mode 100644 index 49f841bf4..000000000 --- a/libpod/image/errors.go +++ /dev/null @@ -1,16 +0,0 @@ -package image - -import ( - "github.com/containers/podman/v3/libpod/define" -) - -var ( - // ErrNoSuchCtr indicates the requested container does not exist - ErrNoSuchCtr = define.ErrNoSuchCtr - // ErrNoSuchPod indicates the requested pod does not exist - ErrNoSuchPod = define.ErrNoSuchPod - // ErrNoSuchImage indicates the requested image does not exist - ErrNoSuchImage = define.ErrNoSuchImage - // ErrNoSuchTag indicates the requested image tag does not exist - ErrNoSuchTag = define.ErrNoSuchTag -) diff --git a/libpod/image/filters.go b/libpod/image/filters.go deleted file mode 100644 index d316c6956..000000000 --- a/libpod/image/filters.go +++ /dev/null @@ -1,196 +0,0 @@ -package image - -import ( - "context" - "fmt" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/containers/podman/v3/pkg/inspect" - "github.com/containers/podman/v3/pkg/util" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// ResultFilter is a mock function for image filtering -type ResultFilter func(*Image) bool - -// Filter is a function to determine whether an image is included in -// command output. Images to be outputted are tested using the function. A true -// return will include the image, a false return will exclude it. -type Filter func(*Image, *inspect.ImageData) bool - -// CreatedBeforeFilter allows you to filter on images created before -// the given time.Time -func CreatedBeforeFilter(createTime time.Time) ResultFilter { - return func(i *Image) bool { - return i.Created().Before(createTime) - } -} - -// IntermediateFilter returns filter for intermediate images (i.e., images -// with children and no tags). -func (ir *Runtime) IntermediateFilter(ctx context.Context, images []*Image) (ResultFilter, error) { - tree, err := ir.layerTree() - if err != nil { - return nil, err - } - return func(i *Image) bool { - if len(i.Names()) > 0 { - return true - } - children, err := tree.children(ctx, i, false) - if err != nil { - logrus.Error(err.Error()) - return false - } - return len(children) == 0 - }, nil -} - -// CreatedAfterFilter allows you to filter on images created after -// the given time.Time -func CreatedAfterFilter(createTime time.Time) ResultFilter { - return func(i *Image) bool { - return i.Created().After(createTime) - } -} - -// DanglingFilter allows you to filter images for dangling images -func DanglingFilter(danglingImages bool) ResultFilter { - return func(i *Image) bool { - if danglingImages { - return i.Dangling() - } - return !i.Dangling() - } -} - -// ReadOnlyFilter allows you to filter images based on read/only and read/write -func ReadOnlyFilter(readOnly bool) ResultFilter { - return func(i *Image) bool { - if readOnly { - return i.IsReadOnly() - } - return !i.IsReadOnly() - } -} - -// LabelFilter allows you to filter by images labels key and/or value -func LabelFilter(ctx context.Context, filter string) ResultFilter { - // We need to handle both label=key and label=key=value - return func(i *Image) bool { - labels, err := i.Labels(ctx) - if err != nil { - return false - } - return util.MatchLabelFilters([]string{filter}, labels) - } -} - -// ReferenceFilter allows you to filter by image name -// Replacing all '/' with '|' so that filepath.Match() can work -// '|' character is not valid in image name, so this is safe -func ReferenceFilter(ctx context.Context, referenceFilter string) ResultFilter { - filter := fmt.Sprintf("*%s*", referenceFilter) - filter = strings.Replace(filter, "/", "|", -1) - return func(i *Image) bool { - if len(referenceFilter) < 1 { - return true - } - for _, name := range i.Names() { - newName := strings.Replace(name, "/", "|", -1) - match, err := filepath.Match(filter, newName) - if err != nil { - logrus.Errorf("failed to match %s and %s, %q", name, referenceFilter, err) - } - if match { - return true - } - } - return false - } -} - -// IDFilter allows you to filter by image Id -func IDFilter(idFilter string) ResultFilter { - return func(i *Image) bool { - return i.ID() == idFilter - } -} - -// OutputImageFilter allows you to filter by an a specific image name -func OutputImageFilter(userImage *Image) ResultFilter { - return func(i *Image) bool { - return userImage.ID() == i.ID() - } -} - -// FilterImages filters images using a set of predefined filter funcs -func FilterImages(images []*Image, filters []ResultFilter) []*Image { - var filteredImages []*Image - for _, image := range images { - include := true - for _, filter := range filters { - include = include && filter(image) - } - if include { - filteredImages = append(filteredImages, image) - } - } - return filteredImages -} - -// createFilterFuncs returns an array of filter functions based on the user inputs -// and is later used to filter images for output -func (ir *Runtime) createFilterFuncs(filters []string, img *Image) ([]ResultFilter, error) { - var filterFuncs []ResultFilter - ctx := context.Background() - for _, filter := range filters { - splitFilter := strings.SplitN(filter, "=", 2) - if len(splitFilter) < 2 { - return nil, errors.Errorf("invalid filter syntax %s", filter) - } - switch splitFilter[0] { - case "before": - before, err := ir.NewFromLocal(splitFilter[1]) - if err != nil { - return nil, errors.Wrapf(err, "unable to find image %s in local stores", splitFilter[1]) - } - filterFuncs = append(filterFuncs, CreatedBeforeFilter(before.Created())) - case "since", "after": - after, err := ir.NewFromLocal(splitFilter[1]) - if err != nil { - return nil, errors.Wrapf(err, "unable to find image %s in local stores", splitFilter[1]) - } - filterFuncs = append(filterFuncs, CreatedAfterFilter(after.Created())) - case "readonly": - readonly, err := strconv.ParseBool(splitFilter[1]) - if err != nil { - return nil, errors.Wrapf(err, "invalid filter readonly=%s", splitFilter[1]) - } - filterFuncs = append(filterFuncs, ReadOnlyFilter(readonly)) - case "dangling": - danglingImages, err := strconv.ParseBool(splitFilter[1]) - if err != nil { - return nil, errors.Wrapf(err, "invalid filter dangling=%s", splitFilter[1]) - } - filterFuncs = append(filterFuncs, DanglingFilter(danglingImages)) - case "label": - labelFilter := strings.Join(splitFilter[1:], "=") - filterFuncs = append(filterFuncs, LabelFilter(ctx, labelFilter)) - case "reference": - filterFuncs = append(filterFuncs, ReferenceFilter(ctx, splitFilter[1])) - case "id": - filterFuncs = append(filterFuncs, IDFilter(splitFilter[1])) - default: - return nil, errors.Errorf("invalid filter %s ", splitFilter[0]) - } - } - if img != nil { - filterFuncs = append(filterFuncs, OutputImageFilter(img)) - } - return filterFuncs, nil -} diff --git a/libpod/image/image.go b/libpod/image/image.go deleted file mode 100644 index 3c9fb3a37..000000000 --- a/libpod/image/image.go +++ /dev/null @@ -1,1858 +0,0 @@ -package image - -import ( - "context" - "encoding/json" - stderrors "errors" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - "syscall" - "time" - - "github.com/containers/common/pkg/retry" - cp "github.com/containers/image/v5/copy" - "github.com/containers/image/v5/directory" - dockerarchive "github.com/containers/image/v5/docker/archive" - "github.com/containers/image/v5/docker/reference" - "github.com/containers/image/v5/image" - "github.com/containers/image/v5/manifest" - ociarchive "github.com/containers/image/v5/oci/archive" - "github.com/containers/image/v5/oci/layout" - "github.com/containers/image/v5/pkg/shortnames" - is "github.com/containers/image/v5/storage" - "github.com/containers/image/v5/tarball" - "github.com/containers/image/v5/transports" - "github.com/containers/image/v5/transports/alltransports" - "github.com/containers/image/v5/types" - "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/libpod/driver" - "github.com/containers/podman/v3/libpod/events" - "github.com/containers/podman/v3/pkg/inspect" - "github.com/containers/podman/v3/pkg/registries" - "github.com/containers/podman/v3/pkg/util" - "github.com/containers/storage" - digest "github.com/opencontainers/go-digest" - ociv1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// Image is the primary struct for dealing with images -// It is still very much a work in progress -type Image struct { - // Adding these two structs for now but will cull when we near - // completion of this library. - imgRef types.Image - imgSrcRef types.ImageSource - inspect.ImageData - inspect.ImageResult - inspectInfo *types.ImageInspectInfo - InputName string - image *storage.Image - imageruntime *Runtime -} - -// Runtime contains the store -type Runtime struct { - store storage.Store - SignaturePolicyPath string - EventsLogFilePath string - EventsLogger string - Eventer events.Eventer -} - -// InfoImage keep information of Image along with all associated layers -type InfoImage struct { - // ID of image - ID string - // Tags of image - Tags []string - // Layers stores all layers of image. - Layers []LayerInfo -} - -const maxRetry = 3 - -// ImageFilter is a function to determine whether a image is included -// in command output. Images to be outputted are tested using the function. -// A true return will include the image, a false return will exclude it. -type ImageFilter func(*Image) bool //nolint - -// ErrRepoTagNotFound is the error returned when the image id given doesn't match a rep tag in store -var ErrRepoTagNotFound = stderrors.New("unable to match user input to any specific repotag") - -// ErrImageIsBareList is the error returned when the image is just a list or index -var ErrImageIsBareList = stderrors.New("image contains a manifest list or image index, but no runnable image") - -// NewImageRuntimeFromStore creates an ImageRuntime based on a provided store -func NewImageRuntimeFromStore(store storage.Store) *Runtime { - return &Runtime{ - store: store, - } -} - -// NewImageRuntimeFromOptions creates an Image Runtime including the store given -// store options -func NewImageRuntimeFromOptions(options storage.StoreOptions) (*Runtime, error) { - store, err := setStore(options) - if err != nil { - return nil, err - } - return NewImageRuntimeFromStore(store), nil -} - -func setStore(options storage.StoreOptions) (storage.Store, error) { - store, err := storage.GetStore(options) - if err != nil { - return nil, err - } - is.Transport.SetStore(store) - return store, nil -} - -// newImage creates a new image object given an "input name" and a storage.Image -func (ir *Runtime) newImage(inputName string, img *storage.Image) *Image { - return &Image{ - InputName: inputName, - imageruntime: ir, - image: img, - } -} - -// newFromStorage creates a new image object from a storage.Image. Its "input name" will be its ID. -func (ir *Runtime) newFromStorage(img *storage.Image) *Image { - return ir.newImage(img.ID, img) -} - -// NewFromLocal creates a new image object that is intended -// to only deal with local images already in the store (or -// its aliases) -func (ir *Runtime) NewFromLocal(name string) (*Image, error) { - updatedInputName, localImage, err := ir.getLocalImage(name) - if err != nil { - return nil, err - } - return ir.newImage(updatedInputName, localImage), nil -} - -// New creates a new image object where the image could be local -// or remote -func (ir *Runtime) New(ctx context.Context, name, signaturePolicyPath, authfile string, writer io.Writer, dockeroptions *DockerRegistryOptions, signingoptions SigningOptions, label *string, pullType util.PullType, progress chan types.ProgressProperties) (*Image, error) { - // We don't know if the image is local or not ... check local first - if pullType != util.PullImageAlways { - newImage, err := ir.NewFromLocal(name) - if err == nil { - return newImage, nil - } else if pullType == util.PullImageNever { - return nil, err - } - } - - // The image is not local - if signaturePolicyPath == "" { - signaturePolicyPath = ir.SignaturePolicyPath - } - imageName, err := ir.pullImageFromHeuristicSource(ctx, name, writer, authfile, signaturePolicyPath, signingoptions, dockeroptions, &retry.RetryOptions{MaxRetry: maxRetry}, label, progress) - if err != nil { - return nil, err - } - - newImage, err := ir.NewFromLocal(imageName[0]) - if err != nil { - return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name) - } - return newImage, nil -} - -// SaveImages stores one more images in a multi-image archive. -// Note that only `docker-archive` supports storing multiple -// image. -func (ir *Runtime) SaveImages(ctx context.Context, namesOrIDs []string, format string, outputFile string, quiet, removeSignatures bool) (finalErr error) { - if format != DockerArchive { - return errors.Errorf("multi-image archives are only supported in in the %q format", DockerArchive) - } - - sys := GetSystemContext("", "", false) - - archWriter, err := dockerarchive.NewWriter(sys, outputFile) - if err != nil { - return err - } - defer func() { - err := archWriter.Close() - if err == nil { - return - } - if finalErr == nil { - finalErr = err - return - } - finalErr = errors.Wrap(finalErr, err.Error()) - }() - - // Decide whether c/image's progress bars should use stderr or stdout. - // Use stderr in case we need to be quiet or if the output is set to - // stdout. If the output is set of stdout, any log message there would - // corrupt the tarfile. - writer := os.Stdout - if quiet { - writer = os.Stderr - } - - // extend an image with additional tags - type imageData struct { - *Image - tags []reference.NamedTagged - } - - // Look up the images (and their tags) in the local storage. - imageMap := make(map[string]*imageData) // to group tags for an image - imageQueue := []string{} // to preserve relative image order - for _, nameOrID := range namesOrIDs { - // Look up the name or ID in the local image storage. - localImage, err := ir.NewFromLocal(nameOrID) - if err != nil { - return err - } - id := localImage.ID() - - iData, exists := imageMap[id] - if !exists { - imageQueue = append(imageQueue, id) - iData = &imageData{Image: localImage} - imageMap[id] = iData - } - - // Unless we referred to an ID, add the input as a tag. - if !strings.HasPrefix(id, nameOrID) { - tag, err := NormalizedTag(nameOrID) - if err != nil { - return err - } - refTagged, isTagged := tag.(reference.NamedTagged) - if isTagged { - iData.tags = append(iData.tags, refTagged) - } - } - } - - policyContext, err := getPolicyContext(sys) - if err != nil { - return err - } - defer func() { - if err := policyContext.Destroy(); err != nil { - logrus.Errorf("failed to destroy policy context: %q", err) - } - }() - - // Now copy the images one-by-one. - for _, id := range imageQueue { - dest, err := archWriter.NewReference(nil) - if err != nil { - return err - } - - img := imageMap[id] - copyOptions := getCopyOptions(sys, writer, nil, nil, SigningOptions{RemoveSignatures: removeSignatures}, "", img.tags) - copyOptions.DestinationCtx.SystemRegistriesConfPath = registries.SystemRegistriesConfPath() - - // For copying, we need a source reference that we can create - // from the image. - src, err := is.Transport.NewStoreReference(img.imageruntime.store, nil, id) - if err != nil { - return errors.Wrapf(err, "error getting source imageReference for %q", img.InputName) - } - _, err = cp.Image(ctx, policyContext, dest, src, copyOptions) - if err != nil { - return err - } - } - - return nil -} - -// LoadAllImagesFromDockerArchive loads all images from the docker archive that -// fileName points to. -func (ir *Runtime) LoadAllImagesFromDockerArchive(ctx context.Context, fileName string, signaturePolicyPath string, writer io.Writer) ([]*Image, error) { - if signaturePolicyPath == "" { - signaturePolicyPath = ir.SignaturePolicyPath - } - - sc := GetSystemContext(signaturePolicyPath, "", false) - reader, err := dockerarchive.NewReader(sc, fileName) - if err != nil { - return nil, err - } - - defer func() { - if err := reader.Close(); err != nil { - logrus.Errorf(err.Error()) - } - }() - - refLists, err := reader.List() - if err != nil { - return nil, err - } - - refPairs := []pullRefPair{} - for _, refList := range refLists { - for _, ref := range refList { - pairs, err := ir.getPullRefPairsFromDockerArchiveReference(ctx, reader, ref, sc) - if err != nil { - return nil, err - } - refPairs = append(refPairs, pairs...) - } - } - - goal := pullGoal{ - pullAllPairs: true, - refPairs: refPairs, - } - - defer goal.cleanUp() - imageNames, err := ir.doPullImage(ctx, sc, goal, writer, SigningOptions{}, &DockerRegistryOptions{}, &retry.RetryOptions{}, nil, nil) - if err != nil { - return nil, err - } - - newImages := make([]*Image, 0, len(imageNames)) - for _, name := range imageNames { - newImage, err := ir.NewFromLocal(name) - if err != nil { - return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name) - } - newImages = append(newImages, newImage) - } - ir.newImageEvent(events.LoadFromArchive, "") - return newImages, nil -} - -// LoadFromArchiveReference creates a new image object for images pulled from a tar archive and the like (podman load) -// This function is needed because it is possible for a tar archive to have multiple tags for one image -func (ir *Runtime) LoadFromArchiveReference(ctx context.Context, srcRef types.ImageReference, signaturePolicyPath string, writer io.Writer) ([]*Image, error) { - if signaturePolicyPath == "" { - signaturePolicyPath = ir.SignaturePolicyPath - } - - imageNames, err := ir.pullImageFromReference(ctx, srcRef, writer, "", signaturePolicyPath, SigningOptions{}, &DockerRegistryOptions{}, &retry.RetryOptions{}) - if err != nil { - return nil, errors.Wrapf(err, "unable to pull %s", transports.ImageName(srcRef)) - } - - newImages := make([]*Image, 0, len(imageNames)) - for _, name := range imageNames { - newImage, err := ir.NewFromLocal(name) - if err != nil { - return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name) - } - newImages = append(newImages, newImage) - } - ir.newImageEvent(events.LoadFromArchive, "") - return newImages, nil -} - -// Shutdown closes down the storage and require a bool arg as to -// whether it should do so forcibly. -func (ir *Runtime) Shutdown(force bool) error { - _, err := ir.store.Shutdown(force) - return err -} - -// GetImagesWithFilters gets images with a series of filters applied -func (ir *Runtime) GetImagesWithFilters(filters []string) ([]*Image, error) { - filterFuncs, err := ir.createFilterFuncs(filters, nil) - if err != nil { - return nil, err - } - images, err := ir.GetImages() - if err != nil { - return nil, err - } - return FilterImages(images, filterFuncs), nil -} - -func (i *Image) reloadImage() error { - newImage, err := i.imageruntime.getImage(i.ID()) - if err != nil { - return errors.Wrapf(err, "unable to reload image") - } - i.image = newImage - return nil -} - -// stringSha256 strips sha256 from user input -func stripSha256(name string) string { - if strings.HasPrefix(name, "sha256:") && len(name) > 7 { - return name[7:] - } - return name -} - -// getLocalImage resolves an unknown input describing an image and -// returns an updated input name, and a storage.Image, or an error. It is used by NewFromLocal. -func (ir *Runtime) getLocalImage(inputName string) (string, *storage.Image, error) { - imageError := fmt.Sprintf("unable to find '%s' in local storage", inputName) - if inputName == "" { - return "", nil, errors.Errorf("input name is blank") - } - - // Check if the input name has a transport and if so strip it - dest, err := alltransports.ParseImageName(inputName) - if err == nil && dest.DockerReference() != nil { - inputName = dest.DockerReference().String() - } - - // Early check for fully-qualified images and (short) IDs. - img, err := ir.store.Image(stripSha256(inputName)) - if err == nil { - return inputName, img, nil - } - - // Note that it's crucial to first decompose the image and check if - // it's a fully-qualified one or a "short name". The latter requires - // some normalization with search registries and the - // "localhost/prefix". - decomposedImage, err := decompose(inputName) - if err != nil { - // We may have a storage reference. We can't parse it to a - // reference before. Otherwise, we'd normalize "alpine" to - // "docker.io/library/alpine:latest" which would break the - // order in which we should query local images below. - if ref, err := is.Transport.ParseStoreReference(ir.store, inputName); err == nil { - img, err = is.Transport.GetStoreImage(ir.store, ref) - if err == nil { - return inputName, img, nil - } - } - return "", nil, err - } - - // The specified image is fully qualified, so it doesn't exist in the - // storage. - if decomposedImage.hasRegistry { - // However ... we may still need to normalize to docker.io: - // `docker.io/foo` -> `docker.io/library/foo` - if ref, err := is.Transport.ParseStoreReference(ir.store, inputName); err == nil { - img, err = is.Transport.GetStoreImage(ir.store, ref) - if err == nil { - return inputName, img, nil - } - } - return "", nil, errors.Wrapf(ErrNoSuchImage, imageError) - } - - sys := &types.SystemContext{ - SystemRegistriesConfPath: registries.SystemRegistriesConfPath(), - } - - candidates, err := shortnames.ResolveLocally(sys, inputName) - if err != nil { - return "", nil, err - } - - for _, candidate := range candidates { - img, err := ir.store.Image(candidate.String()) - if err == nil { - return candidate.String(), img, nil - } - } - - // Backwards compat: normalize to docker.io as some users may very well - // rely on that. - ref, err := is.Transport.ParseStoreReference(ir.store, inputName) - if err == nil { - img, err = is.Transport.GetStoreImage(ir.store, ref) - if err == nil { - return inputName, img, nil - } - } - - // Last resort: look at the repotags of all images and try to find a - // match. - images, err := ir.GetImages() - if err != nil { - return "", nil, err - } - - decomposedImage, err = decompose(inputName) - if err != nil { - return "", nil, err - } - repoImage, err := findImageInRepotags(decomposedImage, images) - if err == nil { - return inputName, repoImage, nil - } - - return "", nil, err -} - -// ID returns the image ID as a string -func (i *Image) ID() string { - return i.image.ID -} - -// IsReadOnly returns whether the image ID comes from a local store -func (i *Image) IsReadOnly() bool { - return i.image.ReadOnly -} - -// Digest returns the image's digest -func (i *Image) Digest() digest.Digest { - return i.image.Digest -} - -// Digests returns the image's digests -func (i *Image) Digests() []digest.Digest { - return i.image.Digests -} - -// GetManifest returns the image's manifest as a byte array -// and manifest type as a string. -func (i *Image) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) { - imgSrcRef, err := i.toImageSourceRef(ctx) - if err != nil { - return nil, "", err - } - return imgSrcRef.GetManifest(ctx, instanceDigest) -} - -// Manifest returns the image's manifest as a byte array -// and manifest type as a string. -func (i *Image) Manifest(ctx context.Context) ([]byte, string, error) { - imgRef, err := i.toImageRef(ctx) - if err != nil { - return nil, "", err - } - return imgRef.Manifest(ctx) -} - -// Names returns a string array of names associated with the image, which may be a mixture of tags and digests -func (i *Image) Names() []string { - return i.image.Names -} - -// NamesHistory returns a string array of names previously associated with the -// image, which may be a mixture of tags and digests -func (i *Image) NamesHistory() []string { - if len(i.image.Names) > 0 && len(i.image.NamesHistory) > 0 && - // We compare the latest (time-referenced) tags for equality and skip - // it in the history if they match to not display them twice. We have - // to compare like this, because `i.image.Names` (latest last) gets - // appended on retag, whereas `i.image.NamesHistory` gets prepended - // (latest first) - i.image.Names[len(i.image.Names)-1] == i.image.NamesHistory[0] { - return i.image.NamesHistory[1:] - } - return i.image.NamesHistory -} - -// RepoTags returns a string array of repotags associated with the image -func (i *Image) RepoTags() ([]string, error) { - var repoTags []string - for _, name := range i.Names() { - named, err := reference.ParseNormalizedNamed(name) - if err != nil { - return nil, err - } - if tagged, isTagged := named.(reference.NamedTagged); isTagged { - repoTags = append(repoTags, tagged.String()) - } - } - return repoTags, nil -} - -// RepoDigests returns a string array of repodigests associated with the image -func (i *Image) RepoDigests() ([]string, error) { - var repoDigests []string - added := make(map[string]struct{}) - - for _, name := range i.Names() { - for _, imageDigest := range append(i.Digests(), i.Digest()) { - if imageDigest == "" { - continue - } - - named, err := reference.ParseNormalizedNamed(name) - if err != nil { - return nil, err - } - - canonical, err := reference.WithDigest(reference.TrimNamed(named), imageDigest) - if err != nil { - return nil, err - } - - if _, alreadyInList := added[canonical.String()]; !alreadyInList { - repoDigests = append(repoDigests, canonical.String()) - added[canonical.String()] = struct{}{} - } - } - } - sort.Strings(repoDigests) - return repoDigests, nil -} - -// Created returns the time the image was created -func (i *Image) Created() time.Time { - return i.image.Created -} - -// TopLayer returns the top layer id as a string -func (i *Image) TopLayer() string { - return i.image.TopLayer -} - -// Remove an image; container removal for the image must be done -// outside the context of images -// TODO: the force param does nothing as of now. Need to move container -// handling logic here eventually. -func (i *Image) Remove(ctx context.Context, force bool) error { - parent, err := i.GetParent(ctx) - if err != nil { - logrus.Warnf("error determining parent of image: %v, ignoring the error", err) - parent = nil - } - if _, err := i.imageruntime.store.DeleteImage(i.ID(), true); err != nil { - return err - } - i.newImageEvent(events.Remove) - for parent != nil { - nextParent, err := parent.GetParent(ctx) - if err != nil { - return err - } - children, err := parent.GetChildren(ctx) - if err != nil { - return err - } - // Do not remove if image is a base image and is not untagged, or if - // the image has more children. - if len(children) > 0 || len(parent.Names()) > 0 { - return nil - } - id := parent.ID() - if _, err := i.imageruntime.store.DeleteImage(id, true); err != nil { - logrus.Debugf("unable to remove intermediate image %q: %v", id, err) - } else { - fmt.Println(id) - } - parent = nextParent - } - return nil -} - -// getImage retrieves an image matching the given name or hash from system -// storage -// If no matching image can be found, an error is returned -func (ir *Runtime) getImage(image string) (*storage.Image, error) { - var img *storage.Image - ref, err := is.Transport.ParseStoreReference(ir.store, image) - if err == nil { - img, err = is.Transport.GetStoreImage(ir.store, ref) - } - if err != nil { - img2, err2 := ir.store.Image(image) - if err2 != nil { - if ref == nil { - return nil, errors.Wrapf(err, "error parsing reference to image %q", image) - } - return nil, errors.Wrapf(err, "unable to locate image %q", image) - } - img = img2 - } - return img, nil -} - -func (ir *Runtime) ImageNames(id string) ([]string, error) { - myImage, err := ir.getImage(id) - if err != nil { - return nil, errors.Wrapf(err, "error getting image %s ", id) - } - return myImage.Names, nil -} - -// GetImages retrieves all images present in storage -func (ir *Runtime) GetImages() ([]*Image, error) { - return ir.getImages(false) -} - -// GetRWImages retrieves all read/write images present in storage -func (ir *Runtime) GetRWImages() ([]*Image, error) { - return ir.getImages(true) -} - -// getImages retrieves all images present in storage -func (ir *Runtime) getImages(rwOnly bool) ([]*Image, error) { - images, err := ir.store.Images() - if err != nil { - return nil, err - } - newImages := []*Image{} - for _, i := range images { - if rwOnly && i.ReadOnly { - continue - } - // iterating over these, be careful to not iterate on the literal - // pointer. - image := i - img := ir.newFromStorage(&image) - newImages = append(newImages, img) - } - return newImages, nil -} - -// getImageDigest creates an image object and uses the hex value of the digest as the image ID -// for parsing the store reference -func getImageDigest(ctx context.Context, src types.ImageReference, sc *types.SystemContext) (string, error) { - newImg, err := src.NewImage(ctx, sc) - if err != nil { - return "", err - } - defer func() { - if err := newImg.Close(); err != nil { - logrus.Errorf("failed to close image: %q", err) - } - }() - imageDigest := newImg.ConfigInfo().Digest - if err = imageDigest.Validate(); err != nil { - return "", errors.Wrapf(err, "error getting config info") - } - return "@" + imageDigest.Hex(), nil -} - -// NormalizedTag returns the canonical version of tag for use in Image.Names() -func NormalizedTag(tag string) (reference.Named, error) { - decomposedTag, err := decompose(tag) - if err != nil { - return nil, err - } - // If the input doesn't specify a registry, set the registry to localhost - var ref reference.Named - if !decomposedTag.hasRegistry { - ref, err = decomposedTag.referenceWithRegistry(DefaultLocalRegistry) - if err != nil { - return nil, err - } - } else { - ref, err = decomposedTag.normalizedReference() - if err != nil { - return nil, err - } - } - // If the input does not have a tag, we need to add one (latest) - ref = reference.TagNameOnly(ref) - return ref, nil -} - -// TagImage adds a tag to the given image -func (i *Image) TagImage(tag string) error { - if err := i.reloadImage(); err != nil { - return err - } - ref, err := NormalizedTag(tag) - if err != nil { - return err - } - tags := i.Names() - if util.StringInSlice(ref.String(), tags) { - return nil - } - tags = append(tags, ref.String()) - if err := i.imageruntime.store.SetNames(i.ID(), tags); err != nil { - return err - } - if err := i.reloadImage(); err != nil { - return err - } - i.newImageEvent(events.Tag) - return nil -} - -// UntagImage removes the specified tag from the image. -// If the tag does not exist, ErrNoSuchTag is returned. -func (i *Image) UntagImage(tag string) error { - if err := i.reloadImage(); err != nil { - return err - } - - // Normalize the tag as we do with TagImage. - ref, err := NormalizedTag(tag) - if err != nil { - return err - } - tag = ref.String() - - var newTags []string - tags := i.Names() - if !util.StringInSlice(tag, tags) { - return errors.Wrapf(ErrNoSuchTag, "%q", tag) - } - for _, t := range tags { - if tag != t { - newTags = append(newTags, t) - } - } - if err := i.imageruntime.store.SetNames(i.ID(), newTags); err != nil { - return err - } - if err := i.reloadImage(); err != nil { - return err - } - i.newImageEvent(events.Untag) - return nil -} - -// PushImageToHeuristicDestination pushes the given image to "destination", which is heuristically parsed. -// Use PushImageToReference if the destination is known precisely. -func (i *Image) PushImageToHeuristicDestination(ctx context.Context, destination, manifestMIMEType, authFile, digestFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged, progress chan types.ProgressProperties) error { - if destination == "" { - return errors.Wrapf(syscall.EINVAL, "destination image name must be specified") - } - - // Get the destination Image Reference - dest, err := alltransports.ParseImageName(destination) - if err != nil { - if hasTransport(destination) { - return errors.Wrapf(err, "error getting destination imageReference for %q", destination) - } - // Try adding the images default transport - destination2 := DefaultTransport + destination - dest, err = alltransports.ParseImageName(destination2) - if err != nil { - return err - } - } - return i.PushImageToReference(ctx, dest, manifestMIMEType, authFile, digestFile, signaturePolicyPath, writer, forceCompress, signingOptions, dockerRegistryOptions, additionalDockerArchiveTags, progress) -} - -// PushImageToReference pushes the given image to a location described by the given path -func (i *Image) PushImageToReference(ctx context.Context, dest types.ImageReference, manifestMIMEType, authFile, digestFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged, progress chan types.ProgressProperties) error { - sc := GetSystemContext(signaturePolicyPath, authFile, forceCompress) - sc.BlobInfoCacheDir = filepath.Join(i.imageruntime.store.GraphRoot(), "cache") - - policyContext, err := getPolicyContext(sc) - if err != nil { - return err - } - defer func() { - if err := policyContext.Destroy(); err != nil { - logrus.Errorf("failed to destroy policy context: %q", err) - } - }() - - // Look up the source image, expecting it to be in local storage - src, err := is.Transport.ParseStoreReference(i.imageruntime.store, i.ID()) - if err != nil { - return errors.Wrapf(err, "error getting source imageReference for %q", i.InputName) - } - copyOptions := getCopyOptions(sc, writer, nil, dockerRegistryOptions, signingOptions, manifestMIMEType, additionalDockerArchiveTags) - copyOptions.DestinationCtx.SystemRegistriesConfPath = registries.SystemRegistriesConfPath() // FIXME: Set this more globally. Probably no reason not to have it in every types.SystemContext, and to compute the value just once in one place. - if progress != nil { - copyOptions.Progress = progress - copyOptions.ProgressInterval = time.Second - } - // Copy the image to the remote destination - manifestBytes, err := cp.Image(ctx, policyContext, dest, src, copyOptions) - if err != nil { - return errors.Wrapf(err, "error copying image to the remote destination") - } - digest, err := manifest.Digest(manifestBytes) - if err != nil { - return errors.Wrapf(err, "error computing digest of manifest of new image %q", transports.ImageName(dest)) - } - - logrus.Debugf("Successfully pushed %s with digest %s", transports.ImageName(dest), digest.String()) - - if digestFile != "" { - if err = ioutil.WriteFile(digestFile, []byte(digest.String()), 0644); err != nil { - return errors.Wrapf(err, "failed to write digest to file %q", digestFile) - } - } - i.newImageEvent(events.Push) - return nil -} - -// MatchesID returns a bool based on if the input id -// matches the image's id -// TODO: This isn't used anywhere, so remove it -func (i *Image) MatchesID(id string) bool { - return strings.HasPrefix(i.ID(), id) -} - -// ToImageRef returns an image reference type from an image -// TODO: Hopefully we can remove this exported function for mheon -func (i *Image) ToImageRef(ctx context.Context) (types.Image, error) { - return i.toImageRef(ctx) -} - -// toImageSourceRef returns an ImageSource Reference type from an image -func (i *Image) toImageSourceRef(ctx context.Context) (types.ImageSource, error) { - if i == nil { - return nil, errors.Errorf("cannot convert nil image to image source reference") - } - if i.imgSrcRef == nil { - ref, err := is.Transport.ParseStoreReference(i.imageruntime.store, "@"+i.ID()) - if err != nil { - return nil, errors.Wrapf(err, "error parsing reference to image %q", i.ID()) - } - imgSrcRef, err := ref.NewImageSource(ctx, nil) - if err != nil { - return nil, errors.Wrapf(err, "error reading image %q as image source", i.ID()) - } - i.imgSrcRef = imgSrcRef - } - return i.imgSrcRef, nil -} - -//Size returns the size of the image -func (i *Image) Size(ctx context.Context) (*uint64, error) { - sum, err := i.imageruntime.store.ImageSize(i.ID()) - if err == nil && sum >= 0 { - usum := uint64(sum) - return &usum, nil - } - return nil, errors.Wrap(err, "unable to determine size") -} - -// toImageRef returns an Image Reference type from an image -func (i *Image) toImageRef(ctx context.Context) (types.Image, error) { - if i == nil { - return nil, errors.Errorf("cannot convert nil image to image reference") - } - imgSrcRef, err := i.toImageSourceRef(ctx) - if err != nil { - return nil, err - } - if i.imgRef == nil { - systemContext := &types.SystemContext{} - unparsedDefaultInstance := image.UnparsedInstance(imgSrcRef, nil) - imgRef, err := image.FromUnparsedImage(ctx, systemContext, unparsedDefaultInstance) - if err != nil { - // check for a "tried-to-treat-a-bare-list-like-a-runnable-image" problem, else - // return info about the not-a-bare-list runnable image part of this storage.Image - if manifestBytes, manifestType, err2 := imgSrcRef.GetManifest(ctx, nil); err2 == nil { - if manifest.MIMETypeIsMultiImage(manifestType) { - if list, err3 := manifest.ListFromBlob(manifestBytes, manifestType); err3 == nil { - switch manifestType { - case ociv1.MediaTypeImageIndex: - err = errors.Wrapf(ErrImageIsBareList, "%q is an image index", i.InputName) - case manifest.DockerV2ListMediaType: - err = errors.Wrapf(ErrImageIsBareList, "%q is a manifest list", i.InputName) - default: - err = errors.Wrapf(ErrImageIsBareList, "%q", i.InputName) - } - for _, instanceDigest := range list.Instances() { - instance := instanceDigest - unparsedInstance := image.UnparsedInstance(imgSrcRef, &instance) - if imgRef2, err4 := image.FromUnparsedImage(ctx, systemContext, unparsedInstance); err4 == nil { - imgRef = imgRef2 - err = nil - break - } - } - } - } - } - if err != nil { - return nil, errors.Wrapf(err, "error reading image %q as image", i.ID()) - } - } - i.imgRef = imgRef - } - return i.imgRef, nil -} - -// DriverData gets the driver data from the store on a layer -func (i *Image) DriverData() (*define.DriverData, error) { - return driver.GetDriverData(i.imageruntime.store, i.TopLayer()) -} - -// Layer returns the image's top layer -func (i *Image) Layer() (*storage.Layer, error) { - return i.imageruntime.store.Layer(i.image.TopLayer) -} - -// History contains the history information of an image -type History struct { - ID string `json:"id"` - Created *time.Time `json:"created"` - CreatedBy string `json:"createdBy"` - Size int64 `json:"size"` - Comment string `json:"comment"` - Tags []string `json:"tags"` -} - -// History gets the history of an image and the IDs of images that are part of -// its history -func (i *Image) History(ctx context.Context) ([]*History, error) { - img, err := i.toImageRef(ctx) - if err != nil { - if errors.Cause(err) == ErrImageIsBareList { - return nil, nil - } - return nil, err - } - oci, err := img.OCIConfig(ctx) - if err != nil { - return nil, err - } - - // Build a mapping from top-layer to image ID. - images, err := i.imageruntime.GetImages() - if err != nil { - return nil, err - } - topLayerMap := make(map[string]string) - for _, image := range images { - if _, exists := topLayerMap[image.TopLayer()]; !exists { - topLayerMap[image.TopLayer()] = image.ID() - } - } - - var allHistory []*History - var layer *storage.Layer - - // Check if we have an actual top layer to prevent lookup errors. - if i.TopLayer() != "" { - layer, err = i.imageruntime.store.Layer(i.TopLayer()) - if err != nil { - return nil, err - } - } - - // Iterate in reverse order over the history entries, and lookup the - // corresponding image ID, size and get the next later if needed. - numHistories := len(oci.History) - 1 - for x := numHistories; x >= 0; x-- { - var size int64 - - id := "" - if x == numHistories { - id = i.ID() - } - if layer != nil { - if !oci.History[x].EmptyLayer { - size = layer.UncompressedSize - } - if imageID, exists := topLayerMap[layer.ID]; exists { - id = imageID - // Delete the entry to avoid reusing it for following history items. - delete(topLayerMap, layer.ID) - } - } - h := History{ - ID: id, - Created: oci.History[x].Created, - CreatedBy: oci.History[x].CreatedBy, - Size: size, - Comment: oci.History[x].Comment, - } - if layer != nil { - h.Tags = layer.Names - } - allHistory = append(allHistory, &h) - - if layer != nil && layer.Parent != "" && !oci.History[x].EmptyLayer { - layer, err = i.imageruntime.store.Layer(layer.Parent) - if err != nil { - return nil, err - } - } - } - - return allHistory, nil -} - -// Dangling returns a bool if the image is "dangling" -func (i *Image) Dangling() bool { - return len(i.Names()) == 0 -} - -// User returns the image's user -func (i *Image) User(ctx context.Context) (string, error) { - imgInspect, err := i.inspect(ctx, false) - if err != nil { - return "", err - } - return imgInspect.Config.User, nil -} - -// StopSignal returns the image's StopSignal -func (i *Image) StopSignal(ctx context.Context) (string, error) { - imgInspect, err := i.inspect(ctx, false) - if err != nil { - return "", err - } - return imgInspect.Config.StopSignal, nil -} - -// WorkingDir returns the image's WorkingDir -func (i *Image) WorkingDir(ctx context.Context) (string, error) { - imgInspect, err := i.inspect(ctx, false) - if err != nil { - return "", err - } - return imgInspect.Config.WorkingDir, nil -} - -// Cmd returns the image's cmd -func (i *Image) Cmd(ctx context.Context) ([]string, error) { - imgInspect, err := i.inspect(ctx, false) - if err != nil { - return nil, err - } - return imgInspect.Config.Cmd, nil -} - -// Entrypoint returns the image's entrypoint -func (i *Image) Entrypoint(ctx context.Context) ([]string, error) { - imgInspect, err := i.inspect(ctx, false) - if err != nil { - return nil, err - } - return imgInspect.Config.Entrypoint, nil -} - -// Env returns the image's env -func (i *Image) Env(ctx context.Context) ([]string, error) { - imgInspect, err := i.imageInspectInfo(ctx) - if err != nil { - return nil, err - } - return imgInspect.Env, nil -} - -// Labels returns the image's labels -func (i *Image) Labels(ctx context.Context) (map[string]string, error) { - imgInspect, err := i.imageInspectInfo(ctx) - if err != nil { - return nil, err - } - return imgInspect.Labels, nil -} - -// GetLabel Returns a case-insensitive match of a given label -func (i *Image) GetLabel(ctx context.Context, label string) (string, error) { - labels, err := i.Labels(ctx) - if err != nil { - return "", err - } - - for k, v := range labels { - if strings.EqualFold(k, label) { - return v, nil - } - } - return "", nil -} - -// Annotations returns the annotations of an image -func (i *Image) Annotations(ctx context.Context) (map[string]string, error) { - imageManifest, manifestType, err := i.Manifest(ctx) - if err != nil { - imageManifest, manifestType, err = i.GetManifest(ctx, nil) - if err != nil { - return nil, err - } - } - annotations := make(map[string]string) - if manifestType == ociv1.MediaTypeImageManifest { - var m ociv1.Manifest - if err := json.Unmarshal(imageManifest, &m); err == nil { - for k, v := range m.Annotations { - annotations[k] = v - } - } - } - return annotations, nil -} - -// ociv1Image converts an image to an imgref and then returns its config blob -// converted to an ociv1 image type -func (i *Image) ociv1Image(ctx context.Context) (*ociv1.Image, error) { - imgRef, err := i.toImageRef(ctx) - if err != nil { - return nil, err - } - return imgRef.OCIConfig(ctx) -} - -func (i *Image) imageInspectInfo(ctx context.Context) (*types.ImageInspectInfo, error) { - if i.inspectInfo == nil { - ic, err := i.toImageRef(ctx) - if err != nil { - return nil, err - } - imgInspect, err := ic.Inspect(ctx) - if err != nil { - return nil, err - } - i.inspectInfo = imgInspect - } - return i.inspectInfo, nil -} - -func (i *Image) inspect(ctx context.Context, calculateSize bool) (*inspect.ImageData, error) { - ociv1Img, err := i.ociv1Image(ctx) - if err != nil { - ociv1Img = &ociv1.Image{} - } - info, err := i.imageInspectInfo(ctx) - if err != nil { - info = &types.ImageInspectInfo{} - } - annotations, err := i.Annotations(ctx) - if err != nil { - return nil, err - } - - size := int64(-1) - if calculateSize { - if usize, err := i.Size(ctx); err == nil { - size = int64(*usize) - } - } - - parent, err := i.ParentID(ctx) - if err != nil { - return nil, err - } - - repoTags, err := i.RepoTags() - if err != nil { - return nil, err - } - - repoDigests, err := i.RepoDigests() - if err != nil { - return nil, err - } - - driver, err := i.DriverData() - if err != nil { - return nil, err - } - - _, manifestType, err := i.GetManifest(ctx, nil) - if err != nil { - return nil, errors.Wrapf(err, "unable to determine manifest type") - } - comment, err := i.Comment(ctx, manifestType) - if err != nil { - return nil, err - } - - data := &inspect.ImageData{ - ID: i.ID(), - Parent: parent, - RepoTags: repoTags, - RepoDigests: repoDigests, - Comment: comment, - Created: ociv1Img.Created, - Author: ociv1Img.Author, - Architecture: ociv1Img.Architecture, - Os: ociv1Img.OS, - Config: &ociv1Img.Config, - Version: info.DockerVersion, - Size: size, - // This is good enough for now, but has to be - // replaced later with correct calculation logic - VirtualSize: size, - Annotations: annotations, - Digest: i.Digest(), - Labels: info.Labels, - RootFS: &inspect.RootFS{ - Type: ociv1Img.RootFS.Type, - Layers: ociv1Img.RootFS.DiffIDs, - }, - GraphDriver: driver, - ManifestType: manifestType, - User: ociv1Img.Config.User, - History: ociv1Img.History, - NamesHistory: i.NamesHistory(), - } - if manifestType == manifest.DockerV2Schema2MediaType { - hc, err := i.GetHealthCheck(ctx) - if err != nil { - return nil, err - } - if hc != nil { - data.HealthCheck = hc - } - } - return data, nil -} - -// Inspect returns an image's inspect data -func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) { - return i.inspect(ctx, true) -} - -// InspectNoSize returns an image's inspect data without calculating the size for the image -func (i *Image) InspectNoSize(ctx context.Context) (*inspect.ImageData, error) { - return i.inspect(ctx, false) -} - -// Import imports and image into the store and returns an image -func (ir *Runtime) Import(ctx context.Context, path, reference string, writer io.Writer, signingOptions SigningOptions, imageConfig ociv1.Image) (*Image, error) { - src, err := tarball.Transport.ParseReference(path) - if err != nil { - return nil, errors.Wrapf(err, "error parsing image name %q", path) - } - - updater, ok := src.(tarball.ConfigUpdater) - if !ok { - return nil, errors.Wrapf(err, "unexpected type, a tarball reference should implement tarball.ConfigUpdater") - } - - annotations := make(map[string]string) - - // config ociv1.Image - err = updater.ConfigUpdate(imageConfig, annotations) - if err != nil { - return nil, errors.Wrapf(err, "error updating image config") - } - - sc := GetSystemContext(ir.SignaturePolicyPath, "", false) - - // if reference not given, get the image digest - if reference == "" { - reference, err = getImageDigest(ctx, src, sc) - if err != nil { - return nil, err - } - } - policyContext, err := getPolicyContext(sc) - if err != nil { - return nil, err - } - defer func() { - if err := policyContext.Destroy(); err != nil { - logrus.Errorf("failed to destroy policy context: %q", err) - } - }() - copyOptions := getCopyOptions(sc, writer, nil, nil, signingOptions, "", nil) - dest, err := is.Transport.ParseStoreReference(ir.store, reference) - if err != nil { - return nil, errors.Wrapf(err, "error getting image reference for %q", reference) - } - _, err = cp.Image(ctx, policyContext, dest, src, copyOptions) - if err != nil { - return nil, err - } - newImage, err := ir.NewFromLocal(reference) - if err == nil { - newImage.newImageEvent(events.Import) - } - return newImage, err -} - -// MatchRepoTag takes a string and tries to match it against an -// image's repotags -func (i *Image) MatchRepoTag(input string) (string, error) { - results := make(map[int][]string) - var maxCount int - // first check if we have an exact match with the input - if util.StringInSlice(input, i.Names()) { - return input, nil - } - // next check if we are missing the tag - dcImage, err := decompose(input) - if err != nil { - return "", err - } - imageRegistry, imageName, imageSuspiciousTagValueForSearch := dcImage.suspiciousRefNameTagValuesForSearch() - for _, repoName := range i.Names() { - count := 0 - dcRepoName, err := decompose(repoName) - if err != nil { - return "", err - } - repoNameRegistry, repoNameName, repoNameSuspiciousTagValueForSearch := dcRepoName.suspiciousRefNameTagValuesForSearch() - if repoNameRegistry == imageRegistry && imageRegistry != "" { - count++ - } - if repoNameName == imageName && imageName != "" { - count++ - } else if splitString(repoNameName) == splitString(imageName) { - count++ - } - if repoNameSuspiciousTagValueForSearch == imageSuspiciousTagValueForSearch { - count++ - } - results[count] = append(results[count], repoName) - if count > maxCount { - maxCount = count - } - } - if maxCount == 0 { - return "", ErrRepoTagNotFound - } - if len(results[maxCount]) > 1 { - return "", errors.Errorf("user input matched multiple repotags for the image") - } - return results[maxCount][0], nil -} - -// splitString splits input string by / and returns the last array item -func splitString(input string) string { - split := strings.Split(input, "/") - return split[len(split)-1] -} - -// IsParent goes through the layers in the store and checks if i.TopLayer is -// the parent of any other layer in store. Double check that image with that -// layer exists as well. -func (i *Image) IsParent(ctx context.Context) (bool, error) { - children, err := i.getChildren(ctx, false) - if err != nil { - if errors.Cause(err) == ErrImageIsBareList { - return false, nil - } - return false, err - } - return len(children) > 0, nil -} - -// historiesMatch returns the number of entries in the histories which have the -// same contents -func historiesMatch(a, b []ociv1.History) int { - i := 0 - for i < len(a) && i < len(b) { - if a[i].Created != nil && b[i].Created == nil { - return i - } - if a[i].Created == nil && b[i].Created != nil { - return i - } - if a[i].Created != nil && b[i].Created != nil { - if !a[i].Created.Equal(*(b[i].Created)) { - return i - } - } - if a[i].CreatedBy != b[i].CreatedBy { - return i - } - if a[i].Author != b[i].Author { - return i - } - if a[i].Comment != b[i].Comment { - return i - } - if a[i].EmptyLayer != b[i].EmptyLayer { - return i - } - i++ - } - return i -} - -// areParentAndChild checks diff ID and history in the two images and return -// true if the second should be considered to be directly based on the first -func areParentAndChild(parent, child *ociv1.Image) bool { - // the child and candidate parent should share all of the - // candidate parent's diff IDs, which together would have - // controlled which layers were used - - // Both, child and parent, may be nil when the storage is left in an - // incoherent state. Issue #7444 describes such a case when a build - // has been killed. - if child == nil || parent == nil { - return false - } - - if len(parent.RootFS.DiffIDs) > len(child.RootFS.DiffIDs) { - return false - } - childUsesCandidateDiffs := true - for i := range parent.RootFS.DiffIDs { - if child.RootFS.DiffIDs[i] != parent.RootFS.DiffIDs[i] { - childUsesCandidateDiffs = false - break - } - } - if !childUsesCandidateDiffs { - return false - } - // the child should have the same history as the parent, plus - // one more entry - if len(parent.History)+1 != len(child.History) { - return false - } - if historiesMatch(parent.History, child.History) != len(parent.History) { - return false - } - return true -} - -// GetParent returns the image ID of the parent. Return nil if a parent is not found. -func (i *Image) GetParent(ctx context.Context) (*Image, error) { - tree, err := i.imageruntime.layerTree() - if err != nil { - return nil, err - } - return tree.parent(ctx, i) -} - -// ParentID returns the image ID of the parent. Return empty string if a parent is not found. -func (i *Image) ParentID(ctx context.Context) (string, error) { - parent, err := i.GetParent(ctx) - if err == nil && parent != nil { - return parent.ID(), nil - } - return "", err -} - -// GetChildren returns a list of the imageIDs that depend on the image -func (i *Image) GetChildren(ctx context.Context) ([]string, error) { - children, err := i.getChildren(ctx, true) - if err != nil { - if errors.Cause(err) == ErrImageIsBareList { - return nil, nil - } - return nil, err - } - return children, nil -} - -// getChildren returns a list of imageIDs that depend on the image. If all is -// false, only the first child image is returned. -func (i *Image) getChildren(ctx context.Context, all bool) ([]string, error) { - tree, err := i.imageruntime.layerTree() - if err != nil { - return nil, err - } - - return tree.children(ctx, i, all) -} - -// InputIsID returns a bool if the user input for an image -// is the image's partial or full id -func (i *Image) InputIsID() bool { - return strings.HasPrefix(i.ID(), i.InputName) -} - -// Containers a list of container IDs associated with the image -func (i *Image) Containers() ([]string, error) { - containers, err := i.imageruntime.store.Containers() - if err != nil { - return nil, err - } - var imageContainers []string - for _, c := range containers { - if c.ImageID == i.ID() { - imageContainers = append(imageContainers, c.ID) - } - } - return imageContainers, err -} - -// Comment returns the Comment for an image depending on its ManifestType -func (i *Image) Comment(ctx context.Context, manifestType string) (string, error) { - if manifestType == manifest.DockerV2Schema2MediaType { - imgRef, err := i.toImageRef(ctx) - if err != nil { - return "", errors.Wrapf(err, "unable to create image reference from image") - } - blob, err := imgRef.ConfigBlob(ctx) - if err != nil { - return "", errors.Wrapf(err, "unable to get config blob from image") - } - b := manifest.Schema2Image{} - if err := json.Unmarshal(blob, &b); err != nil { - return "", err - } - return b.Comment, nil - } - ociv1Img, err := i.ociv1Image(ctx) - if err != nil { - if errors.Cause(err) == ErrImageIsBareList { - return "", nil - } - return "", err - } - if len(ociv1Img.History) > 0 { - return ociv1Img.History[0].Comment, nil - } - return "", nil -} - -// Save writes a container image to the filesystem -func (i *Image) Save(ctx context.Context, source, format, output string, moreTags []string, quiet, compress, removeSignatures bool) error { - var ( - writer io.Writer - destRef types.ImageReference - manifestType string - err error - ) - - if quiet { - writer = os.Stderr - } - switch format { - case "oci-archive": - destImageName := imageNameForSaveDestination(i, source) - destRef, err = ociarchive.NewReference(output, destImageName) // destImageName may be "" - if err != nil { - return errors.Wrapf(err, "error getting OCI archive ImageReference for (%q, %q)", output, destImageName) - } - case "oci-dir": - destImageName := imageNameForSaveDestination(i, source) - destRef, err = layout.NewReference(output, destImageName) // destImageName may be "" - if err != nil { - return errors.Wrapf(err, "error getting the OCI directory ImageReference for (%q, %q)", output, destImageName) - } - manifestType = ociv1.MediaTypeImageManifest - case "docker-dir": - destRef, err = directory.NewReference(output) - if err != nil { - return errors.Wrapf(err, "error getting directory ImageReference for %q", output) - } - manifestType = manifest.DockerV2Schema2MediaType - case "docker-archive", "": - destImageName := imageNameForSaveDestination(i, source) - ref, err := dockerArchiveDstReference(destImageName) - if err != nil { - return err - } - destRef, err = dockerarchive.NewReference(output, ref) - if err != nil { - return errors.Wrapf(err, "error getting Docker archive ImageReference for %s:%v", output, ref) - } - default: - return errors.Errorf("unknown format option %q", format) - } - // supports saving multiple tags to the same tar archive - var additionaltags []reference.NamedTagged - if len(moreTags) > 0 { - additionaltags, err = GetAdditionalTags(moreTags) - if err != nil { - return err - } - } - if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", "", writer, compress, SigningOptions{RemoveSignatures: removeSignatures}, &DockerRegistryOptions{}, additionaltags, nil); err != nil { - return errors.Wrapf(err, "unable to save %q", source) - } - i.newImageEvent(events.Save) - return nil -} - -// dockerArchiveDestReference returns a NamedTagged reference for a tagged image and nil for untagged image. -func dockerArchiveDstReference(normalizedInput string) (reference.NamedTagged, error) { - if normalizedInput == "" { - return nil, nil - } - ref, err := reference.ParseNormalizedNamed(normalizedInput) - if err != nil { - return nil, errors.Wrapf(err, "docker-archive parsing reference %s", normalizedInput) - } - ref = reference.TagNameOnly(ref) - namedTagged, isTagged := ref.(reference.NamedTagged) - if !isTagged { - namedTagged = nil - } - return namedTagged, nil -} - -// GetConfigBlob returns a schema2image. If the image is not a schema2, then -// it will return an error -func (i *Image) GetConfigBlob(ctx context.Context) (*manifest.Schema2Image, error) { - imageRef, err := i.toImageRef(ctx) - if err != nil { - return nil, err - } - b, err := imageRef.ConfigBlob(ctx) - if err != nil { - return nil, errors.Wrapf(err, "unable to get config blob for %s", i.ID()) - } - blob := manifest.Schema2Image{} - if err := json.Unmarshal(b, &blob); err != nil { - return nil, errors.Wrapf(err, "unable to parse image blob for %s", i.ID()) - } - return &blob, nil -} - -// GetHealthCheck returns a HealthConfig for an image. This function only works with -// schema2 images. -func (i *Image) GetHealthCheck(ctx context.Context) (*manifest.Schema2HealthConfig, error) { - configBlob, err := i.GetConfigBlob(ctx) - if err != nil { - return nil, err - } - return configBlob.ContainerConfig.Healthcheck, nil -} - -// newImageEvent creates a new event based on an image -func (ir *Runtime) newImageEvent(status events.Status, name string) { - e := events.NewEvent(status) - e.Type = events.Image - e.Name = name - if err := ir.Eventer.Write(e); err != nil { - logrus.Infof("unable to write event to %s", ir.EventsLogFilePath) - } -} - -// newImageEvent creates a new event based on an image -func (i *Image) newImageEvent(status events.Status) { - e := events.NewEvent(status) - e.ID = i.ID() - e.Type = events.Image - if len(i.Names()) > 0 { - e.Name = i.Names()[0] - } - if err := i.imageruntime.Eventer.Write(e); err != nil { - logrus.Infof("unable to write event to %s", i.imageruntime.EventsLogFilePath) - } -} - -// Mount mounts a image's filesystem on the host -// The path where the image has been mounted is returned -func (i *Image) Mount(options []string, mountLabel string) (string, error) { - defer i.newImageEvent(events.Mount) - return i.mount(options, mountLabel) -} - -// Unmount unmounts a image's filesystem on the host -func (i *Image) Unmount(force bool) error { - defer i.newImageEvent(events.Unmount) - return i.unmount(force) -} - -// Mounted returns whether the image is mounted and the path it is mounted -// at (if it is mounted). -// If the image is not mounted, no error is returned, and the mountpoint -// will be set to "". -func (i *Image) Mounted() (bool, string, error) { - mountedTimes, err := i.imageruntime.store.Mounted(i.TopLayer()) - if err != nil { - return false, "", err - } - - if mountedTimes > 0 { - layer, err := i.imageruntime.store.Layer(i.TopLayer()) - if err != nil { - return false, "", err - } - return true, layer.MountPoint, nil - } - - return false, "", nil -} - -// mount mounts the container's root filesystem -func (i *Image) mount(options []string, mountLabel string) (string, error) { - mountPoint, err := i.imageruntime.store.MountImage(i.ID(), options, mountLabel) - if err != nil { - return "", errors.Wrapf(err, "error mounting storage for image %s", i.ID()) - } - mountPoint, err = filepath.EvalSymlinks(mountPoint) - if err != nil { - return "", errors.Wrapf(err, "error resolving storage path for image %s", i.ID()) - } - return mountPoint, nil -} - -// unmount unmounts the image's root filesystem -func (i *Image) unmount(force bool) error { - // Also unmount storage - if _, err := i.imageruntime.store.UnmountImage(i.ID(), force); err != nil { - return errors.Wrapf(err, "error unmounting image %s root filesystem", i.ID()) - } - - return nil -} - -// LayerInfo keeps information of single layer -type LayerInfo struct { - // Layer ID - ID string - // Parent ID of current layer. - ParentID string - // ChildID of current layer. - // there can be multiple children in case of fork - ChildID []string - // RepoTag will have image repo names, if layer is top layer of image - RepoTags []string - // Size stores Uncompressed size of layer. - Size int64 -} - -// GetLayersMapWithImageInfo returns map of image-layers, with associated information like RepoTags, parent and list of child layers. -func GetLayersMapWithImageInfo(imageruntime *Runtime) (map[string]*LayerInfo, error) { - // TODO: evaluate if we can reuse `layerTree` here. - - // Memory allocated to store map of layers with key LayerID. - // Map will build dependency chain with ParentID and ChildID(s) - layerInfoMap := make(map[string]*LayerInfo) - - // scan all layers & fill size and parent id for each layer in layerInfoMap - layers, err := imageruntime.store.Layers() - if err != nil { - return nil, err - } - for _, layer := range layers { - _, ok := layerInfoMap[layer.ID] - if !ok { - layerInfoMap[layer.ID] = &LayerInfo{ - ID: layer.ID, - Size: layer.UncompressedSize, - ParentID: layer.Parent, - } - } else { - return nil, fmt.Errorf("detected multiple layers with the same ID %q", layer.ID) - } - } - - // scan all layers & add all childid's for each layers to layerInfo - for _, layer := range layers { - _, ok := layerInfoMap[layer.ID] - if ok { - if layer.Parent != "" { - layerInfoMap[layer.Parent].ChildID = append(layerInfoMap[layer.Parent].ChildID, layer.ID) - } - } else { - return nil, fmt.Errorf("lookup error: layer-id %s, not found", layer.ID) - } - } - - // Add the Repo Tags to Top layer of each image. - imgs, err := imageruntime.store.Images() - if err != nil { - return nil, err - } - layerInfoMap[""] = &LayerInfo{} - for _, img := range imgs { - e, ok := layerInfoMap[img.TopLayer] - if !ok { - return nil, fmt.Errorf("top-layer for image %s not found local store", img.ID) - } - e.RepoTags = append(e.RepoTags, img.Names...) - } - return layerInfoMap, nil -} - -// BuildImageHierarchyMap stores hierarchy of images such that all parent layers using which image is built are stored in imageInfo -// Layers are added such that (Start)RootLayer->...intermediate Parent Layer(s)-> TopLayer(End) -func BuildImageHierarchyMap(imageInfo *InfoImage, layerMap map[string]*LayerInfo, layerID string) error { - if layerID == "" { - return nil - } - ll, ok := layerMap[layerID] - if !ok { - return fmt.Errorf("lookup error: layerid %s not found", layerID) - } - if err := BuildImageHierarchyMap(imageInfo, layerMap, ll.ParentID); err != nil { - return err - } - - imageInfo.Layers = append(imageInfo.Layers, *ll) - return nil -} diff --git a/libpod/image/image_test.go b/libpod/image/image_test.go deleted file mode 100644 index 2b42d6394..000000000 --- a/libpod/image/image_test.go +++ /dev/null @@ -1,318 +0,0 @@ -package image - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "testing" - - "github.com/containers/podman/v3/libpod/events" - "github.com/containers/podman/v3/pkg/util" - podmanVersion "github.com/containers/podman/v3/version" - "github.com/containers/storage" - "github.com/containers/storage/pkg/reexec" - "github.com/opencontainers/go-digest" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - bbNames = []string{"docker.io/library/busybox:latest", "docker.io/library/busybox", "docker.io/busybox:latest", "docker.io/busybox", "busybox:latest", "busybox"} - bbGlibcNames = []string{"docker.io/library/busybox:glibc", "docker.io/busybox:glibc", "busybox:glibc"} -) - -type localImageTest struct { - fqname, taggedName string - img *Image - names []string -} - -// make a temporary directory for the runtime -func mkWorkDir() (string, error) { - return ioutil.TempDir("", "podman-test") -} - -// shutdown the runtime and clean behind it -func cleanup(workdir string, ir *Runtime) { - if err := ir.Shutdown(false); err != nil { - fmt.Println(err) - os.Exit(1) - } - err := os.RemoveAll(workdir) - if err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func makeLocalMatrix(b, bg *Image) []localImageTest { - var l []localImageTest - // busybox - busybox := localImageTest{ - fqname: "docker.io/library/busybox:latest", - taggedName: "bb:latest", - } - busybox.img = b - busybox.names = b.Names() - busybox.names = append(busybox.names, []string{"bb:latest", "bb", b.ID(), b.ID()[0:7], fmt.Sprintf("busybox@%s", b.Digest())}...) - - // busybox-glibc - busyboxGlibc := localImageTest{ - fqname: "docker.io/library/busybox:glibc", - taggedName: "bb:glibc", - } - - busyboxGlibc.img = bg - busyboxGlibc.names = bbGlibcNames - - l = append(l, busybox, busyboxGlibc) - return l -} - -func TestMain(m *testing.M) { - if reexec.Init() { - return - } - os.Exit(m.Run()) -} - -// TestImage_NewFromLocal tests finding the image locally by various names, -// tags, and aliases -func TestImage_NewFromLocal(t *testing.T) { - if os.Geteuid() != 0 { // containers/storage requires root access - t.Skipf("Test not running as root") - } - - workdir, err := mkWorkDir() - assert.NoError(t, err) - so := storage.StoreOptions{ - RunRoot: workdir, - GraphRoot: workdir, - } - writer := os.Stdout - - // Need images to be present for this test - ir, err := NewImageRuntimeFromOptions(so) - assert.NoError(t, err) - defer cleanup(workdir, ir) - - ir.Eventer = events.NewNullEventer() - bb, err := ir.New(context.Background(), "docker.io/library/busybox:latest", "", "", writer, nil, SigningOptions{}, nil, util.PullImageMissing, nil) - assert.NoError(t, err) - bbglibc, err := ir.New(context.Background(), "docker.io/library/busybox:glibc", "", "", writer, nil, SigningOptions{}, nil, util.PullImageMissing, nil) - assert.NoError(t, err) - - tm := makeLocalMatrix(bb, bbglibc) - for _, image := range tm { - // tag our images - err = image.img.TagImage(image.taggedName) - assert.NoError(t, err) - for _, name := range image.names { - newImage, err := ir.NewFromLocal(name) - require.NoError(t, err) - assert.Equal(t, newImage.ID(), image.img.ID()) - } - } -} - -// TestImage_New tests pulling the image by various names, tags, and from -// different registries -func TestImage_New(t *testing.T) { - if os.Geteuid() != 0 { // containers/storage requires root access - t.Skipf("Test not running as root") - } - - var names []string - workdir, err := mkWorkDir() - assert.NoError(t, err) - so := storage.StoreOptions{ - RunRoot: workdir, - GraphRoot: workdir, - } - ir, err := NewImageRuntimeFromOptions(so) - assert.NoError(t, err) - defer cleanup(workdir, ir) - - ir.Eventer = events.NewNullEventer() - // Build the list of pull names - names = append(names, bbNames...) - writer := os.Stdout - - opts := DockerRegistryOptions{ - RegistriesConfPath: "testdata/registries.conf", - } - // Iterate over the names and delete the image - // after the pull - for _, img := range names { - newImage, err := ir.New(context.Background(), img, "", "", writer, &opts, SigningOptions{}, nil, util.PullImageMissing, nil) - require.NoError(t, err, img) - assert.NotEqual(t, newImage.ID(), "") - err = newImage.Remove(context.Background(), false) - assert.NoError(t, err) - } -} - -// TestImage_MatchRepoTag tests the various inputs we need to match -// against an image's reponames -func TestImage_MatchRepoTag(t *testing.T) { - if os.Geteuid() != 0 { // containers/storage requires root access - t.Skipf("Test not running as root") - } - - //Set up - workdir, err := mkWorkDir() - assert.NoError(t, err) - so := storage.StoreOptions{ - RunRoot: workdir, - GraphRoot: workdir, - } - ir, err := NewImageRuntimeFromOptions(so) - require.NoError(t, err) - defer cleanup(workdir, ir) - - opts := DockerRegistryOptions{ - RegistriesConfPath: "testdata/registries.conf", - } - ir.Eventer = events.NewNullEventer() - newImage, err := ir.New(context.Background(), "busybox", "", "", os.Stdout, &opts, SigningOptions{}, nil, util.PullImageMissing, nil) - require.NoError(t, err) - err = newImage.TagImage("foo:latest") - require.NoError(t, err) - err = newImage.TagImage("foo:bar") - require.NoError(t, err) - - // Tests start here. - for _, name := range bbNames { - repoTag, err := newImage.MatchRepoTag(name) - assert.NoError(t, err) - assert.Equal(t, "docker.io/library/busybox:latest", repoTag) - } - - // Test against tagged images of busybox - - // foo should resolve to foo:latest - repoTag, err := newImage.MatchRepoTag("foo") - require.NoError(t, err) - assert.Equal(t, "localhost/foo:latest", repoTag) - - // foo:bar should resolve to foo:bar - repoTag, err = newImage.MatchRepoTag("foo:bar") - require.NoError(t, err) - assert.Equal(t, "localhost/foo:bar", repoTag) -} - -// TestImage_RepoDigests tests RepoDigest generation. -func TestImage_RepoDigests(t *testing.T) { - dgst, err := digest.Parse("sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc") - require.NoError(t, err) - - for _, tt := range []struct { - name string - names []string - expected []string - }{ - { - name: "empty", - names: []string{}, - expected: nil, - }, - { - name: "tagged", - names: []string{"docker.io/library/busybox:latest"}, - expected: []string{"docker.io/library/busybox@sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc"}, - }, - { - name: "digest", - names: []string{"docker.io/library/busybox@sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc"}, - expected: []string{"docker.io/library/busybox@sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc"}, - }, - } { - test := tt - t.Run(test.name, func(t *testing.T) { - image := &Image{ - image: &storage.Image{ - Names: test.names, - Digest: dgst, - }, - } - actual, err := image.RepoDigests() - require.NoError(t, err) - assert.Equal(t, test.expected, actual) - - image = &Image{ - image: &storage.Image{ - Names: test.names, - Digests: []digest.Digest{dgst}, - }, - } - actual, err = image.RepoDigests() - require.NoError(t, err) - assert.Equal(t, test.expected, actual) - }) - } -} - -// Test_splitString tests the splitString function in image that -// takes input and splits on / and returns the last array item -func Test_splitString(t *testing.T) { - assert.Equal(t, splitString("foo/bar"), "bar") - assert.Equal(t, splitString("a/foo/bar"), "bar") - assert.Equal(t, splitString("bar"), "bar") -} - -// Test_stripSha256 tests test the stripSha256 function which removes -// the prefix "sha256:" from a string if it is present -func Test_stripSha256(t *testing.T) { - assert.Equal(t, stripSha256(""), "") - assert.Equal(t, stripSha256("test1"), "test1") - assert.Equal(t, stripSha256("sha256:9110ae7f579f35ee0c3938696f23fe0f5fbe641738ea52eb83c2df7e9995fa17"), "9110ae7f579f35ee0c3938696f23fe0f5fbe641738ea52eb83c2df7e9995fa17") - assert.Equal(t, stripSha256("sha256:9110ae7f"), "9110ae7f") - assert.Equal(t, stripSha256("sha256:"), "sha256:") - assert.Equal(t, stripSha256("sha256:a"), "a") -} - -func TestNormalizedTag(t *testing.T) { - const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - - for _, c := range []struct{ input, expected string }{ - {"#", ""}, // Clearly invalid - {"example.com/busybox", "example.com/busybox:latest"}, // Qualified name-only - {"example.com/busybox:notlatest", "example.com/busybox:notlatest"}, // Qualified name:tag - {"example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix}, // Qualified name@digest; FIXME? Should we allow tagging with a digest at all? - {"example.com/busybox:notlatest" + digestSuffix, "example.com/busybox:notlatest" + digestSuffix}, // Qualified name:tag@digest - {"busybox:latest", "localhost/busybox:latest"}, // Unqualified name-only - {"ns/busybox:latest", "localhost/ns/busybox:latest"}, // Unqualified with a dot-less namespace - {"docker.io/busybox:latest", "docker.io/library/busybox:latest"}, // docker.io without /library/ - } { - res, err := NormalizedTag(c.input) - if c.expected == "" { - assert.Error(t, err, c.input) - } else { - assert.NoError(t, err, c.input) - assert.Equal(t, c.expected, res.String()) - } - } -} - -func TestGetSystemContext(t *testing.T) { - sc := GetSystemContext("", "", false) - assert.Equal(t, sc.SignaturePolicyPath, "") - assert.Equal(t, sc.AuthFilePath, "") - assert.Equal(t, sc.DirForceCompress, false) - assert.Equal(t, sc.DockerRegistryUserAgent, fmt.Sprintf("libpod/%s", podmanVersion.Version)) - assert.Equal(t, sc.BigFilesTemporaryDir, "/var/tmp") - - oldtmpdir := os.Getenv("TMPDIR") - os.Setenv("TMPDIR", "/mnt") - sc = GetSystemContext("/tmp/foo", "/tmp/bar", true) - assert.Equal(t, sc.SignaturePolicyPath, "/tmp/foo") - assert.Equal(t, sc.AuthFilePath, "/tmp/bar") - assert.Equal(t, sc.DirForceCompress, true) - assert.Equal(t, sc.BigFilesTemporaryDir, "/mnt") - if oldtmpdir != "" { - os.Setenv("TMPDIR", oldtmpdir) - } else { - os.Unsetenv("TMPDIR") - } -} diff --git a/libpod/image/layer_tree.go b/libpod/image/layer_tree.go deleted file mode 100644 index aa3084449..000000000 --- a/libpod/image/layer_tree.go +++ /dev/null @@ -1,239 +0,0 @@ -package image - -import ( - "context" - - ociv1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/sirupsen/logrus" -) - -// layerTree is an internal representation of local layers. -type layerTree struct { - // nodes is the actual layer tree with layer IDs being keys. - nodes map[string]*layerNode - // ociCache is a cache for Image.ID -> OCI Image. Translations are done - // on-demand. - ociCache map[string]*ociv1.Image -} - -// node returns a layerNode for the specified layerID. -func (t *layerTree) node(layerID string) *layerNode { - node, exists := t.nodes[layerID] - if !exists { - node = &layerNode{} - t.nodes[layerID] = node - } - return node -} - -// toOCI returns an OCI image for the specified image. -func (t *layerTree) toOCI(ctx context.Context, i *Image) (*ociv1.Image, error) { - var err error - oci, exists := t.ociCache[i.ID()] - if !exists { - oci, err = i.ociv1Image(ctx) - if err == nil { - t.ociCache[i.ID()] = oci - } - } - return oci, err -} - -// layerNode is a node in a layerTree. It's ID is the key in a layerTree. -type layerNode struct { - children []*layerNode - images []*Image - parent *layerNode -} - -// layerTree extracts a layerTree from the layers in the local storage and -// relates them to the specified images. -func (ir *Runtime) layerTree() (*layerTree, error) { - layers, err := ir.store.Layers() - if err != nil { - return nil, err - } - - images, err := ir.GetImages() - if err != nil { - return nil, err - } - - tree := layerTree{ - nodes: make(map[string]*layerNode), - ociCache: make(map[string]*ociv1.Image), - } - - // First build a tree purely based on layer information. - for _, layer := range layers { - node := tree.node(layer.ID) - if layer.Parent == "" { - continue - } - parent := tree.node(layer.Parent) - node.parent = parent - parent.children = append(parent.children, node) - } - - // Now assign the images to each (top) layer. - for i := range images { - img := images[i] // do not leak loop variable outside the scope - topLayer := img.TopLayer() - if topLayer == "" { - continue - } - node, exists := tree.nodes[topLayer] - if !exists { - // Note: erroring out in this case has turned out having been a - // mistake. Users may not be able to recover, so we're now - // throwing a warning to guide them to resolve the issue and - // turn the errors non-fatal. - logrus.Warnf("Top layer %s of image %s not found in layer tree. The storage may be corrupted, consider running `podman system reset`.", topLayer, img.ID()) - continue - } - node.images = append(node.images, img) - } - - return &tree, nil -} - -// children returns the image IDs of children . Child images are images -// with either the same top layer as parent or parent being the true parent -// layer. Furthermore, the history of the parent and child images must match -// with the parent having one history item less. -// If all is true, all images are returned. Otherwise, the first image is -// returned. -func (t *layerTree) children(ctx context.Context, parent *Image, all bool) ([]string, error) { - if parent.TopLayer() == "" { - return nil, nil - } - - var children []string - - parentNode, exists := t.nodes[parent.TopLayer()] - if !exists { - // Note: erroring out in this case has turned out having been a - // mistake. Users may not be able to recover, so we're now - // throwing a warning to guide them to resolve the issue and - // turn the errors non-fatal. - logrus.Warnf("Layer %s not found in layer. The storage may be corrupted, consider running `podman system reset`.", parent.TopLayer()) - return children, nil - } - - parentID := parent.ID() - parentOCI, err := t.toOCI(ctx, parent) - if err != nil { - return nil, err - } - - // checkParent returns true if child and parent are in such a relation. - checkParent := func(child *Image) (bool, error) { - if parentID == child.ID() { - return false, nil - } - childOCI, err := t.toOCI(ctx, child) - if err != nil { - return false, err - } - // History check. - return areParentAndChild(parentOCI, childOCI), nil - } - - // addChildrenFrom adds child images of parent to children. Returns - // true if any image is a child of parent. - addChildrenFromNode := func(node *layerNode) (bool, error) { - foundChildren := false - for _, childImage := range node.images { - isChild, err := checkParent(childImage) - if err != nil { - return foundChildren, err - } - if isChild { - foundChildren = true - children = append(children, childImage.ID()) - if all { - return foundChildren, nil - } - } - } - return foundChildren, nil - } - - // First check images where parent's top layer is also the parent - // layer. - for _, childNode := range parentNode.children { - found, err := addChildrenFromNode(childNode) - if err != nil { - return nil, err - } - if found && all { - return children, nil - } - } - - // Now check images with the same top layer. - if _, err := addChildrenFromNode(parentNode); err != nil { - return nil, err - } - - return children, nil -} - -// parent returns the parent image or nil if no parent image could be found. -func (t *layerTree) parent(ctx context.Context, child *Image) (*Image, error) { - if child.TopLayer() == "" { - return nil, nil - } - - node, exists := t.nodes[child.TopLayer()] - if !exists { - // Note: erroring out in this case has turned out having been a - // mistake. Users may not be able to recover, so we're now - // throwing a warning to guide them to resolve the issue and - // turn the errors non-fatal. - logrus.Warnf("Layer %s not found in layer. The storage may be corrupted, consider running `podman system reset`.", child.TopLayer()) - return nil, nil - } - - childOCI, err := t.toOCI(ctx, child) - if err != nil { - return nil, err - } - - // Check images from the parent node (i.e., parent layer) and images - // with the same layer (i.e., same top layer). - childID := child.ID() - images := node.images - if node.parent != nil { - images = append(images, node.parent.images...) - } - for _, parent := range images { - if parent.ID() == childID { - continue - } - parentOCI, err := t.toOCI(ctx, parent) - if err != nil { - return nil, err - } - // History check. - if areParentAndChild(parentOCI, childOCI) { - return parent, nil - } - } - - return nil, nil -} - -// hasChildrenAndParent returns true if the specified image has children and a -// parent. -func (t *layerTree) hasChildrenAndParent(ctx context.Context, i *Image) (bool, error) { - children, err := t.children(ctx, i, false) - if err != nil { - return false, err - } - if len(children) == 0 { - return false, nil - } - parent, err := t.parent(ctx, i) - return parent != nil, err -} diff --git a/libpod/image/manifests.go b/libpod/image/manifests.go deleted file mode 100644 index 1ae3693c9..000000000 --- a/libpod/image/manifests.go +++ /dev/null @@ -1,209 +0,0 @@ -package image - -import ( - "context" - "fmt" - - "github.com/containers/buildah/manifests" - "github.com/containers/image/v5/docker" - "github.com/containers/image/v5/manifest" - "github.com/containers/image/v5/transports/alltransports" - "github.com/containers/image/v5/types" - "github.com/opencontainers/go-digest" -) - -// Options for adding a manifest -// swagger:model ManifestAddOpts -type ManifestAddOpts struct { - All bool `json:"all"` - Annotation map[string]string `json:"annotation"` - Arch string `json:"arch"` - Features []string `json:"features"` - Images []string `json:"images"` - OS string `json:"os"` - OSVersion string `json:"os_version"` - Variant string `json:"variant"` -} - -// ManifestAnnotateOptions defines the options for -// manifest annotate -type ManifestAnnotateOpts struct { - Annotation map[string]string `json:"annotation"` - Arch string `json:"arch"` - Features []string `json:"features"` - OS string `json:"os"` - OSFeatures []string `json:"os_feature"` - OSVersion string `json:"os_version"` - Variant string `json:"variant"` -} - -// InspectManifest returns a dockerized version of the manifest list -func (i *Image) InspectManifest() (*manifest.Schema2List, error) { - list, err := i.getManifestList() - if err != nil { - return nil, err - } - return list.Docker(), nil -} - -// ExistsManifest checks if a manifest list exists -func (i *Image) ExistsManifest() (bool, error) { - _, err := i.getManifestList() - if err != nil { - return false, err - } - return true, nil -} - -// RemoveManifest removes the given digest from the manifest list. -func (i *Image) RemoveManifest(d digest.Digest) (string, error) { - list, err := i.getManifestList() - if err != nil { - return "", err - } - if err := list.Remove(d); err != nil { - return "", err - } - return list.SaveToImage(i.imageruntime.store, i.ID(), nil, "") -} - -// getManifestList is a helper to obtain a manifest list -func (i *Image) getManifestList() (manifests.List, error) { - _, list, err := manifests.LoadFromImage(i.imageruntime.store, i.ID()) - return list, err -} - -// CreateManifestList creates a new manifest list and can optionally add given images -// to the list -func CreateManifestList(rt *Runtime, systemContext types.SystemContext, names []string, imgs []string, all bool) (string, error) { - list := manifests.Create() - opts := ManifestAddOpts{Images: names, All: all} - for _, img := range imgs { - ref, err := alltransports.ParseImageName(img) - if err != nil { - dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) - ref, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, img)) - if err != nil { - return "", err - } - } - list, err = addManifestToList(ref, list, systemContext, opts) - if err != nil { - return "", err - } - } - return list.SaveToImage(rt.store, "", names, manifest.DockerV2ListMediaType) -} - -func addManifestToList(ref types.ImageReference, list manifests.List, systemContext types.SystemContext, opts ManifestAddOpts) (manifests.List, error) { - d, err := list.Add(context.Background(), &systemContext, ref, opts.All) - if err != nil { - return nil, err - } - if opts.OS != "" { - if err := list.SetOS(d, opts.OS); err != nil { - return nil, err - } - } - if len(opts.OSVersion) > 0 { - if err := list.SetOSVersion(d, opts.OSVersion); err != nil { - return nil, err - } - } - if len(opts.Features) > 0 { - if err := list.SetFeatures(d, opts.Features); err != nil { - return nil, err - } - } - if len(opts.Arch) > 0 { - if err := list.SetArchitecture(d, opts.Arch); err != nil { - return nil, err - } - } - if len(opts.Variant) > 0 { - if err := list.SetVariant(d, opts.Variant); err != nil { - return nil, err - } - } - if len(opts.Annotation) > 0 { - if err := list.SetAnnotations(&d, opts.Annotation); err != nil { - return nil, err - } - } - return list, err -} - -// AddManifest adds a manifest to a given manifest list. -func (i *Image) AddManifest(systemContext types.SystemContext, opts ManifestAddOpts) (string, error) { - ref, err := alltransports.ParseImageName(opts.Images[0]) - if err != nil { - dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) - ref, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, opts.Images[0])) - if err != nil { - return "", err - } - } - list, err := i.getManifestList() - if err != nil { - return "", err - } - list, err = addManifestToList(ref, list, systemContext, opts) - if err != nil { - return "", err - } - return list.SaveToImage(i.imageruntime.store, i.ID(), nil, "") -} - -// PushManifest pushes a manifest to a destination -func (i *Image) PushManifest(dest types.ImageReference, opts manifests.PushOptions) (digest.Digest, error) { - list, err := i.getManifestList() - if err != nil { - return "", err - } - _, d, err := list.Push(context.Background(), dest, opts) - return d, err -} - -// AnnotateManifest updates an image configuration of a manifest list. -func (i *Image) AnnotateManifest(systemContext types.SystemContext, d digest.Digest, opts ManifestAnnotateOpts) (string, error) { - list, err := i.getManifestList() - if err != nil { - return "", err - } - if len(opts.OS) > 0 { - if err := list.SetOS(d, opts.OS); err != nil { - return "", err - } - } - if len(opts.OSVersion) > 0 { - if err := list.SetOSVersion(d, opts.OSVersion); err != nil { - return "", err - } - } - if len(opts.Features) > 0 { - if err := list.SetFeatures(d, opts.Features); err != nil { - return "", err - } - } - if len(opts.OSFeatures) > 0 { - if err := list.SetOSFeatures(d, opts.OSFeatures); err != nil { - return "", err - } - } - if len(opts.Arch) > 0 { - if err := list.SetArchitecture(d, opts.Arch); err != nil { - return "", err - } - } - if len(opts.Variant) > 0 { - if err := list.SetVariant(d, opts.Variant); err != nil { - return "", err - } - } - if len(opts.Annotation) > 0 { - if err := list.SetAnnotations(&d, opts.Annotation); err != nil { - return "", err - } - } - return list.SaveToImage(i.imageruntime.store, i.ID(), nil, "") -} diff --git a/libpod/image/parts.go b/libpod/image/parts.go deleted file mode 100644 index 08421320c..000000000 --- a/libpod/image/parts.go +++ /dev/null @@ -1,104 +0,0 @@ -package image - -import ( - "strings" - - "github.com/containers/image/v5/docker/reference" - "github.com/pkg/errors" -) - -// imageParts describes the parts of an image's name -type imageParts struct { - unnormalizedRef reference.Named // WARNING: Did not go through docker.io[/library] normalization - hasRegistry bool -} - -// Registries must contain a ":" or a "." or be localhost; this helper exists for users of reference.Parse. -// For inputs that should use the docker.io[/library] normalization, use reference.ParseNormalizedNamed instead. -func isRegistry(name string) bool { - return strings.ContainsAny(name, ".:") || name == "localhost" -} - -// GetImageBaseName uses decompose and string splits to obtain the base -// name of an image. Doing this here because it beats changing the -// imageParts struct names to be exported as well. -func GetImageBaseName(input string) (string, error) { - decomposedImage, err := decompose(input) - if err != nil { - return "", err - } - splitImageName := strings.Split(decomposedImage.unnormalizedRef.Name(), "/") - return splitImageName[len(splitImageName)-1], nil -} - -// decompose breaks an input name into an imageParts description -func decompose(input string) (imageParts, error) { - imgRef, err := reference.Parse(input) - if err != nil { - return imageParts{}, err - } - unnormalizedNamed := imgRef.(reference.Named) - // ip.unnormalizedRef, because it uses reference.Parse and not reference.ParseNormalizedNamed, - // does not use the standard heuristics for domains vs. namespaces/repos, so we need to check - // explicitly. - hasRegistry := isRegistry(reference.Domain(unnormalizedNamed)) - return imageParts{ - unnormalizedRef: unnormalizedNamed, - hasRegistry: hasRegistry, - }, nil -} - -// suspiciousRefNameTagValuesForSearch returns a "tag" value used in a previous implementation. -// This exists only to preserve existing behavior in heuristic code; it’s dubious that that behavior is correct, -// especially for the tag value. -func (ip *imageParts) suspiciousRefNameTagValuesForSearch() (string, string, string) { - registry := reference.Domain(ip.unnormalizedRef) - imageName := reference.Path(ip.unnormalizedRef) - // ip.unnormalizedRef, because it uses reference.Parse and not reference.ParseNormalizedNamed, - // does not use the standard heuristics for domains vs. namespaces/repos. - if registry != "" && !isRegistry(registry) { - imageName = registry + "/" + imageName - registry = "" - } - - var tag string - if tagged, isTagged := ip.unnormalizedRef.(reference.NamedTagged); isTagged { - tag = tagged.Tag() - } else if _, hasDigest := ip.unnormalizedRef.(reference.Digested); hasDigest { - tag = "none" - } else { - tag = LatestTag - } - return registry, imageName, tag -} - -// referenceWithRegistry returns a (normalized) reference.Named composed of ip (with !ip.hasRegistry) -// qualified with registry. -func (ip *imageParts) referenceWithRegistry(registry string) (reference.Named, error) { - if ip.hasRegistry { - return nil, errors.Errorf("internal error: referenceWithRegistry called on imageParts with a registry (%#v)", *ip) - } - // We could build a reference.WithName+WithTag/WithDigest here, but we need to round-trip via a string - // and a ParseNormalizedNamed anyway to get the right normalization of docker.io/library, so - // just use a string directly. - qualified := registry + "/" + ip.unnormalizedRef.String() - ref, err := reference.ParseNormalizedNamed(qualified) - if err != nil { - return nil, errors.Wrapf(err, "error normalizing registry+unqualified reference %#v", qualified) - } - return ref, nil -} - -// normalizedReference returns a (normalized) reference for ip (with ip.hasRegistry) -func (ip *imageParts) normalizedReference() (reference.Named, error) { - if !ip.hasRegistry { - return nil, errors.Errorf("internal error: normalizedReference called on imageParts without a registry (%#v)", *ip) - } - // We need to round-trip via a string to get the right normalization of docker.io/library - s := ip.unnormalizedRef.String() - ref, err := reference.ParseNormalizedNamed(s) - if err != nil { // Should never happen - return nil, errors.Wrapf(err, "error normalizing qualified reference %#v", s) - } - return ref, nil -} diff --git a/libpod/image/parts_test.go b/libpod/image/parts_test.go deleted file mode 100644 index 726e55e86..000000000 --- a/libpod/image/parts_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package image - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDecompose(t *testing.T) { - const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - - for _, c := range []struct { - input string - registry, name, suspiciousTagValueForSearch string - hasRegistry bool - }{ - {"#", "", "", "", false}, // Entirely invalid input - { // Fully qualified docker.io, name-only input - "docker.io/library/busybox", "docker.io", "library/busybox", "latest", true, - }, - { // Fully qualified example.com, name-only input - "example.com/ns/busybox", "example.com", "ns/busybox", "latest", true, - }, - { // Unqualified single-name input - "busybox", "", "busybox", "latest", false, - }, - { // Unqualified namespaced input - "ns/busybox", "", "ns/busybox", "latest", false, - }, - { // name:tag - "example.com/ns/busybox:notlatest", "example.com", "ns/busybox", "notlatest", true, - }, - { // name@digest - // FIXME? .suspiciousTagValueForSearch == "none" - "example.com/ns/busybox" + digestSuffix, "example.com", "ns/busybox", "none", true, - }, - { // name:tag@digest - "example.com/ns/busybox:notlatest" + digestSuffix, "example.com", "ns/busybox", "notlatest", true, - }, - } { - parts, err := decompose(c.input) - if c.name == "" { - assert.Error(t, err, c.input) - } else { - assert.NoError(t, err, c.input) - registry, name, suspiciousTagValueForSearch := parts.suspiciousRefNameTagValuesForSearch() - assert.Equal(t, c.registry, registry, c.input) - assert.Equal(t, c.name, name, c.input) - assert.Equal(t, c.suspiciousTagValueForSearch, suspiciousTagValueForSearch, c.input) - assert.Equal(t, c.hasRegistry, parts.hasRegistry, c.input) - } - } -} - -func TestImagePartsReferenceWithRegistry(t *testing.T) { - const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - - for _, c := range []struct { - input string - withDocker, withNonDocker string - }{ - {"example.com/ns/busybox", "", ""}, // Fully-qualified input is invalid. - {"busybox", "docker.io/library/busybox", "example.com/busybox"}, // Single-name input - {"ns/busybox", "docker.io/ns/busybox", "example.com/ns/busybox"}, // Namespaced input - {"ns/busybox:notlatest", "docker.io/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}, // name:tag - {"ns/busybox" + digestSuffix, "docker.io/ns/busybox" + digestSuffix, "example.com/ns/busybox" + digestSuffix}, // name@digest - { // name:tag@digest - "ns/busybox:notlatest" + digestSuffix, - "docker.io/ns/busybox:notlatest" + digestSuffix, "example.com/ns/busybox:notlatest" + digestSuffix, - }, - } { - parts, err := decompose(c.input) - require.NoError(t, err) - if c.withDocker == "" { - _, err := parts.referenceWithRegistry("docker.io") - assert.Error(t, err, c.input) - _, err = parts.referenceWithRegistry("example.com") - assert.Error(t, err, c.input) - } else { - ref, err := parts.referenceWithRegistry("docker.io") - require.NoError(t, err, c.input) - assert.Equal(t, c.withDocker, ref.String()) - ref, err = parts.referenceWithRegistry("example.com") - require.NoError(t, err, c.input) - assert.Equal(t, c.withNonDocker, ref.String()) - } - } - - // Invalid registry value - parts, err := decompose("busybox") - require.NoError(t, err) - _, err = parts.referenceWithRegistry("invalid@domain") - assert.Error(t, err) -} - -func TestImagePartsNormalizedReference(t *testing.T) { - const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - - for _, c := range []struct{ input, expected string }{ - {"busybox", ""}, // Unqualified input is invalid - {"docker.io/busybox", "docker.io/library/busybox"}, // docker.io single-name - {"example.com/busybox", "example.com/busybox"}, // example.com single-name - {"docker.io/ns/busybox", "docker.io/ns/busybox"}, // docker.io namespaced - {"example.com/ns/busybox", "example.com/ns/busybox"}, // example.com namespaced - {"example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}, // name:tag - {"example.com/ns/busybox" + digestSuffix, "example.com/ns/busybox" + digestSuffix}, // name@digest - { // name:tag@digest - "example.com/ns/busybox:notlatest" + digestSuffix, "example.com/ns/busybox:notlatest" + digestSuffix, - }, - } { - parts, err := decompose(c.input) - require.NoError(t, err) - if c.expected == "" { - _, err := parts.normalizedReference() - assert.Error(t, err, c.input) - } else { - ref, err := parts.normalizedReference() - require.NoError(t, err, c.input) - assert.Equal(t, c.expected, ref.String()) - } - } -} diff --git a/libpod/image/prune.go b/libpod/image/prune.go deleted file mode 100644 index e0480d3d1..000000000 --- a/libpod/image/prune.go +++ /dev/null @@ -1,164 +0,0 @@ -package image - -import ( - "context" - "strconv" - "strings" - - "github.com/containers/podman/v3/libpod/events" - "github.com/containers/podman/v3/pkg/domain/entities/reports" - "github.com/containers/podman/v3/pkg/util" - "github.com/containers/storage" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -func generatePruneFilterFuncs(filter, filterValue string) (ImageFilter, error) { - switch filter { - case "label": - return func(i *Image) bool { - labels, err := i.Labels(context.Background()) - if err != nil { - return false - } - return util.MatchLabelFilters([]string{filterValue}, labels) - }, nil - - case "until": - until, err := util.ComputeUntilTimestamp([]string{filterValue}) - if err != nil { - return nil, err - } - return func(i *Image) bool { - if !until.IsZero() && i.Created().Before(until) { - return true - } - return false - }, nil - case "dangling": - danglingImages, err := strconv.ParseBool(filterValue) - if err != nil { - return nil, errors.Wrapf(err, "invalid filter dangling=%s", filterValue) - } - return ImageFilter(DanglingFilter(danglingImages)), nil - } - return nil, nil -} - -// GetPruneImages returns a slice of images that have no names/unused -func (ir *Runtime) GetPruneImages(ctx context.Context, all bool, filterFuncs []ImageFilter) ([]*Image, error) { - var ( - pruneImages []*Image - ) - - allImages, err := ir.GetRWImages() - if err != nil { - return nil, err - } - - tree, err := ir.layerTree() - if err != nil { - return nil, err - } - - for _, i := range allImages { - // filter the images based on this. - for _, filterFunc := range filterFuncs { - if !filterFunc(i) { - continue - } - } - - if all { - containers, err := i.Containers() - if err != nil { - return nil, err - } - if len(containers) < 1 { - pruneImages = append(pruneImages, i) - continue - } - } - - // skip the cache (i.e., with parent) and intermediate (i.e., - // with children) images - intermediate, err := tree.hasChildrenAndParent(ctx, i) - if err != nil { - return nil, err - } - if intermediate { - continue - } - - if i.Dangling() { - pruneImages = append(pruneImages, i) - } - } - return pruneImages, nil -} - -// PruneImages prunes dangling and optionally all unused images from the local -// image store -func (ir *Runtime) PruneImages(ctx context.Context, all bool, filter []string) ([]*reports.PruneReport, error) { - preports := make([]*reports.PruneReport, 0) - filterFuncs := make([]ImageFilter, 0, len(filter)) - for _, f := range filter { - filterSplit := strings.SplitN(f, "=", 2) - if len(filterSplit) < 2 { - return nil, errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) - } - - generatedFunc, err := generatePruneFilterFuncs(filterSplit[0], filterSplit[1]) - if err != nil { - return nil, errors.Wrapf(err, "invalid filter") - } - filterFuncs = append(filterFuncs, generatedFunc) - } - - prev := 0 - for { - toPrune, err := ir.GetPruneImages(ctx, all, filterFuncs) - if err != nil { - return nil, errors.Wrap(err, "unable to get images to prune") - } - numImages := len(toPrune) - if numImages == 0 || numImages == prev { - // If there's nothing left to do, return. - break - } - prev = numImages - for _, img := range toPrune { - repotags, err := img.RepoTags() - if err != nil { - return nil, err - } - nameOrID := img.ID() - s, err := img.Size(ctx) - imgSize := uint64(0) - if err != nil { - logrus.Warnf("Failed to collect image size for: %s, %s", nameOrID, err) - } else { - imgSize = *s - } - if err := img.Remove(ctx, false); err != nil { - if errors.Cause(err) == storage.ErrImageUsedByContainer { - logrus.Warnf("Failed to prune image %s as it is in use: %v.\nA container associated with containers/storage (e.g., Buildah, CRI-O, etc.) maybe associated with this image.\nUsing the rmi command with the --force option will remove the container and image, but may cause failures for other dependent systems.", img.ID(), err) - continue - } - return nil, errors.Wrap(err, "failed to prune image") - } - defer img.newImageEvent(events.Prune) - - if len(repotags) > 0 { - nameOrID = repotags[0] - } - - preports = append(preports, &reports.PruneReport{ - Id: nameOrID, - Err: nil, - Size: uint64(imgSize), - }) - } - } - return preports, nil -} diff --git a/libpod/image/pull.go b/libpod/image/pull.go deleted file mode 100644 index 6517fbd07..000000000 --- a/libpod/image/pull.go +++ /dev/null @@ -1,437 +0,0 @@ -package image - -import ( - "context" - "fmt" - "io" - "path/filepath" - "strings" - "time" - - "github.com/containers/common/pkg/retry" - cp "github.com/containers/image/v5/copy" - "github.com/containers/image/v5/directory" - "github.com/containers/image/v5/docker" - dockerarchive "github.com/containers/image/v5/docker/archive" - ociarchive "github.com/containers/image/v5/oci/archive" - oci "github.com/containers/image/v5/oci/layout" - "github.com/containers/image/v5/pkg/shortnames" - 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/podman/v3/libpod/events" - "github.com/containers/podman/v3/pkg/errorhandling" - "github.com/containers/podman/v3/pkg/registries" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -var ( - // DockerArchive is the transport we prepend to an image name - // when saving to docker-archive - DockerArchive = dockerarchive.Transport.Name() - // OCIArchive is the transport we prepend to an image name - // when saving to oci-archive - OCIArchive = ociarchive.Transport.Name() - // DirTransport is the transport for pushing and pulling - // images to and from a directory - DirTransport = directory.Transport.Name() - // DockerTransport is the transport for docker registries - DockerTransport = docker.Transport.Name() - // OCIDirTransport is the transport for pushing and pulling - // images to and from a directory containing an OCI image - OCIDirTransport = oci.Transport.Name() - // AtomicTransport is the transport for atomic registries - AtomicTransport = "atomic" - // DefaultTransport is a prefix that we apply to an image name - // NOTE: This is a string prefix, not actually a transport name usable for transports.Get(); - // and because syntaxes of image names are transport-dependent, the prefix is not really interchangeable; - // each user implicitly assumes the appended string is a Docker-like reference. - DefaultTransport = DockerTransport + "://" - // DefaultLocalRegistry is the default local registry for local image operations - // Remote pulls will still use defined registries - DefaultLocalRegistry = "localhost" -) - -// pullRefPair records a pair of prepared image references to pull. -type pullRefPair struct { - image string - srcRef types.ImageReference - dstRef types.ImageReference - resolvedShortname *shortnames.PullCandidate // if set, must be recorded after successful pull -} - -// cleanUpFunc is a function prototype for clean-up functions. -type cleanUpFunc func() error - -// pullGoal represents the prepared image references and decided behavior to be executed by imagePull -type pullGoal struct { - refPairs []pullRefPair - pullAllPairs bool // Pull all refPairs instead of stopping on first success. - cleanUpFuncs []cleanUpFunc // Mainly used to close long-lived objects (e.g., an archive.Reader) - shortName string // Set when pulling a short name - resolved *shortnames.Resolved // Set when pulling a short name -} - -// cleanUp invokes all cleanUpFuncs. Certain resources may not be available -// anymore. Errors are logged. -func (p *pullGoal) cleanUp() { - for _, f := range p.cleanUpFuncs { - if err := f(); err != nil { - logrus.Error(err.Error()) - } - } -} - -// singlePullRefPairGoal returns a no-frills pull goal for the specified reference pair. -func singlePullRefPairGoal(rp pullRefPair) *pullGoal { - return &pullGoal{ - refPairs: []pullRefPair{rp}, - pullAllPairs: false, // Does not really make a difference. - } -} - -func (ir *Runtime) getPullRefPair(srcRef types.ImageReference, destName string) (pullRefPair, error) { - decomposedDest, err := decompose(destName) - if err == nil && !decomposedDest.hasRegistry { - // If the image doesn't have a registry, set it as the default repo - ref, err := decomposedDest.referenceWithRegistry(DefaultLocalRegistry) - if err != nil { - return pullRefPair{}, err - } - destName = ref.String() - } - - reference := destName - if srcRef.DockerReference() != nil { - reference = srcRef.DockerReference().String() - } - destRef, err := is.Transport.ParseStoreReference(ir.store, reference) - if err != nil { - return pullRefPair{}, errors.Wrapf(err, "error parsing dest reference name %#v", destName) - } - return pullRefPair{ - image: destName, - srcRef: srcRef, - dstRef: destRef, - }, nil -} - -// getSinglePullRefPairGoal calls getPullRefPair with the specified parameters, and returns a single-pair goal for the return value. -func (ir *Runtime) getSinglePullRefPairGoal(srcRef types.ImageReference, destName string) (*pullGoal, error) { - rp, err := ir.getPullRefPair(srcRef, destName) - if err != nil { - return nil, err - } - return singlePullRefPairGoal(rp), nil -} - -// getPullRefPairsFromDockerArchiveReference returns a slice of pullRefPairs -// for the specified docker reference and the corresponding archive.Reader. -func (ir *Runtime) getPullRefPairsFromDockerArchiveReference(ctx context.Context, reader *dockerarchive.Reader, ref types.ImageReference, sc *types.SystemContext) ([]pullRefPair, error) { - destNames, err := reader.ManifestTagsForReference(ref) - if err != nil { - return nil, err - } - - if len(destNames) == 0 { - destName, err := getImageDigest(ctx, ref, sc) - if err != nil { - return nil, err - } - destNames = append(destNames, destName) - } else { - for i := range destNames { - ref, err := NormalizedTag(destNames[i]) - if err != nil { - return nil, err - } - destNames[i] = ref.String() - } - } - - refPairs := []pullRefPair{} - for _, destName := range destNames { - destRef, err := is.Transport.ParseStoreReference(ir.store, destName) - if err != nil { - return nil, errors.Wrapf(err, "error parsing dest reference name %#v", destName) - } - pair := pullRefPair{ - image: destName, - srcRef: ref, - dstRef: destRef, - } - refPairs = append(refPairs, pair) - } - - return refPairs, nil -} - -// pullGoalFromImageReference returns a pull goal for a single ImageReference, depending on the used transport. -// Note that callers are responsible for invoking (*pullGoal).cleanUp() to clean up possibly open resources. -func (ir *Runtime) pullGoalFromImageReference(ctx context.Context, srcRef types.ImageReference, imgName string, sc *types.SystemContext) (*pullGoal, error) { - // supports pulling from docker-archive, oci, and registries - switch srcRef.Transport().Name() { - case DockerArchive: - reader, readerRef, err := dockerarchive.NewReaderForReference(sc, srcRef) - if err != nil { - return nil, err - } - - pairs, err := ir.getPullRefPairsFromDockerArchiveReference(ctx, reader, readerRef, sc) - if err != nil { - // No need to defer for a single error path. - if err := reader.Close(); err != nil { - logrus.Error(err.Error()) - } - return nil, err - } - - return &pullGoal{ - pullAllPairs: true, - refPairs: pairs, - cleanUpFuncs: []cleanUpFunc{reader.Close}, - }, nil - - case OCIArchive: - // retrieve the manifest from index.json to access the image name - manifest, err := ociarchive.LoadManifestDescriptor(srcRef) - if err != nil { - return nil, errors.Wrapf(err, "error loading manifest for %q", srcRef) - } - var dest string - if manifest.Annotations == nil || manifest.Annotations["org.opencontainers.image.ref.name"] == "" { - // If the input image has no image.ref.name, we need to feed it a dest anyways - // use the hex of the digest - dest, err = getImageDigest(ctx, srcRef, sc) - if err != nil { - return nil, errors.Wrapf(err, "error getting image digest; image reference not found") - } - } else { - dest = manifest.Annotations["org.opencontainers.image.ref.name"] - } - return ir.getSinglePullRefPairGoal(srcRef, dest) - - case DirTransport: - image := toLocalImageName(srcRef.StringWithinTransport()) - return ir.getSinglePullRefPairGoal(srcRef, image) - - case OCIDirTransport: - split := strings.SplitN(srcRef.StringWithinTransport(), ":", 2) - image := toLocalImageName(split[0]) - return ir.getSinglePullRefPairGoal(srcRef, image) - - default: - return ir.getSinglePullRefPairGoal(srcRef, imgName) - } -} - -// toLocalImageName converts an image name into a 'localhost/' prefixed one -func toLocalImageName(imageName string) string { - return fmt.Sprintf( - "%s/%s", - DefaultLocalRegistry, - strings.TrimLeft(imageName, "/"), - ) -} - -// pullImageFromHeuristicSource pulls an image based on inputName, which is heuristically parsed and may involve configured registries. -// Use pullImageFromReference if the source is known precisely. -func (ir *Runtime) pullImageFromHeuristicSource(ctx context.Context, inputName string, writer io.Writer, authfile, signaturePolicyPath string, signingOptions SigningOptions, dockerOptions *DockerRegistryOptions, retryOptions *retry.RetryOptions, label *string, progress chan types.ProgressProperties) ([]string, error) { - var goal *pullGoal - sc := GetSystemContext(signaturePolicyPath, authfile, false) - if dockerOptions != nil { - sc.OSChoice = dockerOptions.OSChoice - sc.ArchitectureChoice = dockerOptions.ArchitectureChoice - sc.VariantChoice = dockerOptions.VariantChoice - sc.SystemRegistriesConfPath = dockerOptions.RegistriesConfPath - } - if signaturePolicyPath == "" { - sc.SignaturePolicyPath = ir.SignaturePolicyPath - } - sc.BlobInfoCacheDir = filepath.Join(ir.store.GraphRoot(), "cache") - srcRef, err := alltransports.ParseImageName(inputName) - if err != nil { - // We might be pulling with an unqualified image reference in which case - // we need to make sure that we're not using any other transport. - srcTransport := alltransports.TransportFromImageName(inputName) - if srcTransport != nil && srcTransport.Name() != DockerTransport { - return nil, err - } - goal, err = ir.pullGoalFromPossiblyUnqualifiedName(sc, writer, inputName) - if err != nil { - return nil, errors.Wrap(err, "error getting default registries to try") - } - } else { - goal, err = ir.pullGoalFromImageReference(ctx, srcRef, inputName, sc) - if err != nil { - return nil, errors.Wrapf(err, "error determining pull goal for image %q", inputName) - } - } - defer goal.cleanUp() - return ir.doPullImage(ctx, sc, *goal, writer, signingOptions, dockerOptions, retryOptions, label, progress) -} - -// pullImageFromReference pulls an image from a types.imageReference. -func (ir *Runtime) pullImageFromReference(ctx context.Context, srcRef types.ImageReference, writer io.Writer, authfile, signaturePolicyPath string, signingOptions SigningOptions, dockerOptions *DockerRegistryOptions, retryOptions *retry.RetryOptions) ([]string, error) { - sc := GetSystemContext(signaturePolicyPath, authfile, false) - if dockerOptions != nil { - sc.OSChoice = dockerOptions.OSChoice - sc.ArchitectureChoice = dockerOptions.ArchitectureChoice - sc.VariantChoice = dockerOptions.VariantChoice - } - goal, err := ir.pullGoalFromImageReference(ctx, srcRef, transports.ImageName(srcRef), sc) - if err != nil { - return nil, errors.Wrapf(err, "error determining pull goal for image %q", transports.ImageName(srcRef)) - } - defer goal.cleanUp() - return ir.doPullImage(ctx, sc, *goal, writer, signingOptions, dockerOptions, retryOptions, nil, nil) -} - -func cleanErrorMessage(err error) string { - errMessage := strings.TrimPrefix(errors.Cause(err).Error(), "errors:\n") - errMessage = strings.Split(errMessage, "\n")[0] - return fmt.Sprintf(" %s\n", errMessage) -} - -// doPullImage is an internal helper interpreting pullGoal. Almost everyone should call one of the callers of doPullImage instead. -func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goal pullGoal, writer io.Writer, signingOptions SigningOptions, dockerOptions *DockerRegistryOptions, retryOptions *retry.RetryOptions, label *string, progress chan types.ProgressProperties) ([]string, error) { - policyContext, err := getPolicyContext(sc) - if err != nil { - return nil, err - } - defer func() { - if err := policyContext.Destroy(); err != nil { - logrus.Errorf("failed to destroy policy context: %q", err) - } - }() - - var systemRegistriesConfPath string - if dockerOptions != nil && dockerOptions.RegistriesConfPath != "" { - systemRegistriesConfPath = dockerOptions.RegistriesConfPath - } else { - systemRegistriesConfPath = registries.SystemRegistriesConfPath() - } - - var ( - images []string - pullErrors []error - ) - - for _, imageInfo := range goal.refPairs { - copyOptions := getCopyOptions(sc, writer, dockerOptions, nil, signingOptions, "", nil) - copyOptions.SourceCtx.SystemRegistriesConfPath = systemRegistriesConfPath // FIXME: Set this more globally. Probably no reason not to have it in every types.SystemContext, and to compute the value just once in one place. - if progress != nil { - copyOptions.Progress = progress - copyOptions.ProgressInterval = time.Second - } - // Print the following statement only when pulling from a docker or atomic registry - if writer != nil && (imageInfo.srcRef.Transport().Name() == DockerTransport || imageInfo.srcRef.Transport().Name() == AtomicTransport) { - if _, err := io.WriteString(writer, fmt.Sprintf("Trying to pull %s...\n", imageInfo.image)); err != nil { - return nil, err - } - } - // If the label is not nil, check if the label exists and if not, return err - if label != nil { - if err := checkRemoteImageForLabel(ctx, *label, imageInfo, sc); err != nil { - return nil, err - } - } - imageInfo := imageInfo - if err = retry.RetryIfNecessary(ctx, func() error { - _, err = cp.Image(ctx, policyContext, imageInfo.dstRef, imageInfo.srcRef, copyOptions) - return err - }, retryOptions); err != nil { - pullErrors = append(pullErrors, err) - logrus.Debugf("Error pulling image ref %s: %v", imageInfo.srcRef.StringWithinTransport(), err) - if writer != nil { - _, _ = io.WriteString(writer, cleanErrorMessage(err)) - } - } else { - if imageInfo.resolvedShortname != nil { - if err := imageInfo.resolvedShortname.Record(); err != nil { - logrus.Errorf("Error recording short-name alias %q: %v", imageInfo.resolvedShortname.Value.String(), err) - } - } - if !goal.pullAllPairs { - ir.newImageEvent(events.Pull, "") - return []string{imageInfo.image}, nil - } - images = append(images, imageInfo.image) - } - } - // If no image was found, we should handle. Lets be nicer to the user - // and see if we can figure out why. - if len(images) == 0 { - if goal.resolved != nil { - return nil, goal.resolved.FormatPullErrors(pullErrors) - } - return nil, errorhandling.JoinErrors(pullErrors) - } - - ir.newImageEvent(events.Pull, images[0]) - return images, nil -} - -// pullGoalFromPossiblyUnqualifiedName looks at inputName and determines the possible -// image references to try pulling in combination with the registries.conf file as well -func (ir *Runtime) pullGoalFromPossiblyUnqualifiedName(sys *types.SystemContext, writer io.Writer, inputName string) (*pullGoal, error) { - if sys == nil { - sys = &types.SystemContext{} - } - - resolved, err := shortnames.Resolve(sys, inputName) - if err != nil { - return nil, err - } - - if desc := resolved.Description(); len(desc) > 0 { - logrus.Debug(desc) - if writer != nil { - if _, err := writer.Write([]byte(desc + "\n")); err != nil { - return nil, err - } - } - } - - refPairs := []pullRefPair{} - for i, candidate := range resolved.PullCandidates { - srcRef, err := docker.NewReference(candidate.Value) - if err != nil { - return nil, err - } - ps, err := ir.getPullRefPair(srcRef, candidate.Value.String()) - if err != nil { - return nil, err - } - ps.resolvedShortname = &resolved.PullCandidates[i] - refPairs = append(refPairs, ps) - } - return &pullGoal{ - refPairs: refPairs, - pullAllPairs: false, - shortName: inputName, - resolved: resolved, - }, nil -} - -// checkRemoteImageForLabel checks if the remote image has a specific label. if the label exists, we -// return nil, else we return an error -func checkRemoteImageForLabel(ctx context.Context, label string, imageInfo pullRefPair, sc *types.SystemContext) error { - labelImage, err := imageInfo.srcRef.NewImage(ctx, sc) - if err != nil { - return err - } - remoteInspect, err := labelImage.Inspect(ctx) - if err != nil { - return err - } - // Labels are case insensitive; so we iterate instead of simple lookup - for k := range remoteInspect.Labels { - if strings.EqualFold(label, k) { - return nil - } - } - return errors.Errorf("%s has no label %s in %q", imageInfo.image, label, remoteInspect.Labels) -} diff --git a/libpod/image/pull_test.go b/libpod/image/pull_test.go deleted file mode 100644 index d2930451c..000000000 --- a/libpod/image/pull_test.go +++ /dev/null @@ -1,394 +0,0 @@ -package image - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "testing" - - "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/idtools" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// newTestRuntime returns a *Runtime implementation and a cleanup function which the caller is expected to call. -func newTestRuntime(t *testing.T) (*Runtime, func()) { - wd, err := ioutil.TempDir("", "testStorageRuntime") - require.NoError(t, err) - err = os.MkdirAll(wd, 0700) - require.NoError(t, err) - - store, err := storage.GetStore(storage.StoreOptions{ - RunRoot: filepath.Join(wd, "run"), - GraphRoot: filepath.Join(wd, "root"), - GraphDriverName: "vfs", - GraphDriverOptions: []string{}, - UIDMap: []idtools.IDMap{{ - ContainerID: 0, - HostID: os.Getuid(), - Size: 1, - }}, - GIDMap: []idtools.IDMap{{ - ContainerID: 0, - HostID: os.Getgid(), - Size: 1, - }}, - }) - require.NoError(t, err) - - ir := NewImageRuntimeFromStore(store) - cleanup := func() { _ = os.RemoveAll(wd) } - return ir, cleanup -} - -// storageReferenceWithoutLocation returns ref.StringWithinTransport(), -// stripping the [store-specification] prefix from containers/image/storage reference format. -func storageReferenceWithoutLocation(ref types.ImageReference) string { - res := ref.StringWithinTransport() - if res[0] == '[' { - closeIndex := strings.IndexRune(res, ']') - if closeIndex > 0 { - res = res[closeIndex+1:] - } - } - return res -} - -func TestGetPullRefPair(t *testing.T) { - const imageID = "@0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - - ir, cleanup := newTestRuntime(t) - defer cleanup() - - for _, c := range []struct{ srcName, destName, expectedImage, expectedDstName string }{ - // == Source does not have a Docker reference (as is the case for docker-archive:, oci-archive, dir:); destination formats: - { // registry/name, no tag: - "dir:/dev/this-does-not-exist", "example.com/from-directory", - "example.com/from-directory", "example.com/from-directory:latest", - }, - { // name, no registry, no tag: - "dir:/dev/this-does-not-exist", "from-directory", - "localhost/from-directory", "localhost/from-directory:latest", - }, - { // registry/name:tag : - "dir:/dev/this-does-not-exist", "example.com/from-directory:notlatest", - "example.com/from-directory:notlatest", "example.com/from-directory:notlatest", - }, - { // name:tag, no registry: - "dir:/dev/this-does-not-exist", "from-directory:notlatest", - "localhost/from-directory:notlatest", "localhost/from-directory:notlatest", - }, - { // name@digest, no registry: - "dir:/dev/this-does-not-exist", "from-directory" + digestSuffix, - "localhost/from-directory" + digestSuffix, "localhost/from-directory" + digestSuffix, - }, - { // registry/name@digest: - "dir:/dev/this-does-not-exist", "example.com/from-directory" + digestSuffix, - "example.com/from-directory" + digestSuffix, "example.com/from-directory" + digestSuffix, - }, - { // ns/name:tag, no registry: - "dir:/dev/this-does-not-exist", "ns/from-directory:notlatest", - "localhost/ns/from-directory:notlatest", "localhost/ns/from-directory:notlatest", - }, - { // containers-storage image ID - "dir:/dev/this-does-not-exist", imageID, - imageID, imageID, - }, - // == Source does have a Docker reference. - // In that case getPullListFromRef uses the full transport:name input as a destName, - // which would be invalid in the returned dstName - but dstName is derived from the source, so it does not really matter _so_ much. - // Note that unlike real-world use we use different :source and :destination to verify the data flow in more detail. - { // registry/name:tag - "docker://example.com/busybox:source", "docker://example.com/busybox:destination", - "docker://example.com/busybox:destination", "example.com/busybox:source", - }, - { // Implied docker.io/library and :latest - "docker://busybox", "docker://busybox:destination", - "docker://busybox:destination", "docker.io/library/busybox:latest", - }, - // == Invalid destination format. - {"tarball:/dev/null", "tarball:/dev/null", "", ""}, - } { - testDescription := fmt.Sprintf("%#v %#v", c.srcName, c.destName) - srcRef, err := alltransports.ParseImageName(c.srcName) - require.NoError(t, err, testDescription) - - res, err := ir.getPullRefPair(srcRef, c.destName) - if c.expectedDstName == "" { - assert.Error(t, err, testDescription) - } else { - require.NoError(t, err, testDescription) - assert.Equal(t, c.expectedImage, res.image, testDescription) - assert.Equal(t, srcRef, res.srcRef, testDescription) - assert.Equal(t, c.expectedDstName, storageReferenceWithoutLocation(res.dstRef), testDescription) - } - } -} - -func TestPullGoalFromImageReference(t *testing.T) { - ir, cleanup := newTestRuntime(t) - defer cleanup() - - type expected struct{ image, dstName string } - for _, c := range []struct { - srcName string - expected []expected - expectedPullAllPairs bool - }{ - // == docker-archive: - {"docker-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist. - {"docker-archive:/dev/null", nil, false}, // Input exists but does not contain a manifest. - // FIXME: The implementation has extra code for len(manifest) == 0?! That will fail in getImageDigest anyway. - { // RepoTags is empty - "docker-archive:testdata/docker-unnamed.tar.xz", - []expected{{"@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951", "@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951"}}, - true, - }, - { // RepoTags is a [docker.io/library/]name:latest, normalized to the short format. - "docker-archive:testdata/docker-name-only.tar.xz", - []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, - true, - }, - { // RepoTags is a registry/name:latest - "docker-archive:testdata/docker-registry-name.tar.xz", - []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, - true, - }, - { // RepoTags has multiple items for a single image - "docker-archive:testdata/docker-two-names.tar.xz", - []expected{ - {"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}, - {"example.com/empty:latest", "example.com/empty:latest"}, - }, - true, - }, - { // Reference image by name in multi-image archive - "docker-archive:testdata/docker-two-images.tar.xz:example.com/empty:latest", - []expected{ - {"example.com/empty:latest", "example.com/empty:latest"}, - }, - true, - }, - { // Reference image by name in multi-image archive - "docker-archive:testdata/docker-two-images.tar.xz:example.com/empty/but:different", - []expected{ - {"example.com/empty/but:different", "example.com/empty/but:different"}, - }, - true, - }, - { // Reference image by index in multi-image archive - "docker-archive:testdata/docker-two-images.tar.xz:@0", - []expected{ - {"example.com/empty:latest", "example.com/empty:latest"}, - }, - true, - }, - { // Reference image by index in multi-image archive - "docker-archive:testdata/docker-two-images.tar.xz:@1", - []expected{ - {"example.com/empty/but:different", "example.com/empty/but:different"}, - }, - true, - }, - { // Reference entire multi-image archive must fail (more than one manifest) - "docker-archive:testdata/docker-two-images.tar.xz", - []expected{}, - true, - }, - - // == oci-archive: - {"oci-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist. - {"oci-archive:/dev/null", nil, false}, // Input exists but does not contain a manifest. - // FIXME: The remaining tests are commented out for now, because oci-archive: does not work unprivileged. - // { // No name annotation - // "oci-archive:testdata/oci-unnamed.tar.gz", - // []expected{{"@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6", "@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6"}}, - // false, - // }, - // { // Name is a name:latest (no normalization is defined). - // "oci-archive:testdata/oci-name-only.tar.gz", - // []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, - // false, - // }, - // { // Name is a registry/name:latest - // "oci-archive:testdata/oci-registry-name.tar.gz", - // []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, - // false, - // }, - // // Name exists, but is an invalid Docker reference; such names will fail when creating dstReference. - // {"oci-archive:testdata/oci-non-docker-name.tar.gz", nil, false}, - // Maybe test support of two images in a single archive? It should be transparently handled by adding a reference to srcRef. - - // == dir: - { // Absolute path - "dir:/dev/this-does-not-exist", - []expected{{"localhost/dev/this-does-not-exist", "localhost/dev/this-does-not-exist:latest"}}, - false, - }, - { // Relative path, single element. - "dir:this-does-not-exist", - []expected{{"localhost/this-does-not-exist", "localhost/this-does-not-exist:latest"}}, - false, - }, - { // Relative path, multiple elements. - "dir:testdata/this-does-not-exist", - []expected{{"localhost/testdata/this-does-not-exist", "localhost/testdata/this-does-not-exist:latest"}}, - false, - }, - - // == Others, notably: - // === docker:// (has ImageReference.DockerReference) - { // Fully-specified input - "docker://docker.io/library/busybox:latest", - []expected{{"docker://docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}}, - false, - }, - { // Minimal form of the input - "docker://busybox", - []expected{{"docker://busybox", "docker.io/library/busybox:latest"}}, - false, - }, - - // === tarball: (as an example of what happens when ImageReference.DockerReference is nil). - // FIXME? This tries to parse "tarball:/dev/null" as a storageReference, and fails. - // (This is NOT an API promise that the results will continue to be this way.) - {"tarball:/dev/null", nil, false}, - } { - srcRef, err := alltransports.ParseImageName(c.srcName) - require.NoError(t, err, c.srcName) - - res, err := ir.pullGoalFromImageReference(context.Background(), srcRef, c.srcName, nil) - if len(c.expected) == 0 { - assert.Error(t, err, c.srcName) - } else { - require.NoError(t, err, c.srcName) - require.Len(t, res.refPairs, len(c.expected), c.srcName) - for i, e := range c.expected { - testDescription := fmt.Sprintf("%s #%d", c.srcName, i) - assert.Equal(t, e.image, res.refPairs[i].image, testDescription) - assert.Equal(t, transports.ImageName(srcRef), transports.ImageName(res.refPairs[i].srcRef), testDescription) - assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription) - } - assert.Equal(t, c.expectedPullAllPairs, res.pullAllPairs, c.srcName) - } - } -} - -const registriesConfWithSearch = `unqualified-search-registries = ['example.com', 'docker.io']` - -func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) { - const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - type pullRefStrings struct{ image, srcRef, dstName string } // pullRefPair with string data only - - registriesConf, err := ioutil.TempFile("", "TestPullGoalFromPossiblyUnqualifiedName") - require.NoError(t, err) - defer registriesConf.Close() - defer os.Remove(registriesConf.Name()) - - err = ioutil.WriteFile(registriesConf.Name(), []byte(registriesConfWithSearch), 0600) - require.NoError(t, err) - - ir, cleanup := newTestRuntime(t) - defer cleanup() - - sc := GetSystemContext("", "", false) - - aliasesConf, err := ioutil.TempFile("", "short-name-aliases.conf") - require.NoError(t, err) - defer aliasesConf.Close() - defer os.Remove(aliasesConf.Name()) - sc.UserShortNameAliasConfPath = aliasesConf.Name() - sc.SystemRegistriesConfPath = registriesConf.Name() - - // Make sure to not sure the system's registries.conf.d - dir, err := ioutil.TempDir("", "example") - require.NoError(t, err) - sc.SystemRegistriesConfDirPath = dir - defer os.RemoveAll(dir) // clean up - - for _, c := range []struct { - input string - expected []pullRefStrings - }{ - {"#", nil}, // Clearly invalid. - { // Fully-explicit docker.io, name-only. - "docker.io/library/busybox", - // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) - []pullRefStrings{{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}}, - }, - { // docker.io with implied /library/, name-only. - "docker.io/busybox", - // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) - []pullRefStrings{{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}}, - }, - { // Qualified example.com, name-only. - "example.com/ns/busybox", - []pullRefStrings{{"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}}, - }, - { // Qualified example.com, name:tag. - "example.com/ns/busybox:notlatest", - []pullRefStrings{{"example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}}, - }, - { // Qualified example.com, name@digest. - "example.com/ns/busybox" + digestSuffix, - []pullRefStrings{{"example.com/ns/busybox" + digestSuffix, "docker://example.com/ns/busybox" + digestSuffix, - "example.com/ns/busybox" + digestSuffix}}, - }, - // Qualified example.com, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input. - {"example.com/ns/busybox:notlatest" + digestSuffix, nil}, - { // Unqualified, single-name, name-only - "busybox", - []pullRefStrings{ - {"example.com/busybox:latest", "docker://example.com/busybox:latest", "example.com/busybox:latest"}, - // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) - {"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}, - }, - }, - { // Unqualified, namespaced, name-only - "ns/busybox", - []pullRefStrings{ - {"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}, - }, - }, - { // Unqualified, name:tag - "busybox:notlatest", - []pullRefStrings{ - {"example.com/busybox:notlatest", "docker://example.com/busybox:notlatest", "example.com/busybox:notlatest"}, - // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) - {"docker.io/library/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"}, - }, - }, - { // Unqualified, name@digest - "busybox" + digestSuffix, - []pullRefStrings{ - {"example.com/busybox" + digestSuffix, "docker://example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix}, - // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) - {"docker.io/library/busybox" + digestSuffix, "docker://busybox" + digestSuffix, "docker.io/library/busybox" + digestSuffix}, - }, - }, - // Unqualified, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input. - {"busybox:notlatest" + digestSuffix, nil}, - } { - res, err := ir.pullGoalFromPossiblyUnqualifiedName(sc, nil, c.input) - if len(c.expected) == 0 { - assert.Error(t, err, c.input) - } else { - assert.NoError(t, err, c.input) - for i, e := range c.expected { - testDescription := fmt.Sprintf("%s #%d (%v)", c.input, i, res.refPairs) - assert.Equal(t, e.image, res.refPairs[i].image, testDescription) - assert.Equal(t, e.srcRef, transports.ImageName(res.refPairs[i].srcRef), testDescription) - assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription) - } - assert.False(t, res.pullAllPairs, c.input) - } - } -} diff --git a/libpod/image/search.go b/libpod/image/search.go deleted file mode 100644 index 714551e6e..000000000 --- a/libpod/image/search.go +++ /dev/null @@ -1,318 +0,0 @@ -package image - -import ( - "context" - "fmt" - "strconv" - "strings" - "sync" - - "github.com/containers/image/v5/docker" - "github.com/containers/image/v5/transports/alltransports" - "github.com/containers/image/v5/types" - sysreg "github.com/containers/podman/v3/pkg/registries" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "golang.org/x/sync/semaphore" -) - -const ( - descriptionTruncLength = 44 - maxQueries = 25 - maxParallelSearches = int64(6) -) - -// SearchResult is holding image-search related data. -type SearchResult struct { - // Index is the image index (e.g., "docker.io" or "quay.io") - Index string - // Name is the canonical name of the image (e.g., "docker.io/library/alpine"). - Name string - // Description of the image. - Description string - // Stars is the number of stars of the image. - Stars int - // Official indicates if it's an official image. - Official string - // Automated indicates if the image was created by an automated build. - Automated string - // Tag is the image tag - Tag string -} - -// SearchOptions are used to control the behaviour of SearchImages. -type SearchOptions struct { - // Filter allows to filter the results. - Filter SearchFilter - // Limit limits the number of queries per index (default: 25). Must be - // greater than 0 to overwrite the default value. - Limit int - // NoTrunc avoids the output to be truncated. - NoTrunc bool - // Authfile is the path to the authentication file. - Authfile string - // InsecureSkipTLSVerify allows to skip TLS verification. - InsecureSkipTLSVerify types.OptionalBool - // ListTags returns the search result with available tags - ListTags bool -} - -// SearchFilter allows filtering the results of SearchImages. -type SearchFilter struct { - // Stars describes the minimal amount of starts of an image. - Stars int - // IsAutomated decides if only images from automated builds are displayed. - IsAutomated types.OptionalBool - // IsOfficial decides if only official images are displayed. - IsOfficial types.OptionalBool -} - -// SearchImages searches images based on term and the specified SearchOptions -// in all registries. -func SearchImages(term string, options SearchOptions) ([]SearchResult, error) { - registry := "" - - // Try to extract a registry from the specified search term. We - // consider everything before the first slash to be the registry. Note - // that we cannot use the reference parser from the containers/image - // library as the search term may container arbitrary input such as - // wildcards. See bugzilla.redhat.com/show_bug.cgi?id=1846629. - if spl := strings.SplitN(term, "/", 2); len(spl) > 1 { - registry = spl[0] - term = spl[1] - } - - registries, err := getRegistries(registry) - if err != nil { - return nil, err - } - - // searchOutputData is used as a return value for searching in parallel. - type searchOutputData struct { - data []SearchResult - err error - } - - // Let's follow Firefox by limiting parallel downloads to 6. - sem := semaphore.NewWeighted(maxParallelSearches) - wg := sync.WaitGroup{} - wg.Add(len(registries)) - data := make([]searchOutputData, len(registries)) - - searchImageInRegistryHelper := func(index int, registry string) { - defer sem.Release(1) - defer wg.Done() - searchOutput, err := searchImageInRegistry(term, registry, options) - data[index] = searchOutputData{data: searchOutput, err: err} - } - - ctx := context.Background() - for i := range registries { - if err := sem.Acquire(ctx, 1); err != nil { - return nil, err - } - go searchImageInRegistryHelper(i, registries[i]) - } - - wg.Wait() - results := []SearchResult{} - var lastError error - for _, d := range data { - if d.err != nil { - if lastError != nil { - logrus.Errorf("%v", lastError) - } - lastError = d.err - continue - } - results = append(results, d.data...) - } - if len(results) > 0 { - return results, nil - } - return results, lastError -} - -// getRegistries returns the list of registries to search, depending on an optional registry specification -func getRegistries(registry string) ([]string, error) { - var registries []string - if registry != "" { - registries = append(registries, registry) - } else { - var err error - registries, err = sysreg.GetRegistries() - if err != nil { - return nil, errors.Wrapf(err, "error getting registries to search") - } - } - return registries, nil -} - -func searchImageInRegistry(term string, registry string, options SearchOptions) ([]SearchResult, error) { - // Max number of queries by default is 25 - limit := maxQueries - if options.Limit > 0 { - limit = options.Limit - } - - sc := GetSystemContext("", options.Authfile, false) - sc.DockerInsecureSkipTLSVerify = options.InsecureSkipTLSVerify - // FIXME: Set this more globally. Probably no reason not to have it in - // every types.SystemContext, and to compute the value just once in one - // place. - sc.SystemRegistriesConfPath = sysreg.SystemRegistriesConfPath() - if options.ListTags { - results, err := searchRepositoryTags(registry, term, sc, options) - if err != nil { - return []SearchResult{}, err - } - return results, nil - } - - results, err := docker.SearchRegistry(context.TODO(), sc, registry, term, limit) - if err != nil { - return []SearchResult{}, err - } - index := registry - arr := strings.Split(registry, ".") - if len(arr) > 2 { - index = strings.Join(arr[len(arr)-2:], ".") - } - - // limit is the number of results to output - // if the total number of results is less than the limit, output all - // if the limit has been set by the user, output those number of queries - limit = maxQueries - if len(results) < limit { - limit = len(results) - } - if options.Limit != 0 { - limit = len(results) - if options.Limit < len(results) { - limit = options.Limit - } - } - - paramsArr := []SearchResult{} - for i := 0; i < limit; i++ { - // Check whether query matches filters - if !(options.Filter.matchesAutomatedFilter(results[i]) && options.Filter.matchesOfficialFilter(results[i]) && options.Filter.matchesStarFilter(results[i])) { - continue - } - official := "" - if results[i].IsOfficial { - official = "[OK]" - } - automated := "" - if results[i].IsAutomated { - automated = "[OK]" - } - description := strings.Replace(results[i].Description, "\n", " ", -1) - if len(description) > 44 && !options.NoTrunc { - description = description[:descriptionTruncLength] + "..." - } - name := registry + "/" + results[i].Name - if index == "docker.io" && !strings.Contains(results[i].Name, "/") { - name = index + "/library/" + results[i].Name - } - params := SearchResult{ - Index: index, - Name: name, - Description: description, - Official: official, - Automated: automated, - Stars: results[i].StarCount, - } - paramsArr = append(paramsArr, params) - } - return paramsArr, nil -} - -func searchRepositoryTags(registry, term string, sc *types.SystemContext, options SearchOptions) ([]SearchResult, error) { - dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) - imageRef, err := alltransports.ParseImageName(fmt.Sprintf("%s/%s", registry, term)) - if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { - return nil, errors.Errorf("reference %q must be a docker reference", term) - } else if err != nil { - imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, fmt.Sprintf("%s/%s", registry, term))) - if err != nil { - return nil, errors.Errorf("reference %q must be a docker reference", term) - } - } - tags, err := docker.GetRepositoryTags(context.TODO(), sc, imageRef) - if err != nil { - return nil, errors.Errorf("error getting repository tags: %v", err) - } - limit := maxQueries - if len(tags) < limit { - limit = len(tags) - } - if options.Limit != 0 { - limit = len(tags) - if options.Limit < limit { - limit = options.Limit - } - } - paramsArr := []SearchResult{} - for i := 0; i < limit; i++ { - params := SearchResult{ - Name: imageRef.DockerReference().Name(), - Tag: tags[i], - } - paramsArr = append(paramsArr, params) - } - return paramsArr, nil -} - -// ParseSearchFilter turns the filter into a SearchFilter that can be used for -// searching images. -func ParseSearchFilter(filter []string) (*SearchFilter, error) { - sFilter := new(SearchFilter) - for _, f := range filter { - arr := strings.SplitN(f, "=", 2) - switch arr[0] { - case "stars": - if len(arr) < 2 { - return nil, errors.Errorf("invalid `stars` filter %q, should be stars=", filter) - } - stars, err := strconv.Atoi(arr[1]) - if err != nil { - return nil, errors.Wrapf(err, "incorrect value type for stars filter") - } - sFilter.Stars = stars - case "is-automated": - if len(arr) == 2 && arr[1] == "false" { - sFilter.IsAutomated = types.OptionalBoolFalse - } else { - sFilter.IsAutomated = types.OptionalBoolTrue - } - case "is-official": - if len(arr) == 2 && arr[1] == "false" { - sFilter.IsOfficial = types.OptionalBoolFalse - } else { - sFilter.IsOfficial = types.OptionalBoolTrue - } - default: - return nil, errors.Errorf("invalid filter type %q", f) - } - } - return sFilter, nil -} - -func (f *SearchFilter) matchesStarFilter(result docker.SearchResult) bool { - return result.StarCount >= f.Stars -} - -func (f *SearchFilter) matchesAutomatedFilter(result docker.SearchResult) bool { - if f.IsAutomated != types.OptionalBoolUndefined { - return result.IsAutomated == (f.IsAutomated == types.OptionalBoolTrue) - } - return true -} - -func (f *SearchFilter) matchesOfficialFilter(result docker.SearchResult) bool { - if f.IsOfficial != types.OptionalBoolUndefined { - return result.IsOfficial == (f.IsOfficial == types.OptionalBoolTrue) - } - return true -} diff --git a/libpod/image/signing_options.go b/libpod/image/signing_options.go deleted file mode 100644 index f310da749..000000000 --- a/libpod/image/signing_options.go +++ /dev/null @@ -1,10 +0,0 @@ -package image - -// SigningOptions encapsulates settings that control whether or not we strip or -// add signatures to images when writing them. -type SigningOptions struct { - // RemoveSignatures directs us to remove any signatures which are already present. - RemoveSignatures bool - // SignBy is a key identifier of some kind, indicating that a signature should be generated using the specified private key and stored with the image. - SignBy string -} diff --git a/libpod/image/testdata/docker-name-only.tar.xz b/libpod/image/testdata/docker-name-only.tar.xz deleted file mode 100644 index 0cad9f108..000000000 Binary files a/libpod/image/testdata/docker-name-only.tar.xz and /dev/null differ diff --git a/libpod/image/testdata/docker-registry-name.tar.xz b/libpod/image/testdata/docker-registry-name.tar.xz deleted file mode 100644 index 181816c2e..000000000 Binary files a/libpod/image/testdata/docker-registry-name.tar.xz and /dev/null differ diff --git a/libpod/image/testdata/docker-two-images.tar.xz b/libpod/image/testdata/docker-two-images.tar.xz deleted file mode 100644 index 148d8a86b..000000000 Binary files a/libpod/image/testdata/docker-two-images.tar.xz and /dev/null differ diff --git a/libpod/image/testdata/docker-two-names.tar.xz b/libpod/image/testdata/docker-two-names.tar.xz deleted file mode 100644 index 07fbc479c..000000000 Binary files a/libpod/image/testdata/docker-two-names.tar.xz and /dev/null differ diff --git a/libpod/image/testdata/docker-unnamed.tar.xz b/libpod/image/testdata/docker-unnamed.tar.xz deleted file mode 100644 index ba6ea1bae..000000000 Binary files a/libpod/image/testdata/docker-unnamed.tar.xz and /dev/null differ diff --git a/libpod/image/testdata/oci-name-only.tar.gz b/libpod/image/testdata/oci-name-only.tar.gz deleted file mode 100644 index 57bc07564..000000000 Binary files a/libpod/image/testdata/oci-name-only.tar.gz and /dev/null differ diff --git a/libpod/image/testdata/oci-non-docker-name.tar.gz b/libpod/image/testdata/oci-non-docker-name.tar.gz deleted file mode 100644 index 5ffc0eabd..000000000 Binary files a/libpod/image/testdata/oci-non-docker-name.tar.gz and /dev/null differ diff --git a/libpod/image/testdata/oci-registry-name.tar.gz b/libpod/image/testdata/oci-registry-name.tar.gz deleted file mode 100644 index e6df87339..000000000 Binary files a/libpod/image/testdata/oci-registry-name.tar.gz and /dev/null differ diff --git a/libpod/image/testdata/oci-unnamed.tar.gz b/libpod/image/testdata/oci-unnamed.tar.gz deleted file mode 100644 index de445fdf8..000000000 Binary files a/libpod/image/testdata/oci-unnamed.tar.gz and /dev/null differ diff --git a/libpod/image/testdata/registries.conf b/libpod/image/testdata/registries.conf deleted file mode 100644 index 16622a1ac..000000000 --- a/libpod/image/testdata/registries.conf +++ /dev/null @@ -1,4 +0,0 @@ -short-name-mode="enforcing" - -[aliases] -"busybox"="docker.io/library/busybox" diff --git a/libpod/image/tree.go b/libpod/image/tree.go deleted file mode 100644 index c7c69462f..000000000 --- a/libpod/image/tree.go +++ /dev/null @@ -1,138 +0,0 @@ -package image - -import ( - "context" - "fmt" - "strings" - - "github.com/docker/go-units" - "github.com/pkg/errors" -) - -const ( - middleItem = "├── " - continueItem = "│ " - lastItem = "└── " -) - -type tree struct { - img *Image - imageInfo *InfoImage - layerInfo map[string]*LayerInfo - sb *strings.Builder -} - -// GenerateTree creates an image tree string representation for displaying it -// to the user. -func (i *Image) GenerateTree(whatRequires bool) (string, error) { - // Fetch map of image-layers, which is used for printing output. - layerInfo, err := GetLayersMapWithImageInfo(i.imageruntime) - if err != nil { - return "", errors.Wrapf(err, "error while retrieving layers of image %q", i.InputName) - } - - // Create an imageInfo and fill the image and layer info - imageInfo := &InfoImage{ - ID: i.ID(), - Tags: i.Names(), - } - - if err := BuildImageHierarchyMap(imageInfo, layerInfo, i.TopLayer()); err != nil { - return "", err - } - sb := &strings.Builder{} - tree := &tree{i, imageInfo, layerInfo, sb} - if err := tree.print(whatRequires); err != nil { - return "", err - } - return tree.string(), nil -} - -func (t *tree) string() string { - return t.sb.String() -} - -func (t *tree) print(whatRequires bool) error { - size, err := t.img.Size(context.Background()) - if err != nil { - return err - } - - fmt.Fprintf(t.sb, "Image ID: %s\n", t.imageInfo.ID[:12]) - fmt.Fprintf(t.sb, "Tags: %s\n", t.imageInfo.Tags) - fmt.Fprintf(t.sb, "Size: %v\n", units.HumanSizeWithPrecision(float64(*size), 4)) - if t.img.TopLayer() != "" { - fmt.Fprintf(t.sb, "Image Layers\n") - } else { - fmt.Fprintf(t.sb, "No Image Layers\n") - } - - if !whatRequires { - // fill imageInfo with layers associated with image. - // the layers will be filled such that - // (Start)RootLayer->...intermediate Parent Layer(s)-> TopLayer(End) - // Build output from imageInfo into buffer - t.printImageHierarchy(t.imageInfo) - } else { - // fill imageInfo with layers associated with image. - // the layers will be filled such that - // (Start)TopLayer->...intermediate Child Layer(s)-> Child TopLayer(End) - // (Forks)... intermediate Child Layer(s) -> Child Top Layer(End) - return t.printImageChildren(t.layerInfo, t.img.TopLayer(), "", true) - } - return nil -} - -// Stores all children layers which are created using given Image. -// Layers are stored as follows -// (Start)TopLayer->...intermediate Child Layer(s)-> Child TopLayer(End) -// (Forks)... intermediate Child Layer(s) -> Child Top Layer(End) -func (t *tree) printImageChildren(layerMap map[string]*LayerInfo, layerID string, prefix string, last bool) error { - if layerID == "" { - return nil - } - ll, ok := layerMap[layerID] - if !ok { - return fmt.Errorf("lookup error: layerid %s, not found", layerID) - } - fmt.Fprint(t.sb, prefix) - - //initialize intend with middleItem to reduce middleItem checks. - intend := middleItem - if !last { - // add continueItem i.e. '|' for next iteration prefix - prefix += continueItem - } else if len(ll.ChildID) > 1 || len(ll.ChildID) == 0 { - // The above condition ensure, alignment happens for node, which has more then 1 children. - // If node is last in printing hierarchy, it should not be printed as middleItem i.e. ├── - intend = lastItem - prefix += " " - } - - var tags string - if len(ll.RepoTags) > 0 { - tags = fmt.Sprintf(" Top Layer of: %s", ll.RepoTags) - } - fmt.Fprintf(t.sb, "%sID: %s Size: %7v%s\n", intend, ll.ID[:12], units.HumanSizeWithPrecision(float64(ll.Size), 4), tags) - for count, childID := range ll.ChildID { - if err := t.printImageChildren(layerMap, childID, prefix, count == len(ll.ChildID)-1); err != nil { - return err - } - } - return nil -} - -// prints the layers info of image -func (t *tree) printImageHierarchy(imageInfo *InfoImage) { - for count, l := range imageInfo.Layers { - var tags string - intend := middleItem - if len(l.RepoTags) > 0 { - tags = fmt.Sprintf(" Top Layer of: %s", l.RepoTags) - } - if count == len(imageInfo.Layers)-1 { - intend = lastItem - } - fmt.Fprintf(t.sb, "%s ID: %s Size: %7v%s\n", intend, l.ID[:12], units.HumanSizeWithPrecision(float64(l.Size), 4), tags) - } -} diff --git a/libpod/image/utils.go b/libpod/image/utils.go deleted file mode 100644 index dfe35c017..000000000 --- a/libpod/image/utils.go +++ /dev/null @@ -1,182 +0,0 @@ -package image - -import ( - "fmt" - "io" - "net/url" - "regexp" - "strings" - - cp "github.com/containers/image/v5/copy" - "github.com/containers/image/v5/docker/reference" - "github.com/containers/image/v5/signature" - "github.com/containers/image/v5/types" - "github.com/containers/podman/v3/libpod/define" - "github.com/containers/storage" - "github.com/pkg/errors" -) - -// findImageInRepotags takes an imageParts struct and searches images' repotags for -// a match on name:tag -func findImageInRepotags(search imageParts, images []*Image) (*storage.Image, error) { - _, searchName, searchSuspiciousTagValueForSearch := search.suspiciousRefNameTagValuesForSearch() - type Candidate struct { - name string - image *Image - } - var candidates []Candidate - for _, image := range images { - for _, name := range image.Names() { - d, err := decompose(name) - // if we get an error, ignore and keep going - if err != nil { - continue - } - _, dName, dSuspiciousTagValueForSearch := d.suspiciousRefNameTagValuesForSearch() - if dSuspiciousTagValueForSearch != searchSuspiciousTagValueForSearch { - continue - } - if dName == searchName || strings.HasSuffix(dName, "/"+searchName) { - candidates = append(candidates, Candidate{ - name: name, - image: image, - }) - } - } - } - if len(candidates) == 0 { - return nil, errors.Wrapf(define.ErrNoSuchImage, "unable to find a name and tag match for %s in repotags", searchName) - } - - // If more then one candidate and the candidates all have same name - // and only one is read/write return it. - // Otherwise return error with the list of candidates - if len(candidates) > 1 { - var ( - rwImage *Image - rwImageCnt int - ) - names := make(map[string]bool) - for _, c := range candidates { - names[c.name] = true - if !c.image.IsReadOnly() { - rwImageCnt++ - rwImage = c.image - } - } - // If only one name used and have read/write image return it - if len(names) == 1 && rwImageCnt == 1 { - return rwImage.image, nil - } - keys := []string{} - for k := range names { - keys = append(keys, k) - } - if rwImageCnt > 1 { - return nil, errors.Wrapf(define.ErrMultipleImages, "found multiple read/write images %s", strings.Join(keys, ",")) - } - return nil, errors.Wrapf(define.ErrMultipleImages, "found multiple read/only images %s", strings.Join(keys, ",")) - } - return candidates[0].image.image, nil -} - -// getCopyOptions constructs a new containers/image/copy.Options{} struct from the given parameters, inheriting some from sc. -func getCopyOptions(sc *types.SystemContext, reportWriter io.Writer, srcDockerRegistry, destDockerRegistry *DockerRegistryOptions, signing SigningOptions, manifestType string, additionalDockerArchiveTags []reference.NamedTagged) *cp.Options { - if srcDockerRegistry == nil { - srcDockerRegistry = &DockerRegistryOptions{} - } - if destDockerRegistry == nil { - destDockerRegistry = &DockerRegistryOptions{} - } - srcContext := srcDockerRegistry.GetSystemContext(sc, additionalDockerArchiveTags) - destContext := destDockerRegistry.GetSystemContext(sc, additionalDockerArchiveTags) - return &cp.Options{ - RemoveSignatures: signing.RemoveSignatures, - SignBy: signing.SignBy, - ReportWriter: reportWriter, - SourceCtx: srcContext, - DestinationCtx: destContext, - ForceManifestMIMEType: manifestType, - } -} - -// getPolicyContext sets up, initializes and returns a new context for the specified policy -func getPolicyContext(ctx *types.SystemContext) (*signature.PolicyContext, error) { - policy, err := signature.DefaultPolicy(ctx) - if err != nil { - return nil, err - } - - policyContext, err := signature.NewPolicyContext(policy) - if err != nil { - return nil, err - } - return policyContext, nil -} - -// hasTransport determines if the image string contains '://', returns bool -func hasTransport(image string) bool { - return strings.Contains(image, "://") -} - -// GetAdditionalTags returns a list of reference.NamedTagged for the -// additional tags given in images -func GetAdditionalTags(images []string) ([]reference.NamedTagged, error) { - var allTags []reference.NamedTagged - for _, img := range images { - ref, err := reference.ParseNormalizedNamed(img) - if err != nil { - return nil, errors.Wrapf(err, "error parsing additional tags") - } - refTagged, isTagged := ref.(reference.NamedTagged) - if isTagged { - allTags = append(allTags, refTagged) - } - } - return allTags, nil -} - -// IsValidImageURI checks if image name has valid format -func IsValidImageURI(imguri string) (bool, error) { - uri := "http://" + imguri - u, err := url.Parse(uri) - if err != nil { - return false, errors.Wrapf(err, "invalid image uri: %s", imguri) - } - reg := regexp.MustCompile(`^[a-zA-Z0-9-_\.]+\/?:?[0-9]*[a-z0-9-\/:]*$`) - ret := reg.FindAllString(u.Host, -1) - if len(ret) == 0 { - return false, errors.Wrapf(err, "invalid image uri: %s", imguri) - } - reg = regexp.MustCompile(`^[a-z0-9-:\./]*$`) - ret = reg.FindAllString(u.Fragment, -1) - if len(ret) == 0 { - return false, errors.Wrapf(err, "invalid image uri: %s", imguri) - } - return true, nil -} - -// imageNameForSaveDestination returns a Docker-like reference appropriate for saving img, -// which the user referred to as imgUserInput; or an empty string, if there is no appropriate -// reference. -func imageNameForSaveDestination(img *Image, imgUserInput string) string { - if strings.Contains(img.ID(), imgUserInput) { - return "" - } - - prepend := "" - localRegistryPrefix := fmt.Sprintf("%s/", DefaultLocalRegistry) - if !strings.HasPrefix(imgUserInput, localRegistryPrefix) { - // we need to check if localhost was added to the image name in NewFromLocal - for _, name := range img.Names() { - // If the user is saving an image in the localhost registry, getLocalImage need - // a name that matches the format localhost/: or localhost/:latest to correctly - // set up the manifest and save. - if strings.HasPrefix(name, localRegistryPrefix) && (strings.HasSuffix(name, imgUserInput) || strings.HasSuffix(name, fmt.Sprintf("%s:latest", imgUserInput))) { - prepend = localRegistryPrefix - break - } - } - } - return fmt.Sprintf("%s%s", prepend, imgUserInput) -} -- cgit v1.2.3-54-g00ecf