From 6979d140f1c531fd32e885542be27407105ebf90 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Tue, 28 Jul 2020 10:25:14 -0400 Subject: Add podman image mount There are many use cases where you want to just mount an image without creating a container on it. For example you might want to just examine the content in an image after you pull it for security analysys. Or you might want to just use the executables on the image without running it in a container. The image is mounted readonly since we do not want people changing images. Signed-off-by: Daniel J Walsh --- pkg/domain/entities/engine_image.go | 2 + pkg/domain/entities/images.go | 30 ++++++++- pkg/domain/infra/abi/images.go | 126 ++++++++++++++++++++++++++++++++++-- pkg/domain/infra/tunnel/images.go | 8 +++ pkg/rootless/rootless_linux.c | 3 +- 5 files changed, 163 insertions(+), 6 deletions(-) (limited to 'pkg') diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 7ece24c60..594f9617f 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -16,6 +16,7 @@ type ImageEngine interface { Inspect(ctx context.Context, namesOrIDs []string, opts InspectOptions) ([]*ImageInspectReport, []error, error) List(ctx context.Context, opts ImageListOptions) ([]*ImageSummary, error) Load(ctx context.Context, opts ImageLoadOptions) (*ImageLoadReport, error) + Mount(ctx context.Context, images []string, options ImageMountOptions) ([]*ImageMountReport, error) Prune(ctx context.Context, opts ImagePruneOptions) (*ImagePruneReport, error) Pull(ctx context.Context, rawImage string, opts ImagePullOptions) (*ImagePullReport, error) Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error @@ -27,6 +28,7 @@ type ImageEngine interface { Shutdown(ctx context.Context) Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error) + Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error) Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error ManifestCreate(ctx context.Context, names, images []string, opts ManifestCreateOptions) (string, error) ManifestInspect(ctx context.Context, name string) ([]byte, error) diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index b38facfb5..cb970b09a 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -91,7 +91,7 @@ type ImageRemoveOptions struct { } // ImageRemoveResponse is the response for removing one or more image(s) from storage -// and containers what was untagged vs actually removed. +// and images what was untagged vs actually removed. type ImageRemoveReport struct { // Deleted images. Deleted []string `json:",omitempty"` @@ -318,3 +318,31 @@ type SignOptions struct { // SignReport describes the result of signing type SignReport struct{} + +// ImageMountOptions describes the input values for mounting images +// in the CLI +type ImageMountOptions struct { + All bool + Format string +} + +// ImageUnmountOptions are the options from the cli for unmounting +type ImageUnmountOptions struct { + All bool + Force bool +} + +// ImageMountReport describes the response from image mount +type ImageMountReport struct { + Err error + Id string //nolint + Name string + Repositories []string + Path string +} + +// ImageUnmountReport describes the response from umounting an image +type ImageUnmountReport struct { + Err error + Id string //nolint +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index e2fe8a5e6..10ec7f8f3 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -11,8 +11,6 @@ import ( "strconv" "strings" - "github.com/containers/podman/v2/pkg/rootless" - "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker" dockerarchive "github.com/containers/image/v5/docker/archive" @@ -27,6 +25,7 @@ import ( libpodImage "github.com/containers/podman/v2/libpod/image" "github.com/containers/podman/v2/pkg/domain/entities" domainUtils "github.com/containers/podman/v2/pkg/domain/utils" + "github.com/containers/podman/v2/pkg/rootless" "github.com/containers/podman/v2/pkg/trust" "github.com/containers/podman/v2/pkg/util" "github.com/containers/storage" @@ -85,6 +84,125 @@ func (ir *ImageEngine) History(ctx context.Context, nameOrID string, opts entiti return &history, nil } +func (ir *ImageEngine) Mount(ctx context.Context, nameOrIDs []string, opts entities.ImageMountOptions) ([]*entities.ImageMountReport, error) { + var ( + images []*image.Image + err error + ) + if os.Geteuid() != 0 { + if driver := ir.Libpod.StorageConfig().GraphDriverName; driver != "vfs" { + // Do not allow to mount a graphdriver that is not vfs if we are creating the userns as part + // of the mount command. + return nil, errors.Errorf("cannot mount using driver %s in rootless mode", driver) + } + + became, ret, err := rootless.BecomeRootInUserNS("") + if err != nil { + return nil, err + } + if became { + os.Exit(ret) + } + } + if opts.All { + allImages, err := ir.Libpod.ImageRuntime().GetImages() + if err != nil { + return nil, err + } + for _, img := range allImages { + if !img.IsReadOnly() { + images = append(images, img) + } + } + } else { + for _, i := range nameOrIDs { + img, err := ir.Libpod.ImageRuntime().NewFromLocal(i) + if err != nil { + return nil, err + } + images = append(images, img) + } + } + reports := make([]*entities.ImageMountReport, 0, len(images)) + for _, img := range images { + report := entities.ImageMountReport{Id: img.ID()} + if img.IsReadOnly() { + report.Err = errors.Errorf("mounting readonly %s image not supported", img.ID()) + } else { + report.Path, report.Err = img.Mount([]string{}, "") + } + reports = append(reports, &report) + } + if len(reports) > 0 { + return reports, nil + } + + images, err = ir.Libpod.ImageRuntime().GetImages() + if err != nil { + return nil, err + } + for _, i := range images { + mounted, path, err := i.Mounted() + if err != nil { + if errors.Cause(err) == storage.ErrLayerUnknown { + continue + } + return nil, err + } + if mounted { + tags, err := i.RepoTags() + if err != nil { + return nil, err + } + reports = append(reports, &entities.ImageMountReport{ + Id: i.ID(), + Name: string(i.Digest()), + Repositories: tags, + Path: path, + }) + } + } + return reports, nil +} + +func (ir *ImageEngine) Unmount(ctx context.Context, nameOrIDs []string, options entities.ImageUnmountOptions) ([]*entities.ImageUnmountReport, error) { + var images []*image.Image + + if options.All { + allImages, err := ir.Libpod.ImageRuntime().GetImages() + if err != nil { + return nil, err + } + for _, img := range allImages { + if !img.IsReadOnly() { + images = append(images, img) + } + } + } else { + for _, i := range nameOrIDs { + img, err := ir.Libpod.ImageRuntime().NewFromLocal(i) + if err != nil { + return nil, err + } + images = append(images, img) + } + } + + reports := []*entities.ImageUnmountReport{} + for _, img := range images { + report := entities.ImageUnmountReport{Id: img.ID()} + if err := img.Unmount(options.Force); err != nil { + if options.All && errors.Cause(err) == storage.ErrLayerNotMounted { + logrus.Debugf("Error umounting image %s, storage.ErrLayerNotMounted", img.ID()) + continue + } + report.Err = errors.Wrapf(err, "error unmounting image %s", img.ID()) + } + reports = append(reports, &report) + } + return reports, nil +} + func ToDomainHistoryLayer(layer *libpodImage.History) entities.ImageHistoryLayer { l := entities.ImageHistoryLayer{} l.ID = layer.ID @@ -225,7 +343,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri case "v2s2", "docker": manifestType = manifest.DockerV2Schema2MediaType default: - return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", options.Format) + return errors.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", options.Format) } var registryCreds *types.DockerAuthConfig @@ -292,7 +410,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri // // // TODO: Determine Size // report := entities.ImagePruneReport{} -// copy(report.Report.Id, id) +// copy(report.Report.ID, id) // return &report, nil // } diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 6407724b5..6845d01c0 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -54,6 +54,14 @@ func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) return is, nil } +func (ir *ImageEngine) Mount(ctx context.Context, images []string, options entities.ImageMountOptions) ([]*entities.ImageMountReport, error) { + return nil, errors.New("mounting images is not supported for remote clients") +} + +func (ir *ImageEngine) Unmount(ctx context.Context, images []string, options entities.ImageUnmountOptions) ([]*entities.ImageUnmountReport, error) { + return nil, errors.New("unmounting images is not supported for remote clients") +} + func (ir *ImageEngine) History(ctx context.Context, nameOrID string, opts entities.ImageHistoryOptions) (*entities.ImageHistoryReport, error) { results, err := images.History(ir.ClientCxt, nameOrID) if err != nil { diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index 8e0151496..d3e43e44d 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -211,7 +211,8 @@ can_use_shortcut () break; } - if (argv[argc+1] != NULL && strcmp (argv[argc], "container") == 0 && + if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 || + strcmp (argv[argc], "image") == 0) && strcmp (argv[argc+1], "mount") == 0) { ret = false; -- cgit v1.2.3-54-g00ecf