mirror of
https://github.com/containers/podman.git
synced 2025-05-22 17:46:52 +08:00
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>
This commit is contained in:
@ -6,18 +6,19 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
urlP "net/url"
|
urlP "net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containers/common/pkg/config"
|
"github.com/containers/common/pkg/config"
|
||||||
"github.com/containers/podman/v3/cmd/podman/common"
|
"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/registry"
|
||||||
"github.com/containers/podman/v3/cmd/podman/system/connection"
|
"github.com/containers/podman/v3/cmd/podman/system/connection"
|
||||||
"github.com/containers/podman/v3/libpod/define"
|
"github.com/containers/podman/v3/libpod/define"
|
||||||
"github.com/containers/podman/v3/pkg/domain/entities"
|
"github.com/containers/podman/v3/pkg/domain/entities"
|
||||||
"github.com/containers/podman/v3/pkg/rootless"
|
"github.com/containers/podman/v3/pkg/rootless"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/containers/podman/v3/utils"
|
||||||
scpD "github.com/dtylman/scp"
|
scpD "github.com/dtylman/scp"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -29,7 +30,11 @@ var (
|
|||||||
saveScpDescription = `Securely copy an image from one host to another.`
|
saveScpDescription = `Securely copy an image from one host to another.`
|
||||||
imageScpCommand = &cobra.Command{
|
imageScpCommand = &cobra.Command{
|
||||||
Use: "scp [options] IMAGE [HOST::]",
|
Use: "scp [options] IMAGE [HOST::]",
|
||||||
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
|
Annotations: map[string]string{
|
||||||
|
registry.UnshareNSRequired: "",
|
||||||
|
registry.ParentNSRequired: "",
|
||||||
|
registry.EngineMode: registry.ABIMode,
|
||||||
|
},
|
||||||
Long: saveScpDescription,
|
Long: saveScpDescription,
|
||||||
Short: "securely copy images",
|
Short: "securely copy images",
|
||||||
RunE: scp,
|
RunE: scp,
|
||||||
@ -40,7 +45,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
scpOpts entities.ImageScpOptions
|
parentFlags []string
|
||||||
|
source entities.ImageScpOptions
|
||||||
|
dest entities.ImageScpOptions
|
||||||
|
sshInfo entities.ImageScpConnections
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -53,7 +61,7 @@ func init() {
|
|||||||
|
|
||||||
func scpFlags(cmd *cobra.Command) {
|
func scpFlags(cmd *cobra.Command) {
|
||||||
flags := cmd.Flags()
|
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) {
|
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
|
// TODO add tag support for images
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if scpOpts.Save.Quiet { // set quiet for both load and save
|
for i, val := range os.Args {
|
||||||
scpOpts.Load.Quiet = true
|
if val == "image" {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(val, "CIRRUS") { // need to skip CIRRUS flags for testing suite purposes
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parentFlags = append(parentFlags, val)
|
||||||
|
}
|
||||||
|
podman, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer os.Remove(f.Name())
|
f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
|
||||||
|
if err != nil {
|
||||||
scpOpts.Save.Output = f.Name()
|
|
||||||
scpOpts.Load.Input = scpOpts.Save.Output
|
|
||||||
if err := parse.ValidateFileName(saveOpts.Output); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
confR, err := config.NewConfig("") // create a hand made config for the remote engine since we might use remote and native at once
|
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 {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "could not make config")
|
return errors.Wrapf(err, "could not make config")
|
||||||
}
|
}
|
||||||
|
|
||||||
abiEng, err := registry.NewImageEngine(cmd, args) // abi native engine
|
abiEng, err := registry.NewImageEngine(cmd, args) // abi native engine
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -88,77 +103,115 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
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
|
// 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
|
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 {
|
switch {
|
||||||
case scpOpts.FromRemote: // if we want to load FROM the remote
|
case source.Remote: // if we want to load FROM the remote, dest can either be local or remote in this case
|
||||||
err = saveToRemote(scpOpts.SourceImageName, scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0])
|
err = saveToRemote(source.Image, source.File, "", sshInfo.URI[0], sshInfo.Identities[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if scpOpts.ToRemote { // we want to load remote -> remote
|
if dest.Remote { // we want to load remote -> remote, both source and dest are remote
|
||||||
rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[1], scpOpts.Iden[1])
|
rep, err := loadToRemote(dest.File, "", sshInfo.URI[1], sshInfo.Identities[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println(rep)
|
fmt.Println(rep)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
report, err := abiEng.Load(context.Background(), scpOpts.Load)
|
err = execPodman(podman, loadCmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("Loaded image(s): " + strings.Join(report.Names, ","))
|
case dest.Remote: // remote host load, implies source is local
|
||||||
case scpOpts.ToRemote: // remote host load
|
err = execPodman(podman, saveCmd)
|
||||||
scpOpts.Save.Format = "oci-archive"
|
if err != nil {
|
||||||
abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh
|
return err
|
||||||
if abiErr != nil {
|
|
||||||
errors.Wrapf(abiErr, "could not save image as specified")
|
|
||||||
}
|
}
|
||||||
rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0])
|
rep, err := loadToRemote(source.File, "", sshInfo.URI[0], sshInfo.Identities[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println(rep)
|
fmt.Println(rep)
|
||||||
|
if err = os.Remove(source.File); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// TODO: Add podman remote support
|
// TODO: Add podman remote support
|
||||||
default: // else native load
|
default: // else native load, both source and dest are local and transferring between users
|
||||||
scpOpts.Save.Format = "oci-archive"
|
if source.User == "" { // source user has to be set, destination does not
|
||||||
_, err := os.Open(scpOpts.Save.Output)
|
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 {
|
if err != nil {
|
||||||
return err
|
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")
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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, ","))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,119 +302,28 @@ func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) {
|
|||||||
return dialAdd, file, nil
|
return dialAdd, file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateImageName makes sure that the image given is valid and no injections are occurring
|
// GetSerivceInformation takes the parsed list of hosts to connect to and validates the information
|
||||||
// we simply use this for error checking, bot setting the image
|
func GetServiceInformation(cliConnections []string, cfg *config.Config) (map[string]config.Destination, error) {
|
||||||
func validateImageName(input string) error {
|
var serv map[string]config.Destination
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var url string
|
var url string
|
||||||
var iden string
|
var iden string
|
||||||
for i, val := range cliConnections {
|
for i, val := range cliConnections {
|
||||||
splitEnv := strings.SplitN(val, "::", 2)
|
splitEnv := strings.SplitN(val, "::", 2)
|
||||||
scpOpts.Connections = append(scpOpts.Connections, splitEnv[0])
|
sshInfo.Connections = append(sshInfo.Connections, splitEnv[0])
|
||||||
if len(splitEnv[1]) != 0 {
|
if len(splitEnv[1]) != 0 {
|
||||||
err := validateImageName(splitEnv[1])
|
err := validateImageName(splitEnv[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
scpOpts.SourceImageName = splitEnv[1]
|
source.Image = splitEnv[1]
|
||||||
//TODO: actually use the new name given by the user
|
//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 {
|
if found {
|
||||||
url = conn.URI
|
url = conn.URI
|
||||||
iden = conn.Identity
|
iden = conn.Identity
|
||||||
} else { // no match, warn user and do a manual connection.
|
} else { // no match, warn user and do a manual connection.
|
||||||
url = "ssh://" + scpOpts.Connections[i]
|
url = "ssh://" + sshInfo.Connections[i]
|
||||||
iden = ""
|
iden = ""
|
||||||
logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location")
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scpOpts.URI = append(scpOpts.URI, urlT)
|
sshInfo.URI = append(sshInfo.URI, urlT)
|
||||||
scpOpts.Iden = append(scpOpts.Iden, iden)
|
sshInfo.Identities = append(sshInfo.Identities, iden)
|
||||||
}
|
}
|
||||||
return serv, nil
|
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
|
||||||
|
}
|
||||||
|
46
cmd/podman/images/scp_test.go
Normal file
46
cmd/podman/images/scp_test.go
Normal file
@ -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")
|
||||||
|
}
|
87
cmd/podman/images/scp_utils.go
Normal file
87
cmd/podman/images/scp_utils.go
Normal file
@ -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
|
||||||
|
}
|
@ -52,14 +52,14 @@ func parseCommands() *cobra.Command {
|
|||||||
// Command cannot be run rootless
|
// Command cannot be run rootless
|
||||||
_, found := c.Command.Annotations[registry.UnshareNSRequired]
|
_, found := c.Command.Annotations[registry.UnshareNSRequired]
|
||||||
if found {
|
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 {
|
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())
|
return fmt.Errorf("cannot run command %q in rootless mode, must execute `podman unshare` first", cmd.CommandPath())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, found = c.Command.Annotations[registry.ParentNSRequired]
|
_, 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 {
|
c.Command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath())
|
return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath())
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,7 @@ setup_rootless() {
|
|||||||
groupadd -g $rootless_gid $ROOTLESS_USER
|
groupadd -g $rootless_gid $ROOTLESS_USER
|
||||||
useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER
|
useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER
|
||||||
chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC"
|
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"
|
mkdir -p "$HOME/.ssh" "/home/$ROOTLESS_USER/.ssh"
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ type ImageEngine interface {
|
|||||||
ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error)
|
ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error)
|
||||||
Shutdown(ctx context.Context)
|
Shutdown(ctx context.Context)
|
||||||
Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error
|
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)
|
Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error)
|
||||||
Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error)
|
Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error)
|
||||||
Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error
|
Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error
|
||||||
|
@ -311,30 +311,28 @@ type ImageSaveOptions struct {
|
|||||||
Quiet bool
|
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 {
|
type ImageScpOptions struct {
|
||||||
// SoureImageName is the image the user is providing to load on a remote machine
|
// Remote determines if this entity is operating on a remote machine
|
||||||
SourceImageName string
|
Remote bool `json:"remote,omitempty"`
|
||||||
// Tag allows for a new image to be created under the given name
|
// File is the input/output file for the save and load Operation
|
||||||
Tag string
|
File string `json:"file,omitempty"`
|
||||||
// ToRemote specifies that we are loading to the remote host
|
// Quiet Determines if the save and load operation will be done quietly
|
||||||
ToRemote bool
|
Quiet bool `json:"quiet,omitempty"`
|
||||||
// FromRemote specifies that we are loading from the remote host
|
// Image is the image the user is providing to save and load
|
||||||
FromRemote bool
|
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 holds the raw string values for connections (ssh or unix)
|
||||||
Connections []string
|
Connections []string
|
||||||
// URI contains the ssh connection URLs to be used by the client
|
// URI contains the ssh connection URLs to be used by the client
|
||||||
URI []*url.URL
|
URI []*url.URL
|
||||||
// Iden contains ssh identity keys to be used by the client
|
// Identities contains ssh identity keys to be used by the client
|
||||||
Iden []string
|
Identities []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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageTreeOptions provides options for ImageEngine.Tree()
|
// ImageTreeOptions provides options for ImageEngine.Tree()
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
domainUtils "github.com/containers/podman/v3/pkg/domain/utils"
|
domainUtils "github.com/containers/podman/v3/pkg/domain/utils"
|
||||||
"github.com/containers/podman/v3/pkg/errorhandling"
|
"github.com/containers/podman/v3/pkg/errorhandling"
|
||||||
"github.com/containers/podman/v3/pkg/rootless"
|
"github.com/containers/podman/v3/pkg/rootless"
|
||||||
|
"github.com/containers/podman/v3/utils"
|
||||||
"github.com/containers/storage"
|
"github.com/containers/storage"
|
||||||
dockerRef "github.com/docker/distribution/reference"
|
dockerRef "github.com/docker/distribution/reference"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
@ -351,65 +352,19 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
|
|||||||
return pushError
|
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
|
// 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, scpOpts entities.ImageScpOptions) error {
|
func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error {
|
||||||
if scpOpts.User == "" {
|
if source.User == "" {
|
||||||
return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage")
|
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()
|
podman, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
machinectl, err := exec.LookPath("machinectl")
|
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
|
||||||
if err != nil {
|
return transferRootless(source, dest, podman, parentFlags)
|
||||||
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 {
|
return transferRootful(source, dest, podman, parentFlags)
|
||||||
cmd := exec.Command(machinectl, "shell", "-q", u.Username+"@.host", podman, "--log-level="+logrus.GetLevel().String(), "--cgroup-manager=cgroupfs", "load", "--input", scpOpts.Save.Output) // load the new image to the rootless storage
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
logrus.Debug("Executing load command machinectl")
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
|
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
|
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()
|
||||||
|
}
|
||||||
|
@ -123,7 +123,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities.
|
|||||||
return &entities.ImagePullReport{Images: pulledImages}, nil
|
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")
|
return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,7 +244,7 @@ can_use_shortcut ()
|
|||||||
|
|
||||||
if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 ||
|
if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 ||
|
||||||
strcmp (argv[argc], "image") == 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;
|
ret = false;
|
||||||
break;
|
break;
|
||||||
|
@ -29,7 +29,6 @@ var _ = Describe("podman image scp", func() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
os.Setenv("CONTAINERS_CONF", conf.Name())
|
os.Setenv("CONTAINERS_CONF", conf.Name())
|
||||||
|
|
||||||
tempdir, err = CreateTempDirInTempDir()
|
tempdir, err = CreateTempDirInTempDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Exit(1)
|
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() {
|
It("podman image scp bogus image", func() {
|
||||||
if IsRemote() {
|
if IsRemote() {
|
||||||
Skip("this test is only for non-remote")
|
Skip("this test is only for non-remote")
|
||||||
@ -119,11 +86,8 @@ var _ = Describe("podman image scp", func() {
|
|||||||
scp.Wait(45)
|
scp.Wait(45)
|
||||||
// exit with error because we cannot make an actual ssh connection
|
// exit with error because we cannot make an actual ssh connection
|
||||||
// This tests that the input we are given is validated and prepared correctly
|
// 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).To(ExitWithError())
|
||||||
Expect(scp.ErrorToString()).To(ContainSubstring(
|
|
||||||
"Error: failed to connect: dial tcp 66.151.147.142:2222: i/o timeout",
|
|
||||||
))
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -78,6 +78,35 @@ verify_iid_and_name() {
|
|||||||
run_podman rmi $fqin
|
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" {
|
@test "podman load - by image ID" {
|
||||||
# FIXME: how to build a simple archive instead?
|
# FIXME: how to build a simple archive instead?
|
||||||
|
@ -11,7 +11,7 @@ function setup() {
|
|||||||
# TL;DR they keep fixing it then breaking it again. There's a test we
|
# 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
|
# 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.
|
# 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, ..."
|
skip "FIXME: checkpointing broken in Ubuntu 2004, 2104, 2110, ..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -318,6 +318,10 @@ function wait_for_port() {
|
|||||||
# BEGIN miscellaneous tools
|
# BEGIN miscellaneous tools
|
||||||
|
|
||||||
# Shortcuts for common needs:
|
# Shortcuts for common needs:
|
||||||
|
function is_ubuntu() {
|
||||||
|
grep -qiw ubuntu /etc/os-release
|
||||||
|
}
|
||||||
|
|
||||||
function is_rootless() {
|
function is_rootless() {
|
||||||
[ "$(id -u)" -ne 0 ]
|
[ "$(id -u)" -ne 0 ]
|
||||||
}
|
}
|
||||||
@ -449,6 +453,16 @@ function skip_if_journald_unavailable {
|
|||||||
fi
|
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
|
# die # Abort with helpful message
|
||||||
#########
|
#########
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user