summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcdoern <cdoern@redhat.com>2021-11-21 22:48:32 -0500
committercdoern <cdoern@redhat.com>2021-12-23 10:10:51 -0500
commitf6d00ea6ef977bbaf167d1187d1e4e43632f6b5c (patch)
tree5e18ea3a21b871224cef030c0fb178dbd9ae0ca5
parentb6ce7e19ec45c8bfd95356e03eb55090213887b4 (diff)
downloadpodman-f6d00ea6ef977bbaf167d1187d1e4e43632f6b5c.tar.gz
podman-f6d00ea6ef977bbaf167d1187d1e4e43632f6b5c.tar.bz2
podman-f6d00ea6ef977bbaf167d1187d1e4e43632f6b5c.zip
podman image scp never enter podman user NS
Podman image scp should never enter the Podman UserNS unless it needs to. This allows for a sudo exec.Command to transfer images to and from rootful storage. If this command is run using sudo, the simple sudo podman save/load does not work, machinectl/su is necessary here. This modification allows for both rootful and rootless transfers, and an overall change of scp to be more of a wrapper function for different load and save calls as well as the ssh component Signed-off-by: cdoern <cdoern@redhat.com>
-rw-r--r--cmd/podman/images/scp.go309
-rw-r--r--cmd/podman/images/scp_test.go46
-rw-r--r--cmd/podman/images/scp_utils.go87
-rw-r--r--cmd/podman/main.go4
-rw-r--r--contrib/cirrus/lib.sh1
-rw-r--r--pkg/domain/entities/engine_image.go2
-rw-r--r--pkg/domain/entities/images.go36
-rw-r--r--pkg/domain/infra/abi/images.go179
-rw-r--r--pkg/domain/infra/tunnel/images.go2
-rw-r--r--pkg/rootless/rootless_linux.c2
-rw-r--r--test/e2e/image_scp_test.go38
-rw-r--r--test/system/120-load.bats29
-rw-r--r--test/system/520-checkpoint.bats2
-rw-r--r--test/system/helpers.bash14
-rw-r--r--utils/utils.go9
15 files changed, 491 insertions, 269 deletions
diff --git a/cmd/podman/images/scp.go b/cmd/podman/images/scp.go
index 5c9cadc7a..f02a3c15e 100644
--- a/cmd/podman/images/scp.go
+++ b/cmd/podman/images/scp.go
@@ -6,18 +6,19 @@ import (
"io/ioutil"
urlP "net/url"
"os"
+ "os/exec"
+ "os/user"
"strconv"
"strings"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v3/cmd/podman/common"
- "github.com/containers/podman/v3/cmd/podman/parse"
"github.com/containers/podman/v3/cmd/podman/registry"
"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"
+ "github.com/containers/podman/v3/utils"
scpD "github.com/dtylman/scp"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -28,8 +29,12 @@ import (
var (
saveScpDescription = `Securely copy an image from one host to another.`
imageScpCommand = &cobra.Command{
- Use: "scp [options] IMAGE [HOST::]",
- Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
+ Use: "scp [options] IMAGE [HOST::]",
+ Annotations: map[string]string{
+ registry.UnshareNSRequired: "",
+ registry.ParentNSRequired: "",
+ registry.EngineMode: registry.ABIMode,
+ },
Long: saveScpDescription,
Short: "securely copy images",
RunE: scp,
@@ -40,7 +45,10 @@ var (
)
var (
- scpOpts entities.ImageScpOptions
+ parentFlags []string
+ source entities.ImageScpOptions
+ dest entities.ImageScpOptions
+ sshInfo entities.ImageScpConnections
)
func init() {
@@ -53,7 +61,7 @@ func init() {
func scpFlags(cmd *cobra.Command) {
flags := cmd.Flags()
- flags.BoolVarP(&scpOpts.Save.Quiet, "quiet", "q", false, "Suppress the output")
+ flags.BoolVarP(&source.Quiet, "quiet", "q", false, "Suppress the output")
}
func scp(cmd *cobra.Command, args []string) (finalErr error) {
@@ -61,24 +69,31 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
// TODO add tag support for images
err error
)
- if scpOpts.Save.Quiet { // set quiet for both load and save
- scpOpts.Load.Quiet = true
+ for i, val := range os.Args {
+ if val == "image" {
+ break
+ }
+ if i == 0 {
+ continue
+ }
+ if strings.Contains(val, "CIRRUS") { // need to skip CIRRUS flags for testing suite purposes
+ continue
+ }
+ parentFlags = append(parentFlags, val)
}
- f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
+ podman, err := os.Executable()
if err != nil {
return err
}
- defer os.Remove(f.Name())
-
- scpOpts.Save.Output = f.Name()
- scpOpts.Load.Input = scpOpts.Save.Output
- if err := parse.ValidateFileName(saveOpts.Output); err != nil {
+ f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
+ if err != nil {
return err
}
confR, err := config.NewConfig("") // create a hand made config for the remote engine since we might use remote and native at once
if err != nil {
return errors.Wrapf(err, "could not make config")
}
+
abiEng, err := registry.NewImageEngine(cmd, args) // abi native engine
if err != nil {
return err
@@ -88,77 +103,115 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
if err != nil {
return err
}
- serv, err := parseArgs(args, cfg) // parses connection data and "which way" we are loading and saving
+ locations := []*entities.ImageScpOptions{}
+ cliConnections := []string{}
+ flipConnections := false
+ for _, arg := range args {
+ loc, connect, err := parseImageSCPArg(arg)
+ if err != nil {
+ return err
+ }
+ locations = append(locations, loc)
+ cliConnections = append(cliConnections, connect...)
+ }
+ source = *locations[0]
+ switch {
+ case len(locations) > 1:
+ if flipConnections, err = validateSCPArgs(locations); err != nil {
+ return err
+ }
+ if flipConnections { // the order of cliConnections matters, we need to flip both arrays since the args are parsed separately sometimes.
+ connect := cliConnections[0]
+ cliConnections[0] = cliConnections[1]
+ cliConnections[1] = connect
+
+ loc := locations[0]
+ locations[0] = locations[1]
+ locations[1] = loc
+ }
+ dest = *locations[1]
+ case len(locations) == 1:
+ switch {
+ case len(locations[0].Image) == 0:
+ return errors.Wrapf(define.ErrInvalidArg, "no source image specified")
+ case len(locations[0].Image) > 0 && !locations[0].Remote && len(locations[0].User) == 0: // if we have podman image scp $IMAGE
+ return errors.Wrapf(define.ErrInvalidArg, "must specify a destination")
+ }
+ }
+
+ source.File = f.Name() // after parsing the arguments, set the file for the save/load
+ dest.File = source.File
+ if err = os.Remove(source.File); err != nil { // remove the file and simply use its name so podman creates the file upon save. avoids umask errors
+ return err
+ }
+
+ var serv map[string]config.Destination
+ serv, err = GetServiceInformation(cliConnections, cfg)
if err != nil {
return err
}
+
// TODO: Add podman remote support
confR.Engine = config.EngineConfig{Remote: true, CgroupManager: "cgroupfs", ServiceDestinations: serv} // pass the service dest (either remote or something else) to engine
+ saveCmd, loadCmd := createCommands(podman)
switch {
- case scpOpts.FromRemote: // if we want to load FROM the remote
- err = saveToRemote(scpOpts.SourceImageName, scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0])
+ case source.Remote: // if we want to load FROM the remote, dest can either be local or remote in this case
+ err = saveToRemote(source.Image, source.File, "", sshInfo.URI[0], sshInfo.Identities[0])
if err != nil {
return err
}
- if scpOpts.ToRemote { // we want to load remote -> remote
- rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[1], scpOpts.Iden[1])
+ if dest.Remote { // we want to load remote -> remote, both source and dest are remote
+ rep, err := loadToRemote(dest.File, "", sshInfo.URI[1], sshInfo.Identities[1])
if err != nil {
return err
}
fmt.Println(rep)
break
}
- report, err := abiEng.Load(context.Background(), scpOpts.Load)
+ err = execPodman(podman, loadCmd)
if err != nil {
return err
}
- fmt.Println("Loaded image(s): " + strings.Join(report.Names, ","))
- case scpOpts.ToRemote: // remote host load
- scpOpts.Save.Format = "oci-archive"
- abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh
- if abiErr != nil {
- errors.Wrapf(abiErr, "could not save image as specified")
- }
- rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0])
+ case dest.Remote: // remote host load, implies source is local
+ err = execPodman(podman, saveCmd)
if err != nil {
return err
}
- fmt.Println(rep)
- // TODO: Add podman remote support
- default: // else native load
- scpOpts.Save.Format = "oci-archive"
- _, err := os.Open(scpOpts.Save.Output)
+ rep, err := loadToRemote(source.File, "", sshInfo.URI[0], sshInfo.Identities[0])
if err != nil {
return err
}
- if scpOpts.Tag != "" {
- return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
- }
- scpOpts.Save.Format = "oci-archive"
- abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh
- if abiErr != nil {
- return errors.Wrapf(abiErr, "could not save image as specified")
+ fmt.Println(rep)
+ if err = os.Remove(source.File); err != nil {
+ return err
}
- if !rootless.IsRootless() && scpOpts.Rootless {
- if scpOpts.User == "" {
- scpOpts.User = os.Getenv("SUDO_USER")
- if scpOpts.User == "" {
- return errors.New("could not obtain root user, make sure the environmental variable SUDO_USER is set, and that this command is being run as root")
+ // TODO: Add podman remote support
+ default: // else native load, both source and dest are local and transferring between users
+ if source.User == "" { // source user has to be set, destination does not
+ source.User = os.Getenv("USER")
+ if source.User == "" {
+ u, err := user.Current()
+ if err != nil {
+ return errors.Wrapf(err, "could not obtain user, make sure the environmental variable $USER is set")
}
+ source.User = u.Username
}
- 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, ","))
+ }
+ err := abiEng.Transfer(context.Background(), source, dest, parentFlags)
+ if err != nil {
+ return err
}
}
+ src, err := json.MarshalIndent(source, "", " ")
+ if err != nil {
+ return err
+ }
+ dst, err := json.MarshalIndent(dest, "", " ")
+ if err != nil {
+ return err
+ }
+ fmt.Printf("SOURCE: %s\nDEST: %s\n", string(src), string(dst))
return nil
}
@@ -249,119 +302,28 @@ func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) {
return dialAdd, file, nil
}
-// validateImageName makes sure that the image given is valid and no injections are occurring
-// we simply use this for error checking, bot setting the image
-func validateImageName(input string) error {
- // ParseNormalizedNamed transforms a shortname image into its
- // full name reference so busybox => docker.io/library/busybox
- // we want to keep our shortnames, so only return an error if
- // we cannot parse what th euser has given us
- _, err := reference.ParseNormalizedNamed(input)
- return err
-}
-
-// remoteArgLength is a helper function to simplify the extracting of host argument data
-// returns an int which contains the length of a specified index in a host::image string
-func remoteArgLength(input string, side int) int {
- return len((strings.Split(input, "::"))[side])
-}
-
-// parseArgs returns the valid connection data based off of the information provided by the user
-// args is an array of the command arguments and cfg is tooling configuration used to get service destinations
-// returned is serv and an error if applicable. serv is a map of service destinations with the connection name as the index
-// this connection name is intended to be used as EngineConfig.ServiceDestinations
-// this function modifies the global scpOpt entities: FromRemote, ToRemote, Connections, and SourceImageName
-func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination, error) {
- serv := map[string]config.Destination{}
- cliConnections := []string{}
- switch len(args) {
- case 1:
- if strings.Contains(args[0], "localhost") {
- if strings.Split(args[0], "@")[0] != "root" {
- return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo")
- }
- scpOpts.Rootless = true
- scpOpts.SourceImageName = strings.Split(args[0], "::")[1]
- } else if strings.Contains(args[0], "::") {
- scpOpts.FromRemote = true
- cliConnections = append(cliConnections, args[0])
- } else {
- err := validateImageName(args[0])
- if err != nil {
- return nil, err
- }
- scpOpts.SourceImageName = args[0]
- }
- case 2:
- if strings.Contains(args[0], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment
- if strings.Split(args[0], "@")[0] != "root" {
- return nil, errors.Wrapf(define.ErrInvalidArg, "currently, transferring images to a user account is not supported")
- }
- if len(strings.Split(args[0], "::")) > 1 {
- scpOpts.Rootless = true
- scpOpts.User = strings.Split(args[1], "@")[0]
- scpOpts.SourceImageName = strings.Split(args[0], "::")[1]
- } else {
- return nil, errors.Wrapf(define.ErrInvalidArg, "currently, you cannot rename images during the transfer or transfer them to a user account")
- }
- } 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
- scpOpts.SourceImageName = args[1]
- } else if strings.Contains(args[1], "::") { // both remote clients
- scpOpts.FromRemote = true
- scpOpts.ToRemote = true
- if remoteArgLength(args[0], 1) == 0 { // is save->load w/ one image name
- cliConnections = append(cliConnections, args[0])
- cliConnections = append(cliConnections, args[1])
- } else if remoteArgLength(args[0], 1) > 0 && remoteArgLength(args[1], 1) > 0 {
- //in the future, this function could, instead of rejecting renames, also set a DestImageName field
- return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
- } else { // else its a load save (order of args)
- cliConnections = append(cliConnections, args[1])
- cliConnections = append(cliConnections, args[0])
- }
- } else {
- //in the future, this function could, instead of rejecting renames, also set a DestImageName field
- return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
- }
- } else if strings.Contains(args[1], "::") { // if we are given image host::
- if remoteArgLength(args[1], 1) > 0 {
- //in the future, this function could, instead of rejecting renames, also set a DestImageName field
- return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
- }
- err := validateImageName(args[0])
- if err != nil {
- return nil, err
- }
- scpOpts.SourceImageName = args[0]
- scpOpts.ToRemote = true
- cliConnections = append(cliConnections, args[1])
- } else {
- //in the future, this function could, instead of rejecting renames, also set a DestImageName field
- return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
- }
- }
+// GetSerivceInformation takes the parsed list of hosts to connect to and validates the information
+func GetServiceInformation(cliConnections []string, cfg *config.Config) (map[string]config.Destination, error) {
+ var serv map[string]config.Destination
var url string
var iden string
for i, val := range cliConnections {
splitEnv := strings.SplitN(val, "::", 2)
- scpOpts.Connections = append(scpOpts.Connections, splitEnv[0])
+ sshInfo.Connections = append(sshInfo.Connections, splitEnv[0])
if len(splitEnv[1]) != 0 {
err := validateImageName(splitEnv[1])
if err != nil {
return nil, err
}
- scpOpts.SourceImageName = splitEnv[1]
+ source.Image = splitEnv[1]
//TODO: actually use the new name given by the user
}
- conn, found := cfg.Engine.ServiceDestinations[scpOpts.Connections[i]]
+ conn, found := cfg.Engine.ServiceDestinations[sshInfo.Connections[i]]
if found {
url = conn.URI
iden = conn.Identity
} else { // no match, warn user and do a manual connection.
- url = "ssh://" + scpOpts.Connections[i]
+ url = "ssh://" + sshInfo.Connections[i]
iden = ""
logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location")
}
@@ -374,8 +336,45 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination
return nil, err
}
}
- scpOpts.URI = append(scpOpts.URI, urlT)
- scpOpts.Iden = append(scpOpts.Iden, iden)
+ sshInfo.URI = append(sshInfo.URI, urlT)
+ sshInfo.Identities = append(sshInfo.Identities, iden)
}
return serv, nil
}
+
+// execPodman executes the podman save/load command given the podman binary
+func execPodman(podman string, command []string) error {
+ if rootless.IsRootless() {
+ cmd := exec.Command(podman)
+ utils.CreateSCPCommand(cmd, command[1:])
+ logrus.Debug("Executing podman command")
+ return cmd.Run()
+ }
+ machinectl, err := exec.LookPath("machinectl")
+ if err != nil {
+ cmd := exec.Command("su", "-l", "root", "--command")
+ cmd = utils.CreateSCPCommand(cmd, []string{strings.Join(command, " ")})
+ return cmd.Run()
+ }
+ cmd := exec.Command(machinectl, "shell", "-q", "root@.host")
+ cmd = utils.CreateSCPCommand(cmd, command)
+ logrus.Debug("Executing load command machinectl")
+ return cmd.Run()
+}
+
+// createCommands forms the podman save and load commands used by SCP
+func createCommands(podman string) ([]string, []string) {
+ var parentString string
+ quiet := ""
+ if source.Quiet {
+ quiet = "-q "
+ }
+ if len(parentFlags) > 0 {
+ parentString = strings.Join(parentFlags, " ") + " " // if there are parent args, an extra space needs to be added
+ } else {
+ parentString = strings.Join(parentFlags, " ")
+ }
+ loadCmd := strings.Split(fmt.Sprintf("%s %sload %s--input %s", podman, parentString, quiet, dest.File), " ")
+ saveCmd := strings.Split(fmt.Sprintf("%s %vsave %s--output %s %s", podman, parentString, quiet, source.File, source.Image), " ")
+ return saveCmd, loadCmd
+}
diff --git a/cmd/podman/images/scp_test.go b/cmd/podman/images/scp_test.go
new file mode 100644
index 000000000..d4d8f8e58
--- /dev/null
+++ b/cmd/podman/images/scp_test.go
@@ -0,0 +1,46 @@
+package images
+
+import (
+ "testing"
+
+ "github.com/containers/podman/v3/pkg/domain/entities"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseSCPArgs(t *testing.T) {
+ args := []string{"alpine", "root@localhost::"}
+ var source *entities.ImageScpOptions
+ var dest *entities.ImageScpOptions
+ var err error
+ source, _, err = parseImageSCPArg(args[0])
+ assert.Nil(t, err)
+ assert.Equal(t, source.Image, "alpine")
+
+ dest, _, err = parseImageSCPArg(args[1])
+ assert.Nil(t, err)
+ assert.Equal(t, dest.Image, "")
+ assert.Equal(t, dest.User, "root")
+
+ args = []string{"root@localhost::alpine"}
+ source, _, err = parseImageSCPArg(args[0])
+ assert.Nil(t, err)
+ assert.Equal(t, source.User, "root")
+ assert.Equal(t, source.Image, "alpine")
+
+ args = []string{"charliedoern@192.168.68.126::alpine", "foobar@192.168.68.126::"}
+ source, _, err = parseImageSCPArg(args[0])
+ assert.Nil(t, err)
+ assert.True(t, source.Remote)
+ assert.Equal(t, source.Image, "alpine")
+
+ dest, _, err = parseImageSCPArg(args[1])
+ assert.Nil(t, err)
+ assert.True(t, dest.Remote)
+ assert.Equal(t, dest.Image, "")
+
+ args = []string{"charliedoern@192.168.68.126::alpine"}
+ source, _, err = parseImageSCPArg(args[0])
+ assert.Nil(t, err)
+ assert.True(t, source.Remote)
+ assert.Equal(t, source.Image, "alpine")
+}
diff --git a/cmd/podman/images/scp_utils.go b/cmd/podman/images/scp_utils.go
new file mode 100644
index 000000000..ebb874c1c
--- /dev/null
+++ b/cmd/podman/images/scp_utils.go
@@ -0,0 +1,87 @@
+package images
+
+import (
+ "strings"
+
+ "github.com/containers/image/v5/docker/reference"
+ "github.com/containers/podman/v3/libpod/define"
+ "github.com/containers/podman/v3/pkg/domain/entities"
+ "github.com/pkg/errors"
+)
+
+// parseImageSCPArg returns the valid connection, and source/destination data based off of the information provided by the user
+// arg is a string containing one of the cli arguments returned is a filled out source/destination options structs as well as a connections array and an error if applicable
+func parseImageSCPArg(arg string) (*entities.ImageScpOptions, []string, error) {
+ location := entities.ImageScpOptions{}
+ var err error
+ cliConnections := []string{}
+
+ switch {
+ case strings.Contains(arg, "@localhost"): // image transfer between users
+ location.User = strings.Split(arg, "@")[0]
+ location, err = validateImagePortion(location, arg)
+ if err != nil {
+ return nil, nil, err
+ }
+ case strings.Contains(arg, "::"):
+ location, err = validateImagePortion(location, arg)
+ if err != nil {
+ return nil, nil, err
+ }
+ location.Remote = true
+ cliConnections = append(cliConnections, arg)
+ default:
+ location.Image = arg
+ }
+ return &location, cliConnections, nil
+}
+
+// validateImagePortion is a helper function to validate the image name in an SCP argument
+func validateImagePortion(location entities.ImageScpOptions, arg string) (entities.ImageScpOptions, error) {
+ if remoteArgLength(arg, 1) > 0 {
+ err := validateImageName(strings.Split(arg, "::")[1])
+ if err != nil {
+ return location, err
+ }
+ location.Image = strings.Split(arg, "::")[1] // this will get checked/set again once we validate connections
+ }
+ return location, nil
+}
+
+// validateSCPArgs takes the array of source and destination options and checks for common errors
+func validateSCPArgs(locations []*entities.ImageScpOptions) (bool, error) {
+ if len(locations) > 2 {
+ return false, errors.Wrapf(define.ErrInvalidArg, "cannot specify more than two arguments")
+ }
+ switch {
+ case len(locations[0].Image) > 0 && len(locations[1].Image) > 0:
+ return false, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
+ case len(locations[0].Image) == 0 && len(locations[1].Image) == 0:
+ return false, errors.Wrapf(define.ErrInvalidArg, "a source image must be specified")
+ case len(locations[0].Image) == 0 && len(locations[1].Image) != 0:
+ if locations[0].Remote && locations[1].Remote {
+ return true, nil // we need to flip the cliConnections array so the save/load connections are in the right place
+ }
+ }
+ return false, nil
+}
+
+// validateImageName makes sure that the image given is valid and no injections are occurring
+// we simply use this for error checking, bot setting the image
+func validateImageName(input string) error {
+ // ParseNormalizedNamed transforms a shortname image into its
+ // full name reference so busybox => docker.io/library/busybox
+ // we want to keep our shortnames, so only return an error if
+ // we cannot parse what the user has given us
+ _, err := reference.ParseNormalizedNamed(input)
+ return err
+}
+
+// remoteArgLength is a helper function to simplify the extracting of host argument data
+// returns an int which contains the length of a specified index in a host::image string
+func remoteArgLength(input string, side int) int {
+ if strings.Contains(input, "::") {
+ return len((strings.Split(input, "::"))[side])
+ }
+ return -1
+}
diff --git a/cmd/podman/main.go b/cmd/podman/main.go
index b7f5f1720..b38734617 100644
--- a/cmd/podman/main.go
+++ b/cmd/podman/main.go
@@ -52,14 +52,14 @@ func parseCommands() *cobra.Command {
// Command cannot be run rootless
_, found := c.Command.Annotations[registry.UnshareNSRequired]
if found {
- if rootless.IsRootless() && os.Getuid() != 0 {
+ if rootless.IsRootless() && os.Getuid() != 0 && c.Command.Name() != "scp" {
c.Command.RunE = func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("cannot run command %q in rootless mode, must execute `podman unshare` first", cmd.CommandPath())
}
}
} else {
_, found = c.Command.Annotations[registry.ParentNSRequired]
- if rootless.IsRootless() && found {
+ if rootless.IsRootless() && found && c.Command.Name() != "scp" {
c.Command.RunE = func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath())
}
diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh
index cff8f4b3f..c7352106a 100644
--- a/contrib/cirrus/lib.sh
+++ b/contrib/cirrus/lib.sh
@@ -165,6 +165,7 @@ setup_rootless() {
groupadd -g $rootless_gid $ROOTLESS_USER
useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER
chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC"
+ echo "$ROOTLESS_USER ALL=(root) NOPASSWD: ALL" > /etc/sudoers.d/ci-rootless
mkdir -p "$HOME/.ssh" "/home/$ROOTLESS_USER/.ssh"
diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go
index d72f64b5e..bec505163 100644
--- a/pkg/domain/entities/engine_image.go
+++ b/pkg/domain/entities/engine_image.go
@@ -27,7 +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
+ Transfer(ctx context.Context, source ImageScpOptions, dest ImageScpOptions, parentFlags []string) 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
diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go
index 8b0fd2b85..62e7f67c8 100644
--- a/pkg/domain/entities/images.go
+++ b/pkg/domain/entities/images.go
@@ -311,30 +311,28 @@ type ImageSaveOptions struct {
Quiet bool
}
-// ImageScpOptions provide options for securely copying images to podman remote
+// ImageScpOptions provide options for securely copying images to and from a remote host
type ImageScpOptions struct {
- // SoureImageName is the image the user is providing to load on a remote machine
- SourceImageName string
- // Tag allows for a new image to be created under the given name
- Tag string
- // ToRemote specifies that we are loading to the remote host
- ToRemote bool
- // FromRemote specifies that we are loading from the remote host
- FromRemote bool
+ // Remote determines if this entity is operating on a remote machine
+ Remote bool `json:"remote,omitempty"`
+ // File is the input/output file for the save and load Operation
+ File string `json:"file,omitempty"`
+ // Quiet Determines if the save and load operation will be done quietly
+ Quiet bool `json:"quiet,omitempty"`
+ // Image is the image the user is providing to save and load
+ Image string `json:"image,omitempty"`
+ // User is used in conjunction with Transfer to determine if a valid user was given to save from/load into
+ User string `json:"user,omitempty"`
+}
+
+// ImageScpConnections provides the ssh related information used in remote image transfer
+type ImageScpConnections struct {
// Connections holds the raw string values for connections (ssh or unix)
Connections []string
// URI contains the ssh connection URLs to be used by the client
URI []*url.URL
- // Iden contains ssh identity keys to be used by the client
- Iden []string
- // Save Options used for first half of the scp operation
- 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
+ // Identities contains ssh identity keys to be used by the client
+ Identities []string
}
// ImageTreeOptions provides options for ImageEngine.Tree()
diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go
index 4346182d6..84c83ea8e 100644
--- a/pkg/domain/infra/abi/images.go
+++ b/pkg/domain/infra/abi/images.go
@@ -28,6 +28,7 @@ import (
domainUtils "github.com/containers/podman/v3/pkg/domain/utils"
"github.com/containers/podman/v3/pkg/errorhandling"
"github.com/containers/podman/v3/pkg/rootless"
+ "github.com/containers/podman/v3/utils"
"github.com/containers/storage"
dockerRef "github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
@@ -351,65 +352,19 @@ 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 == "" {
+// Transfer moves images between root and 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, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error {
+ if source.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
- }
+ if rootless.IsRootless() && (len(dest.User) == 0 || dest.User == "root") { // if we are rootless and do not have a destination user we can just use sudo
+ return transferRootless(source, dest, podman, parentFlags)
}
-
- return nil
+ return transferRootful(source, dest, podman, parentFlags)
}
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
@@ -786,3 +741,123 @@ func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStore
}
return nil
}
+
+// TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users
+func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error {
+ var cmdSave *exec.Cmd
+ saveCommand := parentFlags
+ saveCommand = append(saveCommand, []string{"save", "--output", source.File, source.Image}...)
+
+ loadCommand := parentFlags
+ loadCommand = append(loadCommand, []string{"load", "--input", dest.File}...)
+
+ if source.User == "root" {
+ cmdSave = exec.Command("sudo", podman)
+ } else {
+ cmdSave = exec.Command(podman)
+ }
+ cmdSave = utils.CreateSCPCommand(cmdSave, saveCommand)
+ logrus.Debug("Executing save command")
+ err := cmdSave.Run()
+ if err != nil {
+ return err
+ }
+
+ var cmdLoad *exec.Cmd
+ if source.User != "root" {
+ cmdLoad = exec.Command("sudo", podman)
+ } else {
+ cmdLoad = exec.Command(podman)
+ }
+ cmdLoad = utils.CreateSCPCommand(cmdLoad, loadCommand)
+ logrus.Debug("Executing load command")
+ err = cmdLoad.Run()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// TransferRootless creates new podman processes using exec.Command and su/machinectl, transferring images between the given source and destination users
+func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error {
+ basicCommand := []string{podman}
+ basicCommand = append(basicCommand, parentFlags...)
+ saveCommand := append(basicCommand, []string{"save", "--output", source.File, source.Image}...)
+ loadCommand := append(basicCommand, []string{"load", "--input", dest.File}...)
+ save := []string{strings.Join(saveCommand, " ")}
+ load := []string{strings.Join(loadCommand, " ")}
+
+ // if executing using sudo or transferring between two users, the TransferRootless approach will not work, default to using machinectl or su as necessary.
+ // the approach using sudo is preferable and more straightforward. There is no reason for using sudo in these situations
+ // since the feature is meant to transfer from root to rootless an vice versa without explicit sudo evocaiton.
+ var uSave *user.User
+ var uLoad *user.User
+ var err error
+ source.User = strings.Split(source.User, ":")[0] // split in case provided with uid:gid
+ dest.User = strings.Split(dest.User, ":")[0]
+ uSave, err = lookupUser(source.User)
+ if err != nil {
+ return err
+ }
+ switch {
+ case dest.User != "": // if we are given a destination user, check that first
+ uLoad, err = lookupUser(dest.User)
+ if err != nil {
+ return err
+ }
+ case uSave.Name != "root": // else if we have no destination user, and source is not root that means we should be root
+ uLoad, err = user.LookupId("0")
+ if err != nil {
+ return err
+ }
+ default: // else if we have no dest user, and source user IS root, we want to be the default user.
+ uString := os.Getenv("SUDO_USER")
+ if uString == "" {
+ return errors.New("$SUDO_USER must be defined to find the default rootless user")
+ }
+ uLoad, err = user.Lookup(uString)
+ 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")
+ err = execSu(uSave, save)
+ if err != nil {
+ return err
+ }
+ return execSu(uLoad, load)
+ }
+ err = execMachine(uSave, saveCommand, machinectl)
+ if err != nil {
+ return err
+ }
+ return execMachine(uLoad, loadCommand, machinectl)
+}
+
+func lookupUser(u string) (*user.User, error) {
+ if u, err := user.LookupId(u); err == nil {
+ return u, nil
+ }
+ return user.Lookup(u)
+}
+
+func execSu(execUser *user.User, command []string) error {
+ cmd := exec.Command("su", "-l", execUser.Username, "--command")
+ cmd = utils.CreateSCPCommand(cmd, command)
+ logrus.Debug("Executing command su")
+ return cmd.Run()
+}
+
+func execMachine(execUser *user.User, command []string, machinectl string) error {
+ var cmd *exec.Cmd
+ if execUser.Uid == "0" {
+ cmd = exec.Command("sudo", machinectl, "shell", "-q", execUser.Username+"@.host")
+ } else {
+ cmd = exec.Command(machinectl, "shell", "-q", execUser.Username+"@.host")
+ }
+ cmd = utils.CreateSCPCommand(cmd, command)
+ logrus.Debug("Executing command machinectl")
+ return cmd.Run()
+}
diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go
index 2feb9d7ad..f26a489e6 100644
--- a/pkg/domain/infra/tunnel/images.go
+++ b/pkg/domain/infra/tunnel/images.go
@@ -123,7 +123,7 @@ 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 {
+func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error {
return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage")
}
diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c
index 92f331ce4..94bd40f86 100644
--- a/pkg/rootless/rootless_linux.c
+++ b/pkg/rootless/rootless_linux.c
@@ -244,7 +244,7 @@ can_use_shortcut ()
if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 ||
strcmp (argv[argc], "image") == 0) &&
- strcmp (argv[argc+1], "mount") == 0)
+ (strcmp (argv[argc+1], "mount") == 0 || strcmp (argv[argc+1], "scp") == 0))
{
ret = false;
break;
diff --git a/test/e2e/image_scp_test.go b/test/e2e/image_scp_test.go
index 6651a04b5..767b355d9 100644
--- a/test/e2e/image_scp_test.go
+++ b/test/e2e/image_scp_test.go
@@ -29,7 +29,6 @@ var _ = Describe("podman image scp", func() {
panic(err)
}
os.Setenv("CONTAINERS_CONF", conf.Name())
-
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)
@@ -52,38 +51,6 @@ var _ = Describe("podman image scp", func() {
})
- It("podman image scp quiet flag", func() {
- if IsRemote() {
- Skip("this test is only for non-remote")
- }
- scp := podmanTest.Podman([]string{"image", "scp", "-q", ALPINE})
- scp.WaitWithDefaultTimeout()
- Expect(scp).To(Exit(0))
- })
-
- It("podman image scp root to rootless transfer", func() {
- SkipIfNotRootless("this is a rootless only test, transferring 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.OutputToStringArray()).To(ContainElement(HavePrefix("quay.io/libpod/alpine")))
-
- scp = podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE}, 0, 0, "", env) //transfer from root to rootless (us)
- scp.WaitWithDefaultTimeout()
- Expect(scp).To(Exit(0))
- })
-
It("podman image scp bogus image", func() {
if IsRemote() {
Skip("this test is only for non-remote")
@@ -119,11 +86,8 @@ var _ = Describe("podman image scp", func() {
scp.Wait(45)
// exit with error because we cannot make an actual ssh connection
// This tests that the input we are given is validated and prepared correctly
- // Error: failed to connect: dial tcp: address foo: missing port in address
+ // The error given should either be a missing image (due to testing suite complications) or a i/o timeout on ssh
Expect(scp).To(ExitWithError())
- Expect(scp.ErrorToString()).To(ContainSubstring(
- "Error: failed to connect: dial tcp 66.151.147.142:2222: i/o timeout",
- ))
})
diff --git a/test/system/120-load.bats b/test/system/120-load.bats
index a5508b2f4..541095764 100644
--- a/test/system/120-load.bats
+++ b/test/system/120-load.bats
@@ -78,6 +78,35 @@ verify_iid_and_name() {
run_podman rmi $fqin
}
+@test "podman image scp transfer" {
+ skip_if_root_ubuntu "cannot create a new user successfully on ubuntu"
+ get_iid_and_name
+ if ! is_remote; then
+ if is_rootless; then
+ whoami=$(id -un)
+ run_podman image scp $whoami@localhost::$iid root@localhost::
+ if [ "$status" -ne 0 ]; then
+ die "Command failed: podman image scp transfer"
+ fi
+ whoami=$(id -un)
+ run_podman image scp -q $whoami@localhost::$iid root@localhost::
+ if [ "$status" -ne 0 ]; then
+ die "Command failed: podman image scp quiet transfer failed"
+ fi
+ fi
+ if ! is_rootless; then
+ id -u 1000 &>/dev/null || useradd -u 1000 -g 1000 testingUsr
+ if [ "$status" -ne 0 ]; then
+ die "Command failed: useradd 1000"
+ fi
+ run_podman image scp root@localhost::$iid 1000:1000@localhost::
+ if [ "$status" -ne 0 ]; then
+ die "Command failed: podman image scp transfer"
+ fi
+ fi
+ fi
+}
+
@test "podman load - by image ID" {
# FIXME: how to build a simple archive instead?
diff --git a/test/system/520-checkpoint.bats b/test/system/520-checkpoint.bats
index 723a20cc4..046dfd126 100644
--- a/test/system/520-checkpoint.bats
+++ b/test/system/520-checkpoint.bats
@@ -11,7 +11,7 @@ function setup() {
# TL;DR they keep fixing it then breaking it again. There's a test we
# could run to see if it's fixed, but it's way too complicated. Since
# integration tests also skip checkpoint tests on Ubuntu, do the same here.
- if grep -qiw ubuntu /etc/os-release; then
+ if is_ubuntu; then
skip "FIXME: checkpointing broken in Ubuntu 2004, 2104, 2110, ..."
fi
diff --git a/test/system/helpers.bash b/test/system/helpers.bash
index 97b6db05c..2f36c2239 100644
--- a/test/system/helpers.bash
+++ b/test/system/helpers.bash
@@ -318,6 +318,10 @@ function wait_for_port() {
# BEGIN miscellaneous tools
# Shortcuts for common needs:
+function is_ubuntu() {
+ grep -qiw ubuntu /etc/os-release
+}
+
function is_rootless() {
[ "$(id -u)" -ne 0 ]
}
@@ -449,6 +453,16 @@ function skip_if_journald_unavailable {
fi
}
+function skip_if_root_ubuntu {
+ if is_ubuntu; then
+ if ! is_remote; then
+ if ! is_rootless; then
+ skip "Cannot run this test on rootful ubuntu, usually due to user errors"
+ fi
+ fi
+ fi
+}
+
#########
# die # Abort with helpful message
#########
diff --git a/utils/utils.go b/utils/utils.go
index 095370a08..d1374a39a 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -224,3 +224,12 @@ func MovePauseProcessToScope(pausePidPath string) {
}
}
}
+
+// CreateSCPCommand takes an existing command, appends the given arguments and returns a configured podman command for image scp
+func CreateSCPCommand(cmd *exec.Cmd, command []string) *exec.Cmd {
+ cmd.Args = append(cmd.Args, command...)
+ cmd.Env = os.Environ()
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+ return cmd
+}