summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoropenshift-ci[bot] <75433959+openshift-ci[bot]@users.noreply.github.com>2022-06-28 17:46:12 +0000
committerGitHub <noreply@github.com>2022-06-28 17:46:12 +0000
commitd8f197cc1491dace1ff12bff281d9adfbfd35761 (patch)
treecae59afd4e9f31f1859e24617643c3d0939ca249
parent50fa651a4e3cfde2b64aa818ad1009f4289f0afd (diff)
parent6d3520e8b7d7f57d389da08d1c8104c2cfbdd016 (diff)
downloadpodman-d8f197cc1491dace1ff12bff281d9adfbfd35761.tar.gz
podman-d8f197cc1491dace1ff12bff281d9adfbfd35761.tar.bz2
podman-d8f197cc1491dace1ff12bff281d9adfbfd35761.zip
Merge pull request #14400 from cdoern/scp
podman image scp remote support & podman image scp tagging
-rw-r--r--cmd/podman/images/scp.go302
-rw-r--r--cmd/podman/images/scp_test.go46
-rw-r--r--cmd/podman/images/scp_utils.go88
-rw-r--r--cmd/podman/system/connection/add.go111
-rw-r--r--cmd/podman/system/connection/shared.go27
-rw-r--r--contrib/cirrus/lib.sh2
-rwxr-xr-xcontrib/cirrus/setup_environment.sh4
-rw-r--r--pkg/api/handlers/libpod/images.go31
-rw-r--r--pkg/api/handlers/swagger/responses.go7
-rw-r--r--pkg/api/server/register_images.go34
-rw-r--r--pkg/bindings/images/images.go20
-rw-r--r--pkg/bindings/images/types.go5
-rw-r--r--pkg/bindings/images/types_scp_options.go12
-rw-r--r--pkg/domain/entities/engine_image.go2
-rw-r--r--pkg/domain/entities/images.go2
-rw-r--r--pkg/domain/entities/reports/scp.go5
-rw-r--r--pkg/domain/infra/abi/images.go151
-rw-r--r--pkg/domain/infra/tunnel/images.go26
-rw-r--r--pkg/domain/utils/scp.go581
-rw-r--r--pkg/domain/utils/utils_test.go39
-rw-r--r--test/apiv2/12-imagesMore.at13
-rwxr-xr-xtest/apiv2/test-apiv22
-rw-r--r--test/e2e/image_scp_test.go11
-rw-r--r--test/system/120-load.bats24
-rw-r--r--utils/utils.go24
25 files changed, 896 insertions, 673 deletions
diff --git a/cmd/podman/images/scp.go b/cmd/podman/images/scp.go
index 3dbc9c331..a7aa43e61 100644
--- a/cmd/podman/images/scp.go
+++ b/cmd/podman/images/scp.go
@@ -1,28 +1,12 @@
package images
import (
- "context"
- "fmt"
- "io/ioutil"
- urlP "net/url"
"os"
- "os/exec"
- "os/user"
- "strconv"
"strings"
- "github.com/containers/common/pkg/config"
"github.com/containers/podman/v4/cmd/podman/common"
"github.com/containers/podman/v4/cmd/podman/registry"
- "github.com/containers/podman/v4/cmd/podman/system/connection"
- "github.com/containers/podman/v4/libpod/define"
- "github.com/containers/podman/v4/pkg/domain/entities"
- "github.com/containers/podman/v4/utils"
- scpD "github.com/dtylman/scp"
- "github.com/pkg/errors"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
- "golang.org/x/crypto/ssh"
)
var (
@@ -32,7 +16,6 @@ var (
Annotations: map[string]string{
registry.UnshareNSRequired: "",
registry.ParentNSRequired: "",
- registry.EngineMode: registry.ABIMode,
},
Long: saveScpDescription,
Short: "securely copy images",
@@ -46,9 +29,6 @@ var (
var (
parentFlags []string
quiet bool
- source entities.ImageScpOptions
- dest entities.ImageScpOptions
- sshInfo entities.ImageScpConnections
)
func init() {
@@ -66,7 +46,6 @@ func scpFlags(cmd *cobra.Command) {
func scp(cmd *cobra.Command, args []string) (finalErr error) {
var (
- // TODO add tag support for images
err error
)
for i, val := range os.Args {
@@ -81,288 +60,17 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
}
parentFlags = append(parentFlags, val)
}
- podman, err := os.Executable()
- if err != nil {
- return err
- }
- 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
- }
-
- cfg, err := config.ReadCustomConfig() // get ready to set ssh destination if necessary
- if err != nil {
- return err
- }
- locations := []*entities.ImageScpOptions{}
- cliConnections := []string{}
- var flipConnections bool
- 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.
- cliConnections[0], cliConnections[1] = cliConnections[1], cliConnections[0]
- locations[0], locations[1] = locations[1], locations[0]
- }
- 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.Quiet = quiet
- 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
- }
-
- allLocal := true // if we are all localhost, do not validate connections but if we are using one localhost and one non we need to use sshd
- for _, val := range cliConnections {
- if !strings.Contains(val, "@localhost::") {
- allLocal = false
- break
- }
- }
- if allLocal {
- cliConnections = []string{}
- }
-
- 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 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 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
- }
- err = execPodman(podman, loadCmd)
- if err != nil {
- return err
- }
- case dest.Remote: // remote host load, implies source is local
- err = execPodman(podman, saveCmd)
- if err != nil {
- return err
- }
- rep, err := loadToRemote(source.File, "", sshInfo.URI[0], sshInfo.Identities[0])
- if err != nil {
- return err
- }
- fmt.Println(rep)
- if err = os.Remove(source.File); err != nil {
- return err
- }
- // 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(), source, dest, parentFlags)
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-// loadToRemote takes image and remote connection information. it connects to the specified client
-// and copies the saved image dir over to the remote host and then loads it onto the machine
-// returns a string containing output or an error
-func loadToRemote(localFile string, tag string, url *urlP.URL, iden string) (string, error) {
- dial, remoteFile, err := createConnection(url, iden)
- if err != nil {
- return "", err
- }
- defer dial.Close()
-
- n, err := scpD.CopyTo(dial, localFile, remoteFile)
- if err != nil {
- errOut := strconv.Itoa(int(n)) + " Bytes copied before error"
- return " ", errors.Wrapf(err, errOut)
- }
- var run string
- if tag != "" {
- return "", errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
- }
- podman := os.Args[0]
- run = podman + " image load --input=" + remoteFile + ";rm " + remoteFile // run ssh image load of the file copied via scp
- out, err := connection.ExecRemoteCommand(dial, run)
- if err != nil {
- return "", err
+ src := args[0]
+ dst := ""
+ if len(args) > 1 {
+ dst = args[1]
}
- return strings.TrimSuffix(string(out), "\n"), nil
-}
-
-// saveToRemote takes image information and remote connection information. it connects to the specified client
-// and saves the specified image on the remote machine and then copies it to the specified local location
-// returns an error if one occurs.
-func saveToRemote(image, localFile string, tag string, uri *urlP.URL, iden string) error {
- dial, remoteFile, err := createConnection(uri, iden)
+ err = registry.ImageEngine().Scp(registry.Context(), src, dst, parentFlags, quiet)
if err != nil {
return err
}
- defer dial.Close()
- if tag != "" {
- return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
- }
- podman := os.Args[0]
- run := podman + " image save " + image + " --format=oci-archive --output=" + remoteFile // run ssh image load of the file copied via scp. Files are reverse in this case...
- _, err = connection.ExecRemoteCommand(dial, run)
- if err != nil {
- return err
- }
- n, err := scpD.CopyFrom(dial, remoteFile, localFile)
- if _, conErr := connection.ExecRemoteCommand(dial, "rm "+remoteFile); conErr != nil {
- logrus.Errorf("Removing file on endpoint: %v", conErr)
- }
- if err != nil {
- errOut := strconv.Itoa(int(n)) + " Bytes copied before error"
- return errors.Wrapf(err, errOut)
- }
return nil
}
-
-// makeRemoteFile creates the necessary remote file on the host to
-// save or load the image to. returns a string with the file name or an error
-func makeRemoteFile(dial *ssh.Client) (string, error) {
- run := "mktemp"
- remoteFile, err := connection.ExecRemoteCommand(dial, run)
- if err != nil {
- return "", err
- }
- return strings.TrimSuffix(string(remoteFile), "\n"), nil
-}
-
-// createConnections takes a boolean determining which ssh client to dial
-// and returns the dials client, its newly opened remote file, and an error if applicable.
-func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) {
- cfg, err := connection.ValidateAndConfigure(url, iden)
- if err != nil {
- return nil, "", err
- }
- dialAdd, err := ssh.Dial("tcp", url.Host, cfg) // dial the client
- if err != nil {
- return nil, "", errors.Wrapf(err, "failed to connect")
- }
- file, err := makeRemoteFile(dialAdd)
- if err != nil {
- return nil, "", err
- }
-
- return dialAdd, file, nil
-}
-
-// 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)
- sshInfo.Connections = append(sshInfo.Connections, splitEnv[0])
- if len(splitEnv[1]) != 0 {
- err := validateImageName(splitEnv[1])
- if err != nil {
- return nil, err
- }
- source.Image = splitEnv[1]
- //TODO: actually use the new name given by the user
- }
- 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://" + sshInfo.Connections[i]
- iden = ""
- logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location")
- }
- urlT, err := urlP.Parse(url) // create an actual url to pass to exec command
- if err != nil {
- return nil, err
- }
- if urlT.User.Username() == "" {
- if urlT.User, err = connection.GetUserInfo(urlT); err != nil {
- return nil, err
- }
- }
- 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 {
- cmd := exec.Command(podman)
- utils.CreateSCPCommand(cmd, command[1:])
- logrus.Debugf("Executing podman command: %q", cmd)
- 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
deleted file mode 100644
index 315fda2ab..000000000
--- a/cmd/podman/images/scp_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package images
-
-import (
- "testing"
-
- "github.com/containers/podman/v4/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
deleted file mode 100644
index a85687a42..000000000
--- a/cmd/podman/images/scp_utils.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package images
-
-import (
- "strings"
-
- "github.com/containers/image/v5/docker/reference"
- "github.com/containers/podman/v4/libpod/define"
- "github.com/containers/podman/v4/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
- }
- cliConnections = append(cliConnections, arg)
- 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/system/connection/add.go b/cmd/podman/system/connection/add.go
index d77a39bcc..ec5fdccc8 100644
--- a/cmd/podman/system/connection/add.go
+++ b/cmd/podman/system/connection/add.go
@@ -6,21 +6,18 @@ import (
"net"
"net/url"
"os"
- "os/user"
"regexp"
- "time"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/system"
"github.com/containers/podman/v4/libpod/define"
- "github.com/containers/podman/v4/pkg/terminal"
+ "github.com/containers/podman/v4/pkg/domain/utils"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
- "golang.org/x/crypto/ssh/agent"
)
var (
@@ -95,7 +92,7 @@ func add(cmd *cobra.Command, args []string) error {
switch uri.Scheme {
case "ssh":
if uri.User.Username() == "" {
- if uri.User, err = GetUserInfo(uri); err != nil {
+ if uri.User, err = utils.GetUserInfo(uri); err != nil {
return err
}
}
@@ -180,32 +177,8 @@ func add(cmd *cobra.Command, args []string) error {
return cfg.Write()
}
-func GetUserInfo(uri *url.URL) (*url.Userinfo, error) {
- var (
- usr *user.User
- err error
- )
- if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found {
- usr, err = user.LookupId(u)
- if err != nil {
- return nil, errors.Wrapf(err, "failed to look up rootless user")
- }
- } else {
- usr, err = user.Current()
- if err != nil {
- return nil, errors.Wrapf(err, "failed to obtain current user")
- }
- }
-
- pw, set := uri.User.Password()
- if set {
- return url.UserPassword(usr.Username, pw), nil
- }
- return url.User(usr.Username), nil
-}
-
func getUDS(uri *url.URL, iden string) (string, error) {
- cfg, err := ValidateAndConfigure(uri, iden)
+ cfg, err := utils.ValidateAndConfigure(uri, iden)
if err != nil {
return "", errors.Wrapf(err, "failed to validate")
}
@@ -226,7 +199,7 @@ func getUDS(uri *url.URL, iden string) (string, error) {
if v, found := os.LookupEnv("PODMAN_BINARY"); found {
podman = v
}
- infoJSON, err := ExecRemoteCommand(dial, podman+" info --format=json")
+ infoJSON, err := utils.ExecRemoteCommand(dial, podman+" info --format=json")
if err != nil {
return "", err
}
@@ -241,79 +214,3 @@ func getUDS(uri *url.URL, iden string) (string, error) {
}
return info.Host.RemoteSocket.Path, nil
}
-
-// ValidateAndConfigure will take a ssh url and an identity key (rsa and the like) and ensure the information given is valid
-// iden iden can be blank to mean no identity key
-// once the function validates the information it creates and returns an ssh.ClientConfig.
-func ValidateAndConfigure(uri *url.URL, iden string) (*ssh.ClientConfig, error) {
- var signers []ssh.Signer
- passwd, passwdSet := uri.User.Password()
- if iden != "" { // iden might be blank if coming from image scp or if no validation is needed
- value := iden
- s, err := terminal.PublicKey(value, []byte(passwd))
- if err != nil {
- return nil, errors.Wrapf(err, "failed to read identity %q", value)
- }
- signers = append(signers, s)
- logrus.Debugf("SSH Ident Key %q %s %s", value, ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
- }
- if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { // validate ssh information, specifically the unix file socket used by the ssh agent.
- logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock)
-
- c, err := net.Dial("unix", sock)
- if err != nil {
- return nil, err
- }
- agentSigners, err := agent.NewClient(c).Signers()
- if err != nil {
- return nil, err
- }
-
- signers = append(signers, agentSigners...)
-
- if logrus.IsLevelEnabled(logrus.DebugLevel) {
- for _, s := range agentSigners {
- logrus.Debugf("SSH Agent Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
- }
- }
- }
- var authMethods []ssh.AuthMethod // now we validate and check for the authorization methods, most notaibly public key authorization
- if len(signers) > 0 {
- var dedup = make(map[string]ssh.Signer)
- for _, s := range signers {
- fp := ssh.FingerprintSHA256(s.PublicKey())
- if _, found := dedup[fp]; found {
- logrus.Debugf("Dedup SSH Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
- }
- dedup[fp] = s
- }
-
- var uniq []ssh.Signer
- for _, s := range dedup {
- uniq = append(uniq, s)
- }
- authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
- return uniq, nil
- }))
- }
- if passwdSet { // if password authentication is given and valid, add to the list
- authMethods = append(authMethods, ssh.Password(passwd))
- }
- if len(authMethods) == 0 {
- authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) {
- pass, err := terminal.ReadPassword(fmt.Sprintf("%s's login password:", uri.User.Username()))
- return string(pass), err
- }))
- }
- tick, err := time.ParseDuration("40s")
- if err != nil {
- return nil, err
- }
- cfg := &ssh.ClientConfig{
- User: uri.User.Username(),
- Auth: authMethods,
- HostKeyCallback: ssh.InsecureIgnoreHostKey(),
- Timeout: tick,
- }
- return cfg, nil
-}
diff --git a/cmd/podman/system/connection/shared.go b/cmd/podman/system/connection/shared.go
deleted file mode 100644
index 714ae827d..000000000
--- a/cmd/podman/system/connection/shared.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package connection
-
-import (
- "bytes"
-
- "github.com/pkg/errors"
- "golang.org/x/crypto/ssh"
-)
-
-// ExecRemoteCommand takes a ssh client connection and a command to run and executes the
-// command on the specified client. The function returns the Stdout from the client or the Stderr
-func ExecRemoteCommand(dial *ssh.Client, run string) ([]byte, error) {
- sess, err := dial.NewSession() // new ssh client session
- if err != nil {
- return nil, err
- }
- defer sess.Close()
-
- var buffer bytes.Buffer
- var bufferErr bytes.Buffer
- sess.Stdout = &buffer // output from client funneled into buffer
- sess.Stderr = &bufferErr // err form client funneled into buffer
- if err := sess.Run(run); err != nil { // run the command on the ssh client
- return nil, errors.Wrapf(err, bufferErr.String())
- }
- return buffer.Bytes(), nil
-}
diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh
index 2624af385..e7ea05867 100644
--- a/contrib/cirrus/lib.sh
+++ b/contrib/cirrus/lib.sh
@@ -135,6 +135,7 @@ setup_rootless() {
req_env_vars GOPATH GOSRC SECRET_ENV_RE
ROOTLESS_USER="${ROOTLESS_USER:-some${RANDOM}dude}"
+ ROOTLESS_UID=""
local rootless_uid
local rootless_gid
@@ -158,6 +159,7 @@ setup_rootless() {
cd $GOSRC || exit 1
# Guarantee independence from specific values
rootless_uid=$[RANDOM+1000]
+ ROOTLESS_UID=$rootless_uid
rootless_gid=$[RANDOM+1000]
msg "creating $rootless_uid:$rootless_gid $ROOTLESS_USER user"
groupadd -g $rootless_gid $ROOTLESS_USER
diff --git a/contrib/cirrus/setup_environment.sh b/contrib/cirrus/setup_environment.sh
index f31cd6eeb..9bd35bd06 100755
--- a/contrib/cirrus/setup_environment.sh
+++ b/contrib/cirrus/setup_environment.sh
@@ -186,10 +186,11 @@ esac
# Required to be defined by caller: Are we testing as root or a regular user
case "$PRIV_NAME" in
root)
- if [[ "$TEST_FLAVOR" = "sys" ]]; then
+ if [[ "$TEST_FLAVOR" = "sys" || "$TEST_FLAVOR" = "apiv2" ]]; then
# Used in local image-scp testing
setup_rootless
echo "PODMAN_ROOTLESS_USER=$ROOTLESS_USER" >> /etc/ci_environment
+ echo "PODMAN_ROOTLESS_UID=$ROOTLESS_UID" >> /etc/ci_environment
fi
;;
rootless)
@@ -203,6 +204,7 @@ esac
if [[ -n "$ROOTLESS_USER" ]]; then
echo "ROOTLESS_USER=$ROOTLESS_USER" >> /etc/ci_environment
+ echo "ROOTLESS_UID=$ROOTLESS_UID" >> /etc/ci_environment
fi
# Required to be defined by caller: Are we testing podman or podman-remote client
diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go
index a8a50ae58..2e450051d 100644
--- a/pkg/api/handlers/libpod/images.go
+++ b/pkg/api/handlers/libpod/images.go
@@ -21,7 +21,9 @@ import (
api "github.com/containers/podman/v4/pkg/api/types"
"github.com/containers/podman/v4/pkg/auth"
"github.com/containers/podman/v4/pkg/domain/entities"
+ "github.com/containers/podman/v4/pkg/domain/entities/reports"
"github.com/containers/podman/v4/pkg/domain/infra/abi"
+ domainUtils "github.com/containers/podman/v4/pkg/domain/utils"
"github.com/containers/podman/v4/pkg/errorhandling"
"github.com/containers/podman/v4/pkg/util"
utils2 "github.com/containers/podman/v4/utils"
@@ -670,3 +672,32 @@ func ImagesRemove(w http.ResponseWriter, r *http.Request) {
utils.Error(w, http.StatusInternalServerError, errorhandling.JoinErrors(rmErrors))
}
}
+
+func ImageScp(w http.ResponseWriter, r *http.Request) {
+ decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
+ query := struct {
+ Destination string `schema:"destination"`
+ Quiet bool `schema:"quiet"`
+ }{
+ // This is where you can override the golang default value for one of fields
+ }
+ if err := decoder.Decode(&query, r.URL.Query()); err != nil {
+ utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
+ return
+ }
+
+ sourceArg := utils.GetName(r)
+
+ rep, source, dest, _, err := domainUtils.ExecuteTransfer(sourceArg, query.Destination, []string{}, query.Quiet)
+ if err != nil {
+ utils.Error(w, http.StatusInternalServerError, err)
+ return
+ }
+
+ if source != nil || dest != nil {
+ utils.Error(w, http.StatusBadRequest, errors.Wrapf(define.ErrInvalidArg, "cannot use the user transfer function on the remote client"))
+ return
+ }
+
+ utils.WriteResponse(w, http.StatusOK, &reports.ScpReport{Id: rep.Names[0]})
+}
diff --git a/pkg/api/handlers/swagger/responses.go b/pkg/api/handlers/swagger/responses.go
index 55fc1a77f..93a508b39 100644
--- a/pkg/api/handlers/swagger/responses.go
+++ b/pkg/api/handlers/swagger/responses.go
@@ -41,6 +41,13 @@ type imagesLoadResponseLibpod struct {
Body entities.ImageLoadReport
}
+// Image Scp
+// swagger:response
+type imagesScpResponseLibpod struct {
+ // in:body
+ Body reports.ScpReport
+}
+
// Image Import
// swagger:response
type imagesImportResponseLibpod struct {
diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go
index 1617a5dd7..a9f9cb5b1 100644
--- a/pkg/api/server/register_images.go
+++ b/pkg/api/server/register_images.go
@@ -1615,5 +1615,39 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
// 500:
// $ref: "#/responses/internalError"
r.Handle(VersionedPath("/libpod/build"), s.APIHandler(compat.BuildImage)).Methods(http.MethodPost)
+
+ // swagger:operation POST /libpod/images/scp/{name} libpod ImageScpLibpod
+ // ---
+ // tags:
+ // - images
+ // summary: Copy an image from one host to another
+ // description: Copy an image from one host to another
+ // parameters:
+ // - in: path
+ // name: name
+ // required: true
+ // description: source connection/image
+ // type: string
+ // - in: query
+ // name: destination
+ // required: false
+ // description: dest connection/image
+ // type: string
+ // - in: query
+ // name: quiet
+ // required: false
+ // description: quiet output
+ // type: boolean
+ // default: false
+ // produces:
+ // - application/json
+ // responses:
+ // 200:
+ // $ref: "#/responses/imagesScpResponseLibpod"
+ // 400:
+ // $ref: "#/responses/badParamError"
+ // 500:
+ // $ref: '#/responses/internalError'
+ r.Handle(VersionedPath("/libpod/images/scp/{name:.*}"), s.APIHandler(libpod.ImageScp)).Methods(http.MethodPost)
return nil
}
diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go
index 32372019b..57c8bd597 100644
--- a/pkg/bindings/images/images.go
+++ b/pkg/bindings/images/images.go
@@ -346,3 +346,23 @@ func Search(ctx context.Context, term string, options *SearchOptions) ([]entitie
return results, nil
}
+
+func Scp(ctx context.Context, source, destination *string, options ScpOptions) (reports.ScpReport, error) {
+ rep := reports.ScpReport{}
+
+ conn, err := bindings.GetClient(ctx)
+ if err != nil {
+ return rep, err
+ }
+ params, err := options.ToParams()
+ if err != nil {
+ return rep, err
+ }
+ response, err := conn.DoRequest(ctx, nil, http.MethodPost, fmt.Sprintf("/images/scp/%s", *source), params, nil)
+ if err != nil {
+ return rep, err
+ }
+ defer response.Body.Close()
+
+ return rep, response.Process(&rep)
+}
diff --git a/pkg/bindings/images/types.go b/pkg/bindings/images/types.go
index 16dbad380..2c00c20cd 100644
--- a/pkg/bindings/images/types.go
+++ b/pkg/bindings/images/types.go
@@ -188,3 +188,8 @@ type BuildOptions struct {
// ExistsOptions are optional options for checking if an image exists
type ExistsOptions struct {
}
+
+type ScpOptions struct {
+ Quiet *bool
+ Destination *string
+}
diff --git a/pkg/bindings/images/types_scp_options.go b/pkg/bindings/images/types_scp_options.go
new file mode 100644
index 000000000..5a1178cb1
--- /dev/null
+++ b/pkg/bindings/images/types_scp_options.go
@@ -0,0 +1,12 @@
+package images
+
+import (
+ "net/url"
+
+ "github.com/containers/podman/v4/pkg/bindings/internal/util"
+)
+
+// ToParams formats struct fields to be passed to API service
+func (o *ScpOptions) ToParams() (url.Values, error) {
+ return util.ToParams(o)
+}
diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go
index 5011d82aa..5f76ae50b 100644
--- a/pkg/domain/entities/engine_image.go
+++ b/pkg/domain/entities/engine_image.go
@@ -22,12 +22,12 @@ type ImageEngine interface {
Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error
Remove(ctx context.Context, images []string, opts ImageRemoveOptions) (*ImageRemoveReport, []error)
Save(ctx context.Context, nameOrID string, tags []string, options ImageSaveOptions) error
+ Scp(ctx context.Context, src, dst string, parentFlags []string, quiet bool) error
Search(ctx context.Context, term string, opts ImageSearchOptions) ([]ImageSearchReport, error)
SetTrust(ctx context.Context, args []string, options SetTrustOptions) error
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, 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 11f6e8687..da317cfad 100644
--- a/pkg/domain/entities/images.go
+++ b/pkg/domain/entities/images.go
@@ -325,6 +325,8 @@ type ImageScpOptions struct {
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"`
+ // Tag is the name to be used for the image on the destination
+ Tag string `json:"tag,omitempty"`
}
// ImageScpConnections provides the ssh related information used in remote image transfer
diff --git a/pkg/domain/entities/reports/scp.go b/pkg/domain/entities/reports/scp.go
new file mode 100644
index 000000000..1e102bab3
--- /dev/null
+++ b/pkg/domain/entities/reports/scp.go
@@ -0,0 +1,5 @@
+package reports
+
+type ScpReport struct {
+ Id string `json:"Id"` //nolint:revive,stylecheck
+}
diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go
index d63de2424..02aa5923d 100644
--- a/pkg/domain/infra/abi/images.go
+++ b/pkg/domain/infra/abi/images.go
@@ -3,6 +3,7 @@ package abi
import (
"context"
"fmt"
+ "io/fs"
"io/ioutil"
"net/url"
"os"
@@ -29,7 +30,6 @@ import (
domainUtils "github.com/containers/podman/v4/pkg/domain/utils"
"github.com/containers/podman/v4/pkg/errorhandling"
"github.com/containers/podman/v4/pkg/rootless"
- "github.com/containers/podman/v4/utils"
"github.com/containers/storage"
dockerRef "github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
@@ -350,22 +350,6 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
}
return pushError
}
-
-// 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")
- }
- podman, err := os.Executable()
- 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 transferRootful(source, dest, podman, parentFlags)
-}
-
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}
@@ -694,53 +678,32 @@ func (ir *ImageEngine) Sign(ctx context.Context, names []string, options entitie
return nil, nil
}
-func getSigFilename(sigStoreDirPath string) (string, error) {
- sigFileSuffix := 1
- sigFiles, err := ioutil.ReadDir(sigStoreDirPath)
+func (ir *ImageEngine) Scp(ctx context.Context, src, dst string, parentFlags []string, quiet bool) error {
+ rep, source, dest, flags, err := domainUtils.ExecuteTransfer(src, dst, parentFlags, quiet)
if err != nil {
- return "", err
- }
- sigFilenames := make(map[string]bool)
- for _, file := range sigFiles {
- sigFilenames[file.Name()] = true
+ return err
}
- for {
- sigFilename := "signature-" + strconv.Itoa(sigFileSuffix)
- if _, exists := sigFilenames[sigFilename]; !exists {
- return sigFilename, nil
+ if (rep == nil && err == nil) && (source != nil && dest != nil) { // we need to execute the transfer
+ err := Transfer(ctx, *source, *dest, flags)
+ if err != nil {
+ return err
}
- sigFileSuffix++
- }
-}
-
-func localPathFromURI(url *url.URL) (string, error) {
- if url.Scheme != "file" {
- return "", errors.Errorf("writing to %s is not supported. Use a supported scheme", url.String())
}
- return url.Path, nil
+ return nil
}
-// putSignature creates signature and saves it to the signstore file
-func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStoreDir string, instanceDigest digest.Digest, dockerReference dockerRef.Reference, options entities.SignOptions) error {
- newSig, err := signature.SignDockerManifest(manifestBlob, dockerReference.String(), mech, options.SignBy)
- if err != nil {
- return err
- }
- signatureDir := fmt.Sprintf("%s@%s=%s", sigStoreDir, instanceDigest.Algorithm(), instanceDigest.Hex())
- if err := os.MkdirAll(signatureDir, 0751); err != nil {
- // The directory is allowed to exist
- if !os.IsExist(err) {
- return err
- }
+func 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")
}
- sigFilename, err := getSigFilename(signatureDir)
+ podman, err := os.Executable()
if err != nil {
return err
}
- if err = ioutil.WriteFile(filepath.Join(signatureDir, sigFilename), newSig, 0644); 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)
}
// TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users
@@ -763,7 +726,7 @@ func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOpt
} else {
cmdSave = exec.Command(podman)
}
- cmdSave = utils.CreateSCPCommand(cmdSave, saveCommand)
+ cmdSave = domainUtils.CreateSCPCommand(cmdSave, saveCommand)
logrus.Debugf("Executing save command: %q", cmdSave)
err := cmdSave.Run()
if err != nil {
@@ -776,8 +739,11 @@ func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOpt
} else {
cmdLoad = exec.Command(podman)
}
- cmdLoad = utils.CreateSCPCommand(cmdLoad, loadCommand)
+ cmdLoad = domainUtils.CreateSCPCommand(cmdLoad, loadCommand)
logrus.Debugf("Executing load command: %q", cmdLoad)
+ if len(dest.Tag) > 0 {
+ return domainUtils.ScpTag(cmdLoad, podman, dest)
+ }
return cmdLoad.Run()
}
@@ -833,11 +799,20 @@ func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOpti
return err
}
}
- err = execPodman(uSave, saveCommand)
+ _, err = execTransferPodman(uSave, saveCommand, false)
+ if err != nil {
+ return err
+ }
+ out, err := execTransferPodman(uLoad, loadCommand, (len(dest.Tag) > 0))
if err != nil {
return err
}
- return execPodman(uLoad, loadCommand)
+ if out != nil {
+ image := domainUtils.ExtractImage(out)
+ _, err := execTransferPodman(uLoad, []string{podman, "tag", image, dest.Tag}, false)
+ return err
+ }
+ return nil
}
func lookupUser(u string) (*user.User, error) {
@@ -847,10 +822,10 @@ func lookupUser(u string) (*user.User, error) {
return user.Lookup(u)
}
-func execPodman(execUser *user.User, command []string) error {
- cmdLogin, err := utils.LoginUser(execUser.Username)
+func execTransferPodman(execUser *user.User, command []string, needToTag bool) ([]byte, error) {
+ cmdLogin, err := domainUtils.LoginUser(execUser.Username)
if err != nil {
- return err
+ return nil, err
}
defer func() {
@@ -864,11 +839,11 @@ func execPodman(execUser *user.User, command []string) error {
cmd.Stdout = os.Stdout
uid, err := strconv.ParseInt(execUser.Uid, 10, 32)
if err != nil {
- return err
+ return nil, err
}
gid, err := strconv.ParseInt(execUser.Gid, 10, 32)
if err != nil {
- return err
+ return nil, err
}
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
@@ -878,5 +853,55 @@ func execPodman(execUser *user.User, command []string) error {
NoSetGroups: false,
},
}
- return cmd.Run()
+ if needToTag {
+ cmd.Stdout = nil
+ return cmd.Output()
+ }
+ return nil, cmd.Run()
+}
+
+func getSigFilename(sigStoreDirPath string) (string, error) {
+ sigFileSuffix := 1
+ sigFiles, err := ioutil.ReadDir(sigStoreDirPath)
+ if err != nil {
+ return "", err
+ }
+ sigFilenames := make(map[string]bool)
+ for _, file := range sigFiles {
+ sigFilenames[file.Name()] = true
+ }
+ for {
+ sigFilename := "signature-" + strconv.Itoa(sigFileSuffix)
+ if _, exists := sigFilenames[sigFilename]; !exists {
+ return sigFilename, nil
+ }
+ sigFileSuffix++
+ }
+}
+
+func localPathFromURI(url *url.URL) (string, error) {
+ if url.Scheme != "file" {
+ return "", errors.Errorf("writing to %s is not supported. Use a supported scheme", url.String())
+ }
+ return url.Path, nil
+}
+
+// putSignature creates signature and saves it to the signstore file
+func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStoreDir string, instanceDigest digest.Digest, dockerReference dockerRef.Reference, options entities.SignOptions) error {
+ newSig, err := signature.SignDockerManifest(manifestBlob, dockerReference.String(), mech, options.SignBy)
+ if err != nil {
+ return err
+ }
+ signatureDir := fmt.Sprintf("%s@%s=%s", sigStoreDir, instanceDigest.Algorithm(), instanceDigest.Hex())
+ if err := os.MkdirAll(signatureDir, 0751); err != nil {
+ // The directory is allowed to exist
+ if !errors.Is(err, fs.ErrExist) {
+ return err
+ }
+ }
+ sigFilename, err := getSigFilename(signatureDir)
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(filepath.Join(signatureDir, sigFilename), newSig, 0644)
}
diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go
index 97838d596..b81b64161 100644
--- a/pkg/domain/infra/tunnel/images.go
+++ b/pkg/domain/infra/tunnel/images.go
@@ -2,6 +2,7 @@ package tunnel
import (
"context"
+ "fmt"
"io/ioutil"
"os"
"strconv"
@@ -12,7 +13,6 @@ import (
"github.com/containers/common/pkg/config"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/types"
- "github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/bindings/images"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/containers/podman/v4/pkg/domain/entities/reports"
@@ -123,10 +123,6 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities.
return &entities.ImagePullReport{Images: pulledImages}, nil
}
-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")
-}
-
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, opt entities.ImageTagOptions) error {
options := new(images.TagOptions)
for _, newTag := range tags {
@@ -367,3 +363,23 @@ func (ir *ImageEngine) Shutdown(_ context.Context) {
func (ir *ImageEngine) Sign(ctx context.Context, names []string, options entities.SignOptions) (*entities.SignReport, error) {
return nil, errors.New("not implemented yet")
}
+
+func (ir *ImageEngine) Scp(ctx context.Context, src, dst string, parentFlags []string, quiet bool) error {
+ options := new(images.ScpOptions)
+
+ var destination *string
+ if len(dst) > 1 {
+ destination = &dst
+ }
+ options.Quiet = &quiet
+ options.Destination = destination
+
+ rep, err := images.Scp(ir.ClientCtx, &src, destination, *options)
+ if err != nil {
+ return err
+ }
+
+ fmt.Println("Loaded Image(s):", rep.Id)
+
+ return nil
+}
diff --git a/pkg/domain/utils/scp.go b/pkg/domain/utils/scp.go
new file mode 100644
index 000000000..a4ff6b950
--- /dev/null
+++ b/pkg/domain/utils/scp.go
@@ -0,0 +1,581 @@
+package utils
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/url"
+ "os"
+ "os/exec"
+ "os/user"
+ "strconv"
+ "strings"
+ "time"
+
+ scpD "github.com/dtylman/scp"
+
+ "github.com/containers/common/pkg/config"
+ "github.com/containers/podman/v4/libpod/define"
+ "github.com/containers/podman/v4/pkg/domain/entities"
+ "github.com/containers/podman/v4/pkg/terminal"
+ "github.com/docker/distribution/reference"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/crypto/ssh/agent"
+)
+
+func ExecuteTransfer(src, dst string, parentFlags []string, quiet bool) (*entities.ImageLoadReport, *entities.ImageScpOptions, *entities.ImageScpOptions, []string, error) {
+ source := entities.ImageScpOptions{}
+ dest := entities.ImageScpOptions{}
+ sshInfo := entities.ImageScpConnections{}
+ report := entities.ImageLoadReport{Names: []string{}}
+
+ podman, err := os.Executable()
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+
+ f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
+ if err != nil {
+ return nil, nil, nil, nil, 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 nil, nil, nil, nil, errors.Wrapf(err, "could not make config")
+ }
+
+ cfg, err := config.ReadCustomConfig() // get ready to set ssh destination if necessary
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+ locations := []*entities.ImageScpOptions{}
+ cliConnections := []string{}
+ args := []string{src}
+ if len(dst) > 0 {
+ args = append(args, dst)
+ }
+ for _, arg := range args {
+ loc, connect, err := ParseImageSCPArg(arg)
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+ locations = append(locations, loc)
+ cliConnections = append(cliConnections, connect...)
+ }
+ source = *locations[0]
+ switch {
+ case len(locations) > 1:
+ if err = ValidateSCPArgs(locations); err != nil {
+ return nil, nil, nil, nil, err
+ }
+ dest = *locations[1]
+ case len(locations) == 1:
+ switch {
+ case len(locations[0].Image) == 0:
+ return nil, nil, nil, nil, 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 nil, nil, nil, nil, errors.Wrapf(define.ErrInvalidArg, "must specify a destination")
+ }
+ }
+
+ source.Quiet = quiet
+ 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 nil, nil, nil, nil, err
+ }
+
+ allLocal := true // if we are all localhost, do not validate connections but if we are using one localhost and one non we need to use sshd
+ for _, val := range cliConnections {
+ if !strings.Contains(val, "@localhost::") {
+ allLocal = false
+ break
+ }
+ }
+ if allLocal {
+ cliConnections = []string{}
+ }
+
+ var serv map[string]config.Destination
+ serv, err = GetServiceInformation(&sshInfo, cliConnections, cfg)
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+
+ confR.Engine = config.EngineConfig{Remote: true, CgroupManager: "cgroupfs", ServiceDestinations: serv} // pass the service dest (either remote or something else) to engine
+ saveCmd, loadCmd := CreateCommands(source, dest, parentFlags, podman)
+
+ switch {
+ 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 nil, nil, nil, nil, err
+ }
+ if dest.Remote { // we want to load remote -> remote, both source and dest are remote
+ rep, id, err := LoadToRemote(dest, dest.File, "", sshInfo.URI[1], sshInfo.Identities[1])
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+ if len(rep) > 0 {
+ fmt.Println(rep)
+ }
+ if len(id) > 0 {
+ report.Names = append(report.Names, id)
+ }
+ break
+ }
+ id, err := ExecPodman(dest, podman, loadCmd)
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+ if len(id) > 0 {
+ report.Names = append(report.Names, id)
+ }
+ case dest.Remote: // remote host load, implies source is local
+ _, err = ExecPodman(dest, podman, saveCmd)
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+ rep, id, err := LoadToRemote(dest, source.File, "", sshInfo.URI[0], sshInfo.Identities[0])
+ if err != nil {
+ return nil, nil, nil, nil, err
+ }
+ if len(rep) > 0 {
+ fmt.Println(rep)
+ }
+ if len(id) > 0 {
+ report.Names = append(report.Names, id)
+ }
+ if err = os.Remove(source.File); err != nil {
+ return nil, nil, nil, nil, err
+ }
+ 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 nil, nil, nil, nil, errors.Wrapf(err, "could not obtain user, make sure the environmental variable $USER is set")
+ }
+ source.User = u.Username
+ }
+ }
+ return nil, &source, &dest, parentFlags, nil // transfer needs to be done in ABI due to cross issues
+ }
+
+ return &report, nil, nil, nil, nil
+}
+
+// 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
+}
+
+// ScpTag is a helper function for native podman to tag an image after a local load from image SCP
+func ScpTag(cmd *exec.Cmd, podman string, dest entities.ImageScpOptions) error {
+ cmd.Stdout = nil
+ out, err := cmd.Output() // this function captures the output temporarily in order to execute the next command
+ if err != nil {
+ return err
+ }
+ image := ExtractImage(out)
+ if cmd.Args[0] == "sudo" { // transferRootless will need the sudo since we are loading to sudo from a user acct
+ cmd = exec.Command("sudo", podman, "tag", image, dest.Tag)
+ } else {
+ cmd = exec.Command(podman, "tag", image, dest.Tag)
+ }
+ cmd.Stdout = os.Stdout
+ return cmd.Run()
+}
+
+// ExtractImage pulls out the last line of output from save/load (image id)
+func ExtractImage(out []byte) string {
+ fmt.Println(strings.TrimSuffix(string(out), "\n")) // print output
+ stringOut := string(out) // get all output
+ arrOut := strings.Split(stringOut, " ") // split it into an array
+ return strings.ReplaceAll(arrOut[len(arrOut)-1], "\n", "") // replace the trailing \n
+}
+
+// LoginUser starts the user process on the host so that image scp can use systemd-run
+func LoginUser(user string) (*exec.Cmd, error) {
+ sleep, err := exec.LookPath("sleep")
+ if err != nil {
+ return nil, err
+ }
+ machinectl, err := exec.LookPath("machinectl")
+ if err != nil {
+ return nil, err
+ }
+ cmd := exec.Command(machinectl, "shell", "-q", user+"@.host", sleep, "inf")
+ err = cmd.Start()
+ return cmd, err
+}
+
+// loadToRemote takes image and remote connection information. it connects to the specified client
+// and copies the saved image dir over to the remote host and then loads it onto the machine
+// returns a string containing output or an error
+func LoadToRemote(dest entities.ImageScpOptions, localFile string, tag string, url *url.URL, iden string) (string, string, error) {
+ dial, remoteFile, err := CreateConnection(url, iden)
+ if err != nil {
+ return "", "", err
+ }
+ defer dial.Close()
+
+ n, err := scpD.CopyTo(dial, localFile, remoteFile)
+ if err != nil {
+ errOut := strconv.Itoa(int(n)) + " Bytes copied before error"
+ return " ", "", errors.Wrapf(err, errOut)
+ }
+ var run string
+ if tag != "" {
+ return "", "", errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
+ }
+ podman := os.Args[0]
+ run = podman + " image load --input=" + remoteFile + ";rm " + remoteFile // run ssh image load of the file copied via scp
+ out, err := ExecRemoteCommand(dial, run)
+ if err != nil {
+ return "", "", err
+ }
+ rep := strings.TrimSuffix(string(out), "\n")
+ outArr := strings.Split(rep, " ")
+ id := outArr[len(outArr)-1]
+ if len(dest.Tag) > 0 { // tag the remote image using the output ID
+ run = podman + " tag " + id + " " + dest.Tag
+ _, err = ExecRemoteCommand(dial, run)
+ if err != nil {
+ return "", "", err
+ }
+ }
+ return rep, id, nil
+}
+
+// saveToRemote takes image information and remote connection information. it connects to the specified client
+// and saves the specified image on the remote machine and then copies it to the specified local location
+// returns an error if one occurs.
+func SaveToRemote(image, localFile string, tag string, uri *url.URL, iden string) error {
+ dial, remoteFile, err := CreateConnection(uri, iden)
+
+ if err != nil {
+ return err
+ }
+ defer dial.Close()
+
+ if tag != "" {
+ return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported")
+ }
+ podman := os.Args[0]
+ run := podman + " image save " + image + " --format=oci-archive --output=" + remoteFile // run ssh image load of the file copied via scp. Files are reverse in this case...
+ _, err = ExecRemoteCommand(dial, run)
+ if err != nil {
+ return err
+ }
+ n, err := scpD.CopyFrom(dial, remoteFile, localFile)
+ if _, conErr := ExecRemoteCommand(dial, "rm "+remoteFile); conErr != nil {
+ logrus.Errorf("Removing file on endpoint: %v", conErr)
+ }
+ if err != nil {
+ errOut := strconv.Itoa(int(n)) + " Bytes copied before error"
+ return errors.Wrapf(err, errOut)
+ }
+ return nil
+}
+
+// makeRemoteFile creates the necessary remote file on the host to
+// save or load the image to. returns a string with the file name or an error
+func MakeRemoteFile(dial *ssh.Client) (string, error) {
+ run := "mktemp"
+ remoteFile, err := ExecRemoteCommand(dial, run)
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimSuffix(string(remoteFile), "\n"), nil
+}
+
+// createConnections takes a boolean determining which ssh client to dial
+// and returns the dials client, its newly opened remote file, and an error if applicable.
+func CreateConnection(url *url.URL, iden string) (*ssh.Client, string, error) {
+ cfg, err := ValidateAndConfigure(url, iden)
+ if err != nil {
+ return nil, "", err
+ }
+ dialAdd, err := ssh.Dial("tcp", url.Host, cfg) // dial the client
+ if err != nil {
+ return nil, "", errors.Wrapf(err, "failed to connect")
+ }
+ file, err := MakeRemoteFile(dialAdd)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return dialAdd, file, nil
+}
+
+// GetSerivceInformation takes the parsed list of hosts to connect to and validates the information
+func GetServiceInformation(sshInfo *entities.ImageScpConnections, cliConnections []string, cfg *config.Config) (map[string]config.Destination, error) {
+ var serv map[string]config.Destination
+ var urlS string
+ var iden string
+ for i, val := range cliConnections {
+ splitEnv := strings.SplitN(val, "::", 2)
+ sshInfo.Connections = append(sshInfo.Connections, splitEnv[0])
+ conn, found := cfg.Engine.ServiceDestinations[sshInfo.Connections[i]]
+ if found {
+ urlS = conn.URI
+ iden = conn.Identity
+ } else { // no match, warn user and do a manual connection.
+ urlS = "ssh://" + sshInfo.Connections[i]
+ iden = ""
+ logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location")
+ }
+ urlFinal, err := url.Parse(urlS) // create an actual url to pass to exec command
+ if err != nil {
+ return nil, err
+ }
+ if urlFinal.User.Username() == "" {
+ if urlFinal.User, err = GetUserInfo(urlFinal); err != nil {
+ return nil, err
+ }
+ }
+ sshInfo.URI = append(sshInfo.URI, urlFinal)
+ sshInfo.Identities = append(sshInfo.Identities, iden)
+ }
+ return serv, nil
+}
+
+// execPodman executes the podman save/load command given the podman binary
+func ExecPodman(dest entities.ImageScpOptions, podman string, command []string) (string, error) {
+ cmd := exec.Command(podman)
+ CreateSCPCommand(cmd, command[1:])
+ logrus.Debugf("Executing podman command: %q", cmd)
+ if strings.Contains(strings.Join(command, " "), "load") { // need to tag
+ if len(dest.Tag) > 0 {
+ return "", ScpTag(cmd, podman, dest)
+ }
+ cmd.Stdout = nil
+ out, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+ img := ExtractImage(out)
+ return img, nil
+ }
+ return "", cmd.Run()
+}
+
+// createCommands forms the podman save and load commands used by SCP
+func CreateCommands(source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string, 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
+}
+
+// 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
+ }
+ cliConnections = append(cliConnections, arg)
+ 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) error {
+ if len(locations) > 2 {
+ return errors.Wrapf(define.ErrInvalidArg, "cannot specify more than two arguments")
+ }
+ switch {
+ case len(locations[0].Image) > 0 && len(locations[1].Image) > 0:
+ locations[1].Tag = locations[1].Image
+ locations[1].Image = ""
+ case len(locations[0].Image) == 0 && len(locations[1].Image) == 0:
+ return errors.Wrapf(define.ErrInvalidArg, "a source image must be specified")
+ }
+ return 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
+}
+
+// ExecRemoteCommand takes a ssh client connection and a command to run and executes the
+// command on the specified client. The function returns the Stdout from the client or the Stderr
+func ExecRemoteCommand(dial *ssh.Client, run string) ([]byte, error) {
+ sess, err := dial.NewSession() // new ssh client session
+ if err != nil {
+ return nil, err
+ }
+ defer sess.Close()
+
+ var buffer bytes.Buffer
+ var bufferErr bytes.Buffer
+ sess.Stdout = &buffer // output from client funneled into buffer
+ sess.Stderr = &bufferErr // err form client funneled into buffer
+ if err := sess.Run(run); err != nil { // run the command on the ssh client
+ return nil, errors.Wrapf(err, bufferErr.String())
+ }
+ return buffer.Bytes(), nil
+}
+
+func GetUserInfo(uri *url.URL) (*url.Userinfo, error) {
+ var (
+ usr *user.User
+ err error
+ )
+ if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found {
+ usr, err = user.LookupId(u)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to lookup rootless user")
+ }
+ } else {
+ usr, err = user.Current()
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to obtain current user")
+ }
+ }
+
+ pw, set := uri.User.Password()
+ if set {
+ return url.UserPassword(usr.Username, pw), nil
+ }
+ return url.User(usr.Username), nil
+}
+
+// ValidateAndConfigure will take a ssh url and an identity key (rsa and the like) and ensure the information given is valid
+// iden iden can be blank to mean no identity key
+// once the function validates the information it creates and returns an ssh.ClientConfig.
+func ValidateAndConfigure(uri *url.URL, iden string) (*ssh.ClientConfig, error) {
+ var signers []ssh.Signer
+ passwd, passwdSet := uri.User.Password()
+ if iden != "" { // iden might be blank if coming from image scp or if no validation is needed
+ value := iden
+ s, err := terminal.PublicKey(value, []byte(passwd))
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to read identity %q", value)
+ }
+ signers = append(signers, s)
+ logrus.Debugf("SSH Ident Key %q %s %s", value, ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
+ }
+ if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { // validate ssh information, specifically the unix file socket used by the ssh agent.
+ logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock)
+
+ c, err := net.Dial("unix", sock)
+ if err != nil {
+ return nil, err
+ }
+ agentSigners, err := agent.NewClient(c).Signers()
+ if err != nil {
+ return nil, err
+ }
+
+ signers = append(signers, agentSigners...)
+
+ if logrus.IsLevelEnabled(logrus.DebugLevel) {
+ for _, s := range agentSigners {
+ logrus.Debugf("SSH Agent Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
+ }
+ }
+ }
+ var authMethods []ssh.AuthMethod // now we validate and check for the authorization methods, most notaibly public key authorization
+ if len(signers) > 0 {
+ var dedup = make(map[string]ssh.Signer)
+ for _, s := range signers {
+ fp := ssh.FingerprintSHA256(s.PublicKey())
+ if _, found := dedup[fp]; found {
+ logrus.Debugf("Dedup SSH Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type())
+ }
+ dedup[fp] = s
+ }
+
+ var uniq []ssh.Signer
+ for _, s := range dedup {
+ uniq = append(uniq, s)
+ }
+ authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
+ return uniq, nil
+ }))
+ }
+ if passwdSet { // if password authentication is given and valid, add to the list
+ authMethods = append(authMethods, ssh.Password(passwd))
+ }
+ if len(authMethods) == 0 {
+ authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) {
+ pass, err := terminal.ReadPassword(fmt.Sprintf("%s's login password:", uri.User.Username()))
+ return string(pass), err
+ }))
+ }
+ tick, err := time.ParseDuration("40s")
+ if err != nil {
+ return nil, err
+ }
+ cfg := &ssh.ClientConfig{
+ User: uri.User.Username(),
+ Auth: authMethods,
+ HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+ Timeout: tick,
+ }
+ return cfg, nil
+}
diff --git a/pkg/domain/utils/utils_test.go b/pkg/domain/utils/utils_test.go
index 952a4b5be..291567f6b 100644
--- a/pkg/domain/utils/utils_test.go
+++ b/pkg/domain/utils/utils_test.go
@@ -5,6 +5,7 @@ import (
"sort"
"testing"
+ "github.com/containers/podman/v4/pkg/domain/entities"
"github.com/stretchr/testify/assert"
)
@@ -74,3 +75,41 @@ func TestToURLValues(t *testing.T) {
})
}
}
+
+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/test/apiv2/12-imagesMore.at b/test/apiv2/12-imagesMore.at
index 57d5e114d..fc18dd2d7 100644
--- a/test/apiv2/12-imagesMore.at
+++ b/test/apiv2/12-imagesMore.at
@@ -56,4 +56,17 @@ t GET libpod/images/$IMAGE/json 200 \
t DELETE libpod/images/$IMAGE 200 \
.ExitCode=0
+podman pull -q $IMAGE
+
+# test podman image SCP
+# ssh needs to work so we can validate that the failure is past argument parsing
+podman system connection add --default test ssh://$USER@localhost/run/user/$UID/podman/podman.sock
+# should fail but need to check the output...
+# status 125 here means that the save/load fails due to
+# cirrus weirdness with exec.Command. All of the args have been parsed sucessfully.
+t POST "libpod/images/scp/$IMAGE?destination=QA::" 500 \
+ .cause="exit status 125"
+t DELETE libpod/images/$IMAGE 200 \
+ .ExitCode=0
+
stop_registry
diff --git a/test/apiv2/test-apiv2 b/test/apiv2/test-apiv2
index 25f648d93..8548d84e5 100755
--- a/test/apiv2/test-apiv2
+++ b/test/apiv2/test-apiv2
@@ -23,6 +23,8 @@ REGISTRY_IMAGE="${PODMAN_TEST_IMAGE_REGISTRY}/${PODMAN_TEST_IMAGE_USER}/registry
###############################################################################
# BEGIN setup
+USER=$PODMAN_ROOTLESS_USER
+UID=$PODMAN_ROOTLESS_UID
TMPDIR=${TMPDIR:-/tmp}
WORKDIR=$(mktemp --tmpdir -d $ME.tmp.XXXXXX)
diff --git a/test/e2e/image_scp_test.go b/test/e2e/image_scp_test.go
index 53681f05b..77fe810bd 100644
--- a/test/e2e/image_scp_test.go
+++ b/test/e2e/image_scp_test.go
@@ -50,18 +50,12 @@ var _ = Describe("podman image scp", func() {
})
It("podman image scp bogus image", func() {
- if IsRemote() {
- Skip("this test is only for non-remote")
- }
scp := podmanTest.Podman([]string{"image", "scp", "FOOBAR"})
scp.WaitWithDefaultTimeout()
Expect(scp).Should(ExitWithError())
})
It("podman image scp with proper connection", func() {
- if IsRemote() {
- Skip("this test is only for non-remote")
- }
cmd := []string{"system", "connection", "add",
"--default",
"QA",
@@ -86,7 +80,10 @@ var _ = Describe("podman image scp", func() {
// This tests that the input we are given is validated and prepared correctly
// The error given should either be a missing image (due to testing suite complications) or a no such host timeout on ssh
Expect(scp).Should(ExitWithError())
- Expect(scp.ErrorToString()).Should(ContainSubstring("no such host"))
+ // podman-remote exits with a different error
+ if !IsRemote() {
+ Expect(scp.ErrorToString()).Should(ContainSubstring("no such host"))
+ }
})
diff --git a/test/system/120-load.bats b/test/system/120-load.bats
index 5a7f63b43..7f0bcfd95 100644
--- a/test/system/120-load.bats
+++ b/test/system/120-load.bats
@@ -128,8 +128,24 @@ verify_iid_and_name() {
run_podman image inspect --format '{{.Digest}}' $newname
is "$output" "$src_digest" "Digest of re-fetched image matches original"
- # Clean up
+ # test tagging capability
+ run_podman untag $IMAGE $newname
+ run_podman image scp ${notme}@localhost::$newname foobar:123
+
+ run_podman image inspect --format '{{.Digest}}' foobar:123
+ is "$output" "$src_digest" "Digest of re-fetched image matches original"
+
+ # remove root img for transfer back with another name
_sudo $PODMAN image rm $newname
+
+ # get foobar's ID, for an ID transfer test
+ run_podman image inspect --format '{{.ID}}' foobar:123
+ run_podman image scp $output ${notme}@localhost::foobartwo
+
+ _sudo $PODMAN image exists foobartwo
+
+ # Clean up
+ _sudo $PODMAN image rm foobartwo
run_podman untag $IMAGE $newname
# Negative test for nonexistent image.
@@ -142,12 +158,6 @@ verify_iid_and_name() {
run_podman 125 image scp $nope ${notme}@localhost::
is "$output" "Error: $nope: image not known.*" "Pushing nonexistent image"
- # Negative test for copying to a different name
- run_podman 125 image scp $IMAGE ${notme}@localhost::newname:newtag
- is "$output" "Error: cannot specify an image rename: invalid argument" \
- "Pushing with a different name: not allowed"
-
- # FIXME: any point in copying by image ID? What else should we test?
}
diff --git a/utils/utils.go b/utils/utils.go
index fd66ac2ed..9239cf907 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -243,27 +243,3 @@ 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
-}
-
-// LoginUser starts the user process on the host so that image scp can use systemd-run
-func LoginUser(user string) (*exec.Cmd, error) {
- sleep, err := exec.LookPath("sleep")
- if err != nil {
- return nil, err
- }
- machinectl, err := exec.LookPath("machinectl")
- if err != nil {
- return nil, err
- }
- cmd := exec.Command(machinectl, "shell", "-q", user+"@.host", sleep, "inf")
- err = cmd.Start()
- return cmd, err
-}