Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Podman Image SCP rootful to rootless transfer #11958

Merged
merged 1 commit into from
Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
cdoern marked this conversation as resolved.
Show resolved Hide resolved
if scpOpts.User == "" {
return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage")
cdoern marked this conversation as resolved.
Show resolved Hide resolved
}
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")
cdoern marked this conversation as resolved.
Show resolved Hide resolved
}

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