mirror of
https://github.com/containers/podman.git
synced 2025-06-27 21:50:18 +08:00
Podman Image SCP rootful to rootless transfer
Added functionality for users to transfer images from root storage to rootless storage without using sshd. This is done through rootful podman by running `sudo podman image scp root@localhost::image user@localhost:: the user is needed in order to find and use their uid/gid to exec a new process. added necessary tests, and functions for this implementation. Created new image function Transfer so that the underlying code is majorly removed from CLI Signed-off-by: cdoern <cdoern@redhat.com>
This commit is contained in:
@ -16,6 +16,7 @@ import (
|
||||
"github.com/containers/podman/v3/cmd/podman/system/connection"
|
||||
"github.com/containers/podman/v3/libpod/define"
|
||||
"github.com/containers/podman/v3/pkg/domain/entities"
|
||||
"github.com/containers/podman/v3/pkg/rootless"
|
||||
"github.com/docker/distribution/reference"
|
||||
scpD "github.com/dtylman/scp"
|
||||
"github.com/pkg/errors"
|
||||
@ -125,6 +126,11 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
|
||||
fmt.Println(rep)
|
||||
// TODO: Add podman remote support
|
||||
default: // else native load
|
||||
scpOpts.Save.Format = "oci-archive"
|
||||
_, err := os.Open(scpOpts.Save.Output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if scpOpts.Tag != "" {
|
||||
return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
|
||||
}
|
||||
@ -133,12 +139,20 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
|
||||
if abiErr != nil {
|
||||
errors.Wrapf(abiErr, "could not save image as specified")
|
||||
}
|
||||
rep, err := abiEng.Load(context.Background(), scpOpts.Load)
|
||||
if err != nil {
|
||||
return err
|
||||
if !rootless.IsRootless() && scpOpts.Rootless {
|
||||
err := abiEng.Transfer(context.Background(), scpOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
rep, err := abiEng.Load(context.Background(), scpOpts.Load)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ","))
|
||||
}
|
||||
fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ","))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -271,7 +285,14 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination
|
||||
scpOpts.SourceImageName = args[0]
|
||||
}
|
||||
case 2:
|
||||
if strings.Contains(args[0], "::") {
|
||||
if strings.Contains(args[0], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment
|
||||
scpOpts.Rootless = true
|
||||
scpOpts.User = strings.Split(args[1], "@")[0]
|
||||
scpOpts.SourceImageName = strings.Split(args[0], "::")[1]
|
||||
if strings.Split(args[0], "@")[0] != "root" {
|
||||
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo")
|
||||
}
|
||||
} else if strings.Contains(args[0], "::") {
|
||||
if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client
|
||||
cliConnections = append(cliConnections, args[0])
|
||||
scpOpts.ToRemote = true
|
||||
|
@ -8,7 +8,7 @@ podman-image-scp - Securely copy an image from one host to another
|
||||
|
||||
## DESCRIPTION
|
||||
**podman image scp** copies container images between hosts on a network. You can load to the remote host or from the remote host as well as in between two remote hosts.
|
||||
Note: `::` is used to specify the image name depending on if you are saving or loading.
|
||||
Note: `::` is used to specify the image name depending on if you are saving or loading. Images can also be transferred from rootful to rootless storage on the same machine without using sshd. This feature is not supported on the remote client.
|
||||
|
||||
**podman image scp [GLOBAL OPTIONS]**
|
||||
|
||||
@ -62,6 +62,22 @@ Storing signatures
|
||||
Loaded image(s): docker.io/library/alpine:latest
|
||||
```
|
||||
|
||||
```
|
||||
$ sudo podman image scp root@localhost::alpine username@localhost::
|
||||
Copying blob e2eb06d8af82 done
|
||||
Copying config 696d33ca15 done
|
||||
Writing manifest to image destination
|
||||
Storing signatures
|
||||
Run Directory Obtained: /run/user/1000/
|
||||
[Run Root: /var/tmp/containers-user-1000/containers Graph Root: /root/.local/share/containers/storage DB Path: /root/.local/share/containers/storage/libpod/bolt_state.db]
|
||||
Getting image source signatures
|
||||
Copying blob 5eb901baf107 skipped: already exists
|
||||
Copying config 696d33ca15 done
|
||||
Writing manifest to image destination
|
||||
Storing signatures
|
||||
Loaded image(s): docker.io/library/alpine:latest
|
||||
```
|
||||
|
||||
## SEE ALSO
|
||||
podman(1), podman-load(1), podman-save(1), podman-remote(1), podman-system-connection-add(1), containers.conf(5), containers-transports(5)
|
||||
|
||||
|
@ -27,6 +27,7 @@ type ImageEngine interface {
|
||||
ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error)
|
||||
Shutdown(ctx context.Context)
|
||||
Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error
|
||||
Transfer(ctx context.Context, scpOpts ImageScpOptions) 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
|
||||
|
@ -328,6 +328,10 @@ type ImageScpOptions struct {
|
||||
Save ImageSaveOptions
|
||||
// Load options used for the second half of the scp operation
|
||||
Load ImageLoadOptions
|
||||
// Rootless determines whether we are loading locally from root storage to rootless storage
|
||||
Rootless bool
|
||||
// User is used in conjunction with Rootless to determine which user to use to obtain the uid
|
||||
User string
|
||||
}
|
||||
|
||||
// ImageTreeOptions provides options for ImageEngine.Tree()
|
||||
|
@ -6,9 +6,12 @@ import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/common/libimage"
|
||||
"github.com/containers/common/pkg/config"
|
||||
@ -18,6 +21,7 @@ import (
|
||||
"github.com/containers/image/v5/signature"
|
||||
"github.com/containers/image/v5/transports"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/containers/podman/v3/libpod/define"
|
||||
"github.com/containers/podman/v3/pkg/domain/entities"
|
||||
"github.com/containers/podman/v3/pkg/domain/entities/reports"
|
||||
domainUtils "github.com/containers/podman/v3/pkg/domain/utils"
|
||||
@ -330,6 +334,67 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
|
||||
return pushError
|
||||
}
|
||||
|
||||
// Transfer moves images from root to rootless storage so the user specified in the scp call can access and use the image modified by root
|
||||
func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error {
|
||||
if scpOpts.User == "" {
|
||||
return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage")
|
||||
}
|
||||
var u *user.User
|
||||
scpOpts.User = strings.Split(scpOpts.User, ":")[0] // split in case provided with uid:gid
|
||||
_, err := strconv.Atoi(scpOpts.User)
|
||||
if err != nil {
|
||||
u, err = user.Lookup(scpOpts.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
u, err = user.LookupId(scpOpts.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
uid, err := strconv.Atoi(u.Uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid, err := strconv.Atoi(u.Gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chown(scpOpts.Save.Output, uid, gid) // chown the output because was created by root so we need to give th euser read access
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podman, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
machinectl, err := exec.LookPath("machinectl")
|
||||
if err != nil {
|
||||
logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available")
|
||||
cmd := exec.Command("su", "-l", u.Username, "--command", podman+" --log-level="+logrus.GetLevel().String()+" --cgroup-manager=cgroupfs load --input="+scpOpts.Save.Output) // load the new image to the rootless storage
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
logrus.Debug("Executing load command su")
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
cmd := exec.Command(machinectl, "shell", "-q", u.Username+"@.host", podman, "--log-level="+logrus.GetLevel().String(), "--cgroup-manager=cgroupfs", "load", "--input", scpOpts.Save.Output) // load the new image to the rootless storage
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
logrus.Debug("Executing load command machinectl")
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
|
||||
// Allow tagging manifest list instead of resolving instances from manifest
|
||||
lookupOptions := &libimage.LookupImageOptions{ManifestList: true}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/podman/v3/libpod/define"
|
||||
"github.com/containers/podman/v3/pkg/bindings/images"
|
||||
"github.com/containers/podman/v3/pkg/domain/entities"
|
||||
"github.com/containers/podman/v3/pkg/domain/entities/reports"
|
||||
@ -122,6 +123,10 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities.
|
||||
return &entities.ImagePullReport{Images: pulledImages}, nil
|
||||
}
|
||||
|
||||
func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error {
|
||||
return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage")
|
||||
}
|
||||
|
||||
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, opt entities.ImageTagOptions) error {
|
||||
options := new(images.TagOptions)
|
||||
for _, newTag := range tags {
|
||||
|
@ -22,12 +22,14 @@ var _ = Describe("podman image scp", func() {
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
|
||||
ConfPath.Value, ConfPath.IsSet = os.LookupEnv("CONTAINERS_CONF")
|
||||
conf, err := ioutil.TempFile("", "containersconf")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Setenv("CONTAINERS_CONF", conf.Name())
|
||||
|
||||
tempdir, err = CreateTempDirInTempDir()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
@ -38,6 +40,7 @@ var _ = Describe("podman image scp", func() {
|
||||
|
||||
AfterEach(func() {
|
||||
podmanTest.Cleanup()
|
||||
|
||||
os.Remove(os.Getenv("CONTAINERS_CONF"))
|
||||
if ConfPath.IsSet {
|
||||
os.Setenv("CONTAINERS_CONF", ConfPath.Value)
|
||||
@ -58,6 +61,25 @@ var _ = Describe("podman image scp", func() {
|
||||
Expect(scp).To(Exit(0))
|
||||
})
|
||||
|
||||
It("podman image scp root to rootless transfer", func() {
|
||||
SkipIfNotRootless("this is a rootless only test, transfering from root to rootless using PodmanAsUser")
|
||||
if IsRemote() {
|
||||
Skip("this test is only for non-remote")
|
||||
}
|
||||
env := os.Environ()
|
||||
img := podmanTest.PodmanAsUser([]string{"image", "pull", ALPINE}, 0, 0, "", env) // pull image to root
|
||||
img.WaitWithDefaultTimeout()
|
||||
Expect(img).To(Exit(0))
|
||||
scp := podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE, "1000:1000@localhost::"}, 0, 0, "", env) //transfer from root to rootless (us)
|
||||
scp.WaitWithDefaultTimeout()
|
||||
Expect(scp).To(Exit(0))
|
||||
|
||||
list := podmanTest.Podman([]string{"image", "list"}) // our image should now contain alpine loaded in from root
|
||||
list.WaitWithDefaultTimeout()
|
||||
Expect(list).To(Exit(0))
|
||||
Expect(list.LineInOutputStartsWith("quay.io/libpod/alpine")).To(BeTrue())
|
||||
})
|
||||
|
||||
It("podman image scp bogus image", func() {
|
||||
if IsRemote() {
|
||||
Skip("this test is only for non-remote")
|
||||
|
Reference in New Issue
Block a user