mirror of
https://github.com/containers/podman.git
synced 2025-05-21 17:16:22 +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"
|
||||
urlP "net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/podman/v3/cmd/podman/common"
|
||||
"github.com/containers/podman/v3/cmd/podman/parse"
|
||||
"github.com/containers/podman/v3/cmd/podman/registry"
|
||||
"github.com/containers/podman/v3/cmd/podman/system/connection"
|
||||
"github.com/containers/podman/v3/libpod/define"
|
||||
"github.com/containers/podman/v3/pkg/domain/entities"
|
||||
"github.com/containers/podman/v3/pkg/rootless"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/containers/podman/v3/utils"
|
||||
scpD "github.com/dtylman/scp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -29,7 +30,11 @@ var (
|
||||
saveScpDescription = `Securely copy an image from one host to another.`
|
||||
imageScpCommand = &cobra.Command{
|
||||
Use: "scp [options] IMAGE [HOST::]",
|
||||
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
|
||||
Annotations: map[string]string{
|
||||
registry.UnshareNSRequired: "",
|
||||
registry.ParentNSRequired: "",
|
||||
registry.EngineMode: registry.ABIMode,
|
||||
},
|
||||
Long: saveScpDescription,
|
||||
Short: "securely copy images",
|
||||
RunE: scp,
|
||||
@ -40,7 +45,10 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
scpOpts entities.ImageScpOptions
|
||||
parentFlags []string
|
||||
source entities.ImageScpOptions
|
||||
dest entities.ImageScpOptions
|
||||
sshInfo entities.ImageScpConnections
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -53,7 +61,7 @@ func init() {
|
||||
|
||||
func scpFlags(cmd *cobra.Command) {
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&scpOpts.Save.Quiet, "quiet", "q", false, "Suppress the output")
|
||||
flags.BoolVarP(&source.Quiet, "quiet", "q", false, "Suppress the output")
|
||||
}
|
||||
|
||||
func scp(cmd *cobra.Command, args []string) (finalErr error) {
|
||||
@ -61,24 +69,31 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
|
||||
// TODO add tag support for images
|
||||
err error
|
||||
)
|
||||
if scpOpts.Save.Quiet { // set quiet for both load and save
|
||||
scpOpts.Load.Quiet = true
|
||||
for i, val := range os.Args {
|
||||
if val == "image" {
|
||||
break
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
scpOpts.Save.Output = f.Name()
|
||||
scpOpts.Load.Input = scpOpts.Save.Output
|
||||
if err := parse.ValidateFileName(saveOpts.Output); err != nil {
|
||||
f, err := ioutil.TempFile("", "podman") // open temp file for load/save output
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confR, err := config.NewConfig("") // create a hand made config for the remote engine since we might use remote and native at once
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not make config")
|
||||
}
|
||||
|
||||
abiEng, err := registry.NewImageEngine(cmd, args) // abi native engine
|
||||
if err != nil {
|
||||
return err
|
||||
@ -88,77 +103,115 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serv, err := parseArgs(args, cfg) // parses connection data and "which way" we are loading and saving
|
||||
locations := []*entities.ImageScpOptions{}
|
||||
cliConnections := []string{}
|
||||
flipConnections := false
|
||||
for _, arg := range args {
|
||||
loc, connect, err := parseImageSCPArg(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
locations = append(locations, loc)
|
||||
cliConnections = append(cliConnections, connect...)
|
||||
}
|
||||
source = *locations[0]
|
||||
switch {
|
||||
case len(locations) > 1:
|
||||
if flipConnections, err = validateSCPArgs(locations); err != nil {
|
||||
return err
|
||||
}
|
||||
if flipConnections { // the order of cliConnections matters, we need to flip both arrays since the args are parsed separately sometimes.
|
||||
connect := cliConnections[0]
|
||||
cliConnections[0] = cliConnections[1]
|
||||
cliConnections[1] = connect
|
||||
|
||||
loc := locations[0]
|
||||
locations[0] = locations[1]
|
||||
locations[1] = loc
|
||||
}
|
||||
dest = *locations[1]
|
||||
case len(locations) == 1:
|
||||
switch {
|
||||
case len(locations[0].Image) == 0:
|
||||
return errors.Wrapf(define.ErrInvalidArg, "no source image specified")
|
||||
case len(locations[0].Image) > 0 && !locations[0].Remote && len(locations[0].User) == 0: // if we have podman image scp $IMAGE
|
||||
return errors.Wrapf(define.ErrInvalidArg, "must specify a destination")
|
||||
}
|
||||
}
|
||||
|
||||
source.File = f.Name() // after parsing the arguments, set the file for the save/load
|
||||
dest.File = source.File
|
||||
if err = os.Remove(source.File); err != nil { // remove the file and simply use its name so podman creates the file upon save. avoids umask errors
|
||||
return err
|
||||
}
|
||||
|
||||
var serv map[string]config.Destination
|
||||
serv, err = GetServiceInformation(cliConnections, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Add podman remote support
|
||||
confR.Engine = config.EngineConfig{Remote: true, CgroupManager: "cgroupfs", ServiceDestinations: serv} // pass the service dest (either remote or something else) to engine
|
||||
saveCmd, loadCmd := createCommands(podman)
|
||||
switch {
|
||||
case scpOpts.FromRemote: // if we want to load FROM the remote
|
||||
err = saveToRemote(scpOpts.SourceImageName, scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0])
|
||||
case source.Remote: // if we want to load FROM the remote, dest can either be local or remote in this case
|
||||
err = saveToRemote(source.Image, source.File, "", sshInfo.URI[0], sshInfo.Identities[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if scpOpts.ToRemote { // we want to load remote -> remote
|
||||
rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[1], scpOpts.Iden[1])
|
||||
if dest.Remote { // we want to load remote -> remote, both source and dest are remote
|
||||
rep, err := loadToRemote(dest.File, "", sshInfo.URI[1], sshInfo.Identities[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(rep)
|
||||
break
|
||||
}
|
||||
report, err := abiEng.Load(context.Background(), scpOpts.Load)
|
||||
err = execPodman(podman, loadCmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Loaded image(s): " + strings.Join(report.Names, ","))
|
||||
case scpOpts.ToRemote: // remote host load
|
||||
scpOpts.Save.Format = "oci-archive"
|
||||
abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh
|
||||
if abiErr != nil {
|
||||
errors.Wrapf(abiErr, "could not save image as specified")
|
||||
case dest.Remote: // remote host load, implies source is local
|
||||
err = execPodman(podman, saveCmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
fmt.Println(rep)
|
||||
if err = os.Remove(source.File); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: Add podman remote support
|
||||
default: // else native load
|
||||
scpOpts.Save.Format = "oci-archive"
|
||||
_, err := os.Open(scpOpts.Save.Output)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -249,119 +302,28 @@ func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) {
|
||||
return dialAdd, file, nil
|
||||
}
|
||||
|
||||
// validateImageName makes sure that the image given is valid and no injections are occurring
|
||||
// we simply use this for error checking, bot setting the image
|
||||
func validateImageName(input string) error {
|
||||
// ParseNormalizedNamed transforms a shortname image into its
|
||||
// full name reference so busybox => docker.io/library/busybox
|
||||
// we want to keep our shortnames, so only return an error if
|
||||
// we cannot parse what th euser has given us
|
||||
_, err := reference.ParseNormalizedNamed(input)
|
||||
return err
|
||||
}
|
||||
|
||||
// remoteArgLength is a helper function to simplify the extracting of host argument data
|
||||
// returns an int which contains the length of a specified index in a host::image string
|
||||
func remoteArgLength(input string, side int) int {
|
||||
return len((strings.Split(input, "::"))[side])
|
||||
}
|
||||
|
||||
// parseArgs returns the valid connection data based off of the information provided by the user
|
||||
// args is an array of the command arguments and cfg is tooling configuration used to get service destinations
|
||||
// returned is serv and an error if applicable. serv is a map of service destinations with the connection name as the index
|
||||
// this connection name is intended to be used as EngineConfig.ServiceDestinations
|
||||
// this function modifies the global scpOpt entities: FromRemote, ToRemote, Connections, and SourceImageName
|
||||
func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination, error) {
|
||||
serv := map[string]config.Destination{}
|
||||
cliConnections := []string{}
|
||||
switch len(args) {
|
||||
case 1:
|
||||
if strings.Contains(args[0], "localhost") {
|
||||
if strings.Split(args[0], "@")[0] != "root" {
|
||||
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo")
|
||||
}
|
||||
scpOpts.Rootless = true
|
||||
scpOpts.SourceImageName = strings.Split(args[0], "::")[1]
|
||||
} else if strings.Contains(args[0], "::") {
|
||||
scpOpts.FromRemote = true
|
||||
cliConnections = append(cliConnections, args[0])
|
||||
} else {
|
||||
err := validateImageName(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scpOpts.SourceImageName = args[0]
|
||||
}
|
||||
case 2:
|
||||
if strings.Contains(args[0], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment
|
||||
if strings.Split(args[0], "@")[0] != "root" {
|
||||
return nil, errors.Wrapf(define.ErrInvalidArg, "currently, transferring images to a user account is not supported")
|
||||
}
|
||||
if len(strings.Split(args[0], "::")) > 1 {
|
||||
scpOpts.Rootless = true
|
||||
scpOpts.User = strings.Split(args[1], "@")[0]
|
||||
scpOpts.SourceImageName = strings.Split(args[0], "::")[1]
|
||||
} else {
|
||||
return nil, errors.Wrapf(define.ErrInvalidArg, "currently, you cannot rename images during the transfer or transfer them to a user account")
|
||||
}
|
||||
} else if strings.Contains(args[0], "::") {
|
||||
if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client
|
||||
cliConnections = append(cliConnections, args[0])
|
||||
scpOpts.ToRemote = true
|
||||
scpOpts.SourceImageName = args[1]
|
||||
} else if strings.Contains(args[1], "::") { // both remote clients
|
||||
scpOpts.FromRemote = true
|
||||
scpOpts.ToRemote = true
|
||||
if remoteArgLength(args[0], 1) == 0 { // is save->load w/ one image name
|
||||
cliConnections = append(cliConnections, args[0])
|
||||
cliConnections = append(cliConnections, args[1])
|
||||
} else if remoteArgLength(args[0], 1) > 0 && remoteArgLength(args[1], 1) > 0 {
|
||||
//in the future, this function could, instead of rejecting renames, also set a DestImageName field
|
||||
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
|
||||
} else { // else its a load save (order of args)
|
||||
cliConnections = append(cliConnections, args[1])
|
||||
cliConnections = append(cliConnections, args[0])
|
||||
}
|
||||
} else {
|
||||
//in the future, this function could, instead of rejecting renames, also set a DestImageName field
|
||||
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
|
||||
}
|
||||
} else if strings.Contains(args[1], "::") { // if we are given image host::
|
||||
if remoteArgLength(args[1], 1) > 0 {
|
||||
//in the future, this function could, instead of rejecting renames, also set a DestImageName field
|
||||
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
|
||||
}
|
||||
err := validateImageName(args[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scpOpts.SourceImageName = args[0]
|
||||
scpOpts.ToRemote = true
|
||||
cliConnections = append(cliConnections, args[1])
|
||||
} else {
|
||||
//in the future, this function could, instead of rejecting renames, also set a DestImageName field
|
||||
return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename")
|
||||
}
|
||||
}
|
||||
// GetSerivceInformation takes the parsed list of hosts to connect to and validates the information
|
||||
func GetServiceInformation(cliConnections []string, cfg *config.Config) (map[string]config.Destination, error) {
|
||||
var serv map[string]config.Destination
|
||||
var url string
|
||||
var iden string
|
||||
for i, val := range cliConnections {
|
||||
splitEnv := strings.SplitN(val, "::", 2)
|
||||
scpOpts.Connections = append(scpOpts.Connections, splitEnv[0])
|
||||
sshInfo.Connections = append(sshInfo.Connections, splitEnv[0])
|
||||
if len(splitEnv[1]) != 0 {
|
||||
err := validateImageName(splitEnv[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scpOpts.SourceImageName = splitEnv[1]
|
||||
source.Image = splitEnv[1]
|
||||
//TODO: actually use the new name given by the user
|
||||
}
|
||||
conn, found := cfg.Engine.ServiceDestinations[scpOpts.Connections[i]]
|
||||
conn, found := cfg.Engine.ServiceDestinations[sshInfo.Connections[i]]
|
||||
if found {
|
||||
url = conn.URI
|
||||
iden = conn.Identity
|
||||
} else { // no match, warn user and do a manual connection.
|
||||
url = "ssh://" + scpOpts.Connections[i]
|
||||
url = "ssh://" + sshInfo.Connections[i]
|
||||
iden = ""
|
||||
logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location")
|
||||
}
|
||||
@ -374,8 +336,45 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
scpOpts.URI = append(scpOpts.URI, urlT)
|
||||
scpOpts.Iden = append(scpOpts.Iden, iden)
|
||||
sshInfo.URI = append(sshInfo.URI, urlT)
|
||||
sshInfo.Identities = append(sshInfo.Identities, iden)
|
||||
}
|
||||
return serv, nil
|
||||
}
|
||||
|
||||
// execPodman executes the podman save/load command given the podman binary
|
||||
func execPodman(podman string, command []string) error {
|
||||
if rootless.IsRootless() {
|
||||
cmd := exec.Command(podman)
|
||||
utils.CreateSCPCommand(cmd, command[1:])
|
||||
logrus.Debug("Executing podman command")
|
||||
return cmd.Run()
|
||||
}
|
||||
machinectl, err := exec.LookPath("machinectl")
|
||||
if err != nil {
|
||||
cmd := exec.Command("su", "-l", "root", "--command")
|
||||
cmd = utils.CreateSCPCommand(cmd, []string{strings.Join(command, " ")})
|
||||
return cmd.Run()
|
||||
}
|
||||
cmd := exec.Command(machinectl, "shell", "-q", "root@.host")
|
||||
cmd = utils.CreateSCPCommand(cmd, command)
|
||||
logrus.Debug("Executing load command machinectl")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// createCommands forms the podman save and load commands used by SCP
|
||||
func createCommands(podman string) ([]string, []string) {
|
||||
var parentString string
|
||||
quiet := ""
|
||||
if source.Quiet {
|
||||
quiet = "-q "
|
||||
}
|
||||
if len(parentFlags) > 0 {
|
||||
parentString = strings.Join(parentFlags, " ") + " " // if there are parent args, an extra space needs to be added
|
||||
} else {
|
||||
parentString = strings.Join(parentFlags, " ")
|
||||
}
|
||||
loadCmd := strings.Split(fmt.Sprintf("%s %sload %s--input %s", podman, parentString, quiet, dest.File), " ")
|
||||
saveCmd := strings.Split(fmt.Sprintf("%s %vsave %s--output %s %s", podman, parentString, quiet, source.File, source.Image), " ")
|
||||
return saveCmd, loadCmd
|
||||
}
|
||||
|
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
|
||||
_, found := c.Command.Annotations[registry.UnshareNSRequired]
|
||||
if found {
|
||||
if rootless.IsRootless() && os.Getuid() != 0 {
|
||||
if rootless.IsRootless() && os.Getuid() != 0 && c.Command.Name() != "scp" {
|
||||
c.Command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("cannot run command %q in rootless mode, must execute `podman unshare` first", cmd.CommandPath())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, found = c.Command.Annotations[registry.ParentNSRequired]
|
||||
if rootless.IsRootless() && found {
|
||||
if rootless.IsRootless() && found && c.Command.Name() != "scp" {
|
||||
c.Command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath())
|
||||
}
|
||||
|
@ -165,6 +165,7 @@ setup_rootless() {
|
||||
groupadd -g $rootless_gid $ROOTLESS_USER
|
||||
useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER
|
||||
chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC"
|
||||
echo "$ROOTLESS_USER ALL=(root) NOPASSWD: ALL" > /etc/sudoers.d/ci-rootless
|
||||
|
||||
mkdir -p "$HOME/.ssh" "/home/$ROOTLESS_USER/.ssh"
|
||||
|
||||
|
@ -27,7 +27,7 @@ type ImageEngine interface {
|
||||
ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error)
|
||||
Shutdown(ctx context.Context)
|
||||
Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error
|
||||
Transfer(ctx context.Context, scpOpts ImageScpOptions) error
|
||||
Transfer(ctx context.Context, source ImageScpOptions, dest ImageScpOptions, parentFlags []string) error
|
||||
Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error)
|
||||
Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error)
|
||||
Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error
|
||||
|
@ -311,30 +311,28 @@ type ImageSaveOptions struct {
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
// ImageScpOptions provide options for securely copying images to podman remote
|
||||
// ImageScpOptions provide options for securely copying images to and from a remote host
|
||||
type ImageScpOptions struct {
|
||||
// SoureImageName is the image the user is providing to load on a remote machine
|
||||
SourceImageName string
|
||||
// Tag allows for a new image to be created under the given name
|
||||
Tag string
|
||||
// ToRemote specifies that we are loading to the remote host
|
||||
ToRemote bool
|
||||
// FromRemote specifies that we are loading from the remote host
|
||||
FromRemote bool
|
||||
// Remote determines if this entity is operating on a remote machine
|
||||
Remote bool `json:"remote,omitempty"`
|
||||
// File is the input/output file for the save and load Operation
|
||||
File string `json:"file,omitempty"`
|
||||
// Quiet Determines if the save and load operation will be done quietly
|
||||
Quiet bool `json:"quiet,omitempty"`
|
||||
// Image is the image the user is providing to save and load
|
||||
Image string `json:"image,omitempty"`
|
||||
// User is used in conjunction with Transfer to determine if a valid user was given to save from/load into
|
||||
User string `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// ImageScpConnections provides the ssh related information used in remote image transfer
|
||||
type ImageScpConnections struct {
|
||||
// Connections holds the raw string values for connections (ssh or unix)
|
||||
Connections []string
|
||||
// URI contains the ssh connection URLs to be used by the client
|
||||
URI []*url.URL
|
||||
// Iden contains ssh identity keys to be used by the client
|
||||
Iden []string
|
||||
// Save Options used for first half of the scp operation
|
||||
Save ImageSaveOptions
|
||||
// Load options used for the second half of the scp operation
|
||||
Load ImageLoadOptions
|
||||
// Rootless determines whether we are loading locally from root storage to rootless storage
|
||||
Rootless bool
|
||||
// User is used in conjunction with Rootless to determine which user to use to obtain the uid
|
||||
User string
|
||||
// Identities contains ssh identity keys to be used by the client
|
||||
Identities []string
|
||||
}
|
||||
|
||||
// ImageTreeOptions provides options for ImageEngine.Tree()
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
domainUtils "github.com/containers/podman/v3/pkg/domain/utils"
|
||||
"github.com/containers/podman/v3/pkg/errorhandling"
|
||||
"github.com/containers/podman/v3/pkg/rootless"
|
||||
"github.com/containers/podman/v3/utils"
|
||||
"github.com/containers/storage"
|
||||
dockerRef "github.com/docker/distribution/reference"
|
||||
"github.com/opencontainers/go-digest"
|
||||
@ -351,65 +352,19 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
|
||||
return pushError
|
||||
}
|
||||
|
||||
// Transfer moves images from root to rootless storage so the user specified in the scp call can access and use the image modified by root
|
||||
func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error {
|
||||
if scpOpts.User == "" {
|
||||
// Transfer moves images between root and rootless storage so the user specified in the scp call can access and use the image modified by root
|
||||
func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error {
|
||||
if source.User == "" {
|
||||
return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage")
|
||||
}
|
||||
var u *user.User
|
||||
scpOpts.User = strings.Split(scpOpts.User, ":")[0] // split in case provided with uid:gid
|
||||
_, err := strconv.Atoi(scpOpts.User)
|
||||
if err != nil {
|
||||
u, err = user.Lookup(scpOpts.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
u, err = user.LookupId(scpOpts.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
uid, err := strconv.Atoi(u.Uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid, err := strconv.Atoi(u.Gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chown(scpOpts.Save.Output, uid, gid) // chown the output because was created by root so we need to give th euser read access
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podman, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
machinectl, err := exec.LookPath("machinectl")
|
||||
if err != nil {
|
||||
logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available")
|
||||
cmd := exec.Command("su", "-l", u.Username, "--command", podman+" --log-level="+logrus.GetLevel().String()+" --cgroup-manager=cgroupfs load --input="+scpOpts.Save.Output) // load the new image to the rootless storage
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
logrus.Debug("Executing load command su")
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
cmd := exec.Command(machinectl, "shell", "-q", u.Username+"@.host", podman, "--log-level="+logrus.GetLevel().String(), "--cgroup-manager=cgroupfs", "load", "--input", scpOpts.Save.Output) // load the new image to the rootless storage
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
logrus.Debug("Executing load command machinectl")
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return transferRootful(source, dest, podman, parentFlags)
|
||||
}
|
||||
|
||||
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
|
||||
@ -786,3 +741,123 @@ func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStore
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users
|
||||
func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error {
|
||||
var cmdSave *exec.Cmd
|
||||
saveCommand := parentFlags
|
||||
saveCommand = append(saveCommand, []string{"save", "--output", source.File, source.Image}...)
|
||||
|
||||
loadCommand := parentFlags
|
||||
loadCommand = append(loadCommand, []string{"load", "--input", dest.File}...)
|
||||
|
||||
if source.User == "root" {
|
||||
cmdSave = exec.Command("sudo", podman)
|
||||
} else {
|
||||
cmdSave = exec.Command(podman)
|
||||
}
|
||||
cmdSave = utils.CreateSCPCommand(cmdSave, saveCommand)
|
||||
logrus.Debug("Executing save command")
|
||||
err := cmdSave.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cmdLoad *exec.Cmd
|
||||
if source.User != "root" {
|
||||
cmdLoad = exec.Command("sudo", podman)
|
||||
} else {
|
||||
cmdLoad = exec.Command(podman)
|
||||
}
|
||||
cmdLoad = utils.CreateSCPCommand(cmdLoad, loadCommand)
|
||||
logrus.Debug("Executing load command")
|
||||
err = cmdLoad.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TransferRootless creates new podman processes using exec.Command and su/machinectl, transferring images between the given source and destination users
|
||||
func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error {
|
||||
basicCommand := []string{podman}
|
||||
basicCommand = append(basicCommand, parentFlags...)
|
||||
saveCommand := append(basicCommand, []string{"save", "--output", source.File, source.Image}...)
|
||||
loadCommand := append(basicCommand, []string{"load", "--input", dest.File}...)
|
||||
save := []string{strings.Join(saveCommand, " ")}
|
||||
load := []string{strings.Join(loadCommand, " ")}
|
||||
|
||||
// if executing using sudo or transferring between two users, the TransferRootless approach will not work, default to using machinectl or su as necessary.
|
||||
// the approach using sudo is preferable and more straightforward. There is no reason for using sudo in these situations
|
||||
// since the feature is meant to transfer from root to rootless an vice versa without explicit sudo evocaiton.
|
||||
var uSave *user.User
|
||||
var uLoad *user.User
|
||||
var err error
|
||||
source.User = strings.Split(source.User, ":")[0] // split in case provided with uid:gid
|
||||
dest.User = strings.Split(dest.User, ":")[0]
|
||||
uSave, err = lookupUser(source.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case dest.User != "": // if we are given a destination user, check that first
|
||||
uLoad, err = lookupUser(dest.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case uSave.Name != "root": // else if we have no destination user, and source is not root that means we should be root
|
||||
uLoad, err = user.LookupId("0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default: // else if we have no dest user, and source user IS root, we want to be the default user.
|
||||
uString := os.Getenv("SUDO_USER")
|
||||
if uString == "" {
|
||||
return errors.New("$SUDO_USER must be defined to find the default rootless user")
|
||||
}
|
||||
uLoad, err = user.Lookup(uString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
machinectl, err := exec.LookPath("machinectl")
|
||||
if err != nil {
|
||||
logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available")
|
||||
err = execSu(uSave, save)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return execSu(uLoad, load)
|
||||
}
|
||||
err = execMachine(uSave, saveCommand, machinectl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return execMachine(uLoad, loadCommand, machinectl)
|
||||
}
|
||||
|
||||
func lookupUser(u string) (*user.User, error) {
|
||||
if u, err := user.LookupId(u); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
return user.Lookup(u)
|
||||
}
|
||||
|
||||
func execSu(execUser *user.User, command []string) error {
|
||||
cmd := exec.Command("su", "-l", execUser.Username, "--command")
|
||||
cmd = utils.CreateSCPCommand(cmd, command)
|
||||
logrus.Debug("Executing command su")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func execMachine(execUser *user.User, command []string, machinectl string) error {
|
||||
var cmd *exec.Cmd
|
||||
if execUser.Uid == "0" {
|
||||
cmd = exec.Command("sudo", machinectl, "shell", "-q", execUser.Username+"@.host")
|
||||
} else {
|
||||
cmd = exec.Command(machinectl, "shell", "-q", execUser.Username+"@.host")
|
||||
}
|
||||
cmd = utils.CreateSCPCommand(cmd, command)
|
||||
logrus.Debug("Executing command machinectl")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities.
|
||||
return &entities.ImagePullReport{Images: pulledImages}, nil
|
||||
}
|
||||
|
||||
func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error {
|
||||
func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error {
|
||||
return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage")
|
||||
}
|
||||
|
||||
|
@ -244,7 +244,7 @@ can_use_shortcut ()
|
||||
|
||||
if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 ||
|
||||
strcmp (argv[argc], "image") == 0) &&
|
||||
strcmp (argv[argc+1], "mount") == 0)
|
||||
(strcmp (argv[argc+1], "mount") == 0 || strcmp (argv[argc+1], "scp") == 0))
|
||||
{
|
||||
ret = false;
|
||||
break;
|
||||
|
@ -29,7 +29,6 @@ var _ = Describe("podman image scp", func() {
|
||||
panic(err)
|
||||
}
|
||||
os.Setenv("CONTAINERS_CONF", conf.Name())
|
||||
|
||||
tempdir, err = CreateTempDirInTempDir()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
@ -52,38 +51,6 @@ var _ = Describe("podman image scp", func() {
|
||||
|
||||
})
|
||||
|
||||
It("podman image scp quiet flag", func() {
|
||||
if IsRemote() {
|
||||
Skip("this test is only for non-remote")
|
||||
}
|
||||
scp := podmanTest.Podman([]string{"image", "scp", "-q", ALPINE})
|
||||
scp.WaitWithDefaultTimeout()
|
||||
Expect(scp).To(Exit(0))
|
||||
})
|
||||
|
||||
It("podman image scp root to rootless transfer", func() {
|
||||
SkipIfNotRootless("this is a rootless only test, transferring from root to rootless using PodmanAsUser")
|
||||
if IsRemote() {
|
||||
Skip("this test is only for non-remote")
|
||||
}
|
||||
env := os.Environ()
|
||||
img := podmanTest.PodmanAsUser([]string{"image", "pull", ALPINE}, 0, 0, "", env) // pull image to root
|
||||
img.WaitWithDefaultTimeout()
|
||||
Expect(img).To(Exit(0))
|
||||
scp := podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE, "1000:1000@localhost::"}, 0, 0, "", env) //transfer from root to rootless (us)
|
||||
scp.WaitWithDefaultTimeout()
|
||||
Expect(scp).To(Exit(0))
|
||||
|
||||
list := podmanTest.Podman([]string{"image", "list"}) // our image should now contain alpine loaded in from root
|
||||
list.WaitWithDefaultTimeout()
|
||||
Expect(list).To(Exit(0))
|
||||
Expect(list.OutputToStringArray()).To(ContainElement(HavePrefix("quay.io/libpod/alpine")))
|
||||
|
||||
scp = podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE}, 0, 0, "", env) //transfer from root to rootless (us)
|
||||
scp.WaitWithDefaultTimeout()
|
||||
Expect(scp).To(Exit(0))
|
||||
})
|
||||
|
||||
It("podman image scp bogus image", func() {
|
||||
if IsRemote() {
|
||||
Skip("this test is only for non-remote")
|
||||
@ -119,11 +86,8 @@ var _ = Describe("podman image scp", func() {
|
||||
scp.Wait(45)
|
||||
// exit with error because we cannot make an actual ssh connection
|
||||
// This tests that the input we are given is validated and prepared correctly
|
||||
// Error: failed to connect: dial tcp: address foo: missing port in address
|
||||
// The error given should either be a missing image (due to testing suite complications) or a i/o timeout on ssh
|
||||
Expect(scp).To(ExitWithError())
|
||||
Expect(scp.ErrorToString()).To(ContainSubstring(
|
||||
"Error: failed to connect: dial tcp 66.151.147.142:2222: i/o timeout",
|
||||
))
|
||||
|
||||
})
|
||||
|
||||
|
@ -78,6 +78,35 @@ verify_iid_and_name() {
|
||||
run_podman rmi $fqin
|
||||
}
|
||||
|
||||
@test "podman image scp transfer" {
|
||||
skip_if_root_ubuntu "cannot create a new user successfully on ubuntu"
|
||||
get_iid_and_name
|
||||
if ! is_remote; then
|
||||
if is_rootless; then
|
||||
whoami=$(id -un)
|
||||
run_podman image scp $whoami@localhost::$iid root@localhost::
|
||||
if [ "$status" -ne 0 ]; then
|
||||
die "Command failed: podman image scp transfer"
|
||||
fi
|
||||
whoami=$(id -un)
|
||||
run_podman image scp -q $whoami@localhost::$iid root@localhost::
|
||||
if [ "$status" -ne 0 ]; then
|
||||
die "Command failed: podman image scp quiet transfer failed"
|
||||
fi
|
||||
fi
|
||||
if ! is_rootless; then
|
||||
id -u 1000 &>/dev/null || useradd -u 1000 -g 1000 testingUsr
|
||||
if [ "$status" -ne 0 ]; then
|
||||
die "Command failed: useradd 1000"
|
||||
fi
|
||||
run_podman image scp root@localhost::$iid 1000:1000@localhost::
|
||||
if [ "$status" -ne 0 ]; then
|
||||
die "Command failed: podman image scp transfer"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@test "podman load - by image ID" {
|
||||
# FIXME: how to build a simple archive instead?
|
||||
|
@ -11,7 +11,7 @@ function setup() {
|
||||
# TL;DR they keep fixing it then breaking it again. There's a test we
|
||||
# could run to see if it's fixed, but it's way too complicated. Since
|
||||
# integration tests also skip checkpoint tests on Ubuntu, do the same here.
|
||||
if grep -qiw ubuntu /etc/os-release; then
|
||||
if is_ubuntu; then
|
||||
skip "FIXME: checkpointing broken in Ubuntu 2004, 2104, 2110, ..."
|
||||
fi
|
||||
|
||||
|
@ -318,6 +318,10 @@ function wait_for_port() {
|
||||
# BEGIN miscellaneous tools
|
||||
|
||||
# Shortcuts for common needs:
|
||||
function is_ubuntu() {
|
||||
grep -qiw ubuntu /etc/os-release
|
||||
}
|
||||
|
||||
function is_rootless() {
|
||||
[ "$(id -u)" -ne 0 ]
|
||||
}
|
||||
@ -449,6 +453,16 @@ function skip_if_journald_unavailable {
|
||||
fi
|
||||
}
|
||||
|
||||
function skip_if_root_ubuntu {
|
||||
if is_ubuntu; then
|
||||
if ! is_remote; then
|
||||
if ! is_rootless; then
|
||||
skip "Cannot run this test on rootful ubuntu, usually due to user errors"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
#########
|
||||
# die # Abort with helpful message
|
||||
#########
|
||||
|
@ -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