Skip to content

Commit

Permalink
Podman Image SCP rootful to rootless transfer
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
cdoern authored and cdoern committed Nov 5, 2021
1 parent 85bad0c commit ffa5ed0
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 6 deletions.
31 changes: 26 additions & 5 deletions cmd/podman/images/scp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion docs/source/markdown/podman-image-scp.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]**

Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions pkg/domain/entities/engine_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pkg/domain/entities/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
65 changes: 65 additions & 0 deletions pkg/domain/infra/abi/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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}
Expand Down
5 changes: 5 additions & 0 deletions pkg/domain/infra/tunnel/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions test/e2e/image_scp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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")
Expand Down

0 comments on commit ffa5ed0

Please sign in to comment.