diff options
31 files changed, 789 insertions, 351 deletions
diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index f02c5713b..32d227e65 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -540,14 +540,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(secretFlagName, AutocompleteSecrets) - securityOptFlagName := "security-opt" - createFlags.StringArrayVar( - &cf.SecurityOpt, - securityOptFlagName, []string{}, - "Security Options", - ) - _ = cmd.RegisterFlagCompletionFunc(securityOptFlagName, AutocompleteSecurityOption) - shmSizeFlagName := "shm-size" createFlags.String( shmSizeFlagName, shmSize(), @@ -720,6 +712,13 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, `If a container with the same name exists, replace it`, ) } + securityOptFlagName := "security-opt" + createFlags.StringArrayVar( + &cf.SecurityOpt, + securityOptFlagName, []string{}, + "Security Options", + ) + _ = cmd.RegisterFlagCompletionFunc(securityOptFlagName, AutocompleteSecurityOption) subgidnameFlagName := "subgidname" createFlags.StringVar( @@ -890,6 +889,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, "Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)", ) _ = cmd.RegisterFlagCompletionFunc(deviceReadBpsFlagName, completion.AutocompleteDefault) + volumesFromFlagName := "volumes-from" createFlags.StringArrayVar( &cf.VolumesFrom, diff --git a/cmd/podman/images/scp.go b/cmd/podman/images/scp.go index 5c9cadc7a..f02a3c15e 100644 --- a/cmd/podman/images/scp.go +++ b/cmd/podman/images/scp.go @@ -6,18 +6,19 @@ import ( "io/ioutil" urlP "net/url" "os" + "os/exec" + "os/user" "strconv" "strings" "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/cmd/podman/common" - "github.com/containers/podman/v3/cmd/podman/parse" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/system/connection" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/rootless" - "github.com/docker/distribution/reference" + "github.com/containers/podman/v3/utils" scpD "github.com/dtylman/scp" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -28,8 +29,12 @@ import ( var ( saveScpDescription = `Securely copy an image from one host to another.` imageScpCommand = &cobra.Command{ - Use: "scp [options] IMAGE [HOST::]", - Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + Use: "scp [options] IMAGE [HOST::]", + Annotations: map[string]string{ + registry.UnshareNSRequired: "", + registry.ParentNSRequired: "", + registry.EngineMode: registry.ABIMode, + }, Long: saveScpDescription, Short: "securely copy images", RunE: scp, @@ -40,7 +45,10 @@ var ( ) var ( - scpOpts entities.ImageScpOptions + parentFlags []string + source entities.ImageScpOptions + dest entities.ImageScpOptions + sshInfo entities.ImageScpConnections ) func init() { @@ -53,7 +61,7 @@ func init() { func scpFlags(cmd *cobra.Command) { flags := cmd.Flags() - flags.BoolVarP(&scpOpts.Save.Quiet, "quiet", "q", false, "Suppress the output") + flags.BoolVarP(&source.Quiet, "quiet", "q", false, "Suppress the output") } func scp(cmd *cobra.Command, args []string) (finalErr error) { @@ -61,24 +69,31 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { // TODO add tag support for images err error ) - if scpOpts.Save.Quiet { // set quiet for both load and save - scpOpts.Load.Quiet = true + for i, val := range os.Args { + if val == "image" { + break + } + if i == 0 { + continue + } + if strings.Contains(val, "CIRRUS") { // need to skip CIRRUS flags for testing suite purposes + continue + } + parentFlags = append(parentFlags, val) } - f, err := ioutil.TempFile("", "podman") // open temp file for load/save output + podman, err := os.Executable() if err != nil { return err } - defer os.Remove(f.Name()) - - scpOpts.Save.Output = f.Name() - scpOpts.Load.Input = scpOpts.Save.Output - if err := parse.ValidateFileName(saveOpts.Output); err != nil { + f, err := ioutil.TempFile("", "podman") // open temp file for load/save output + if err != nil { return err } confR, err := config.NewConfig("") // create a hand made config for the remote engine since we might use remote and native at once if err != nil { return errors.Wrapf(err, "could not make config") } + abiEng, err := registry.NewImageEngine(cmd, args) // abi native engine if err != nil { return err @@ -88,77 +103,115 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { if err != nil { return err } - serv, err := parseArgs(args, cfg) // parses connection data and "which way" we are loading and saving + locations := []*entities.ImageScpOptions{} + cliConnections := []string{} + flipConnections := false + for _, arg := range args { + loc, connect, err := parseImageSCPArg(arg) + if err != nil { + return err + } + locations = append(locations, loc) + cliConnections = append(cliConnections, connect...) + } + source = *locations[0] + switch { + case len(locations) > 1: + if flipConnections, err = validateSCPArgs(locations); err != nil { + return err + } + if flipConnections { // the order of cliConnections matters, we need to flip both arrays since the args are parsed separately sometimes. + connect := cliConnections[0] + cliConnections[0] = cliConnections[1] + cliConnections[1] = connect + + loc := locations[0] + locations[0] = locations[1] + locations[1] = loc + } + dest = *locations[1] + case len(locations) == 1: + switch { + case len(locations[0].Image) == 0: + return errors.Wrapf(define.ErrInvalidArg, "no source image specified") + case len(locations[0].Image) > 0 && !locations[0].Remote && len(locations[0].User) == 0: // if we have podman image scp $IMAGE + return errors.Wrapf(define.ErrInvalidArg, "must specify a destination") + } + } + + source.File = f.Name() // after parsing the arguments, set the file for the save/load + dest.File = source.File + if err = os.Remove(source.File); err != nil { // remove the file and simply use its name so podman creates the file upon save. avoids umask errors + return err + } + + var serv map[string]config.Destination + serv, err = GetServiceInformation(cliConnections, cfg) if err != nil { return err } + // TODO: Add podman remote support confR.Engine = config.EngineConfig{Remote: true, CgroupManager: "cgroupfs", ServiceDestinations: serv} // pass the service dest (either remote or something else) to engine + saveCmd, loadCmd := createCommands(podman) switch { - case scpOpts.FromRemote: // if we want to load FROM the remote - err = saveToRemote(scpOpts.SourceImageName, scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0]) + case source.Remote: // if we want to load FROM the remote, dest can either be local or remote in this case + err = saveToRemote(source.Image, source.File, "", sshInfo.URI[0], sshInfo.Identities[0]) if err != nil { return err } - if scpOpts.ToRemote { // we want to load remote -> remote - rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[1], scpOpts.Iden[1]) + if dest.Remote { // we want to load remote -> remote, both source and dest are remote + rep, err := loadToRemote(dest.File, "", sshInfo.URI[1], sshInfo.Identities[1]) if err != nil { return err } fmt.Println(rep) break } - report, err := abiEng.Load(context.Background(), scpOpts.Load) + err = execPodman(podman, loadCmd) if err != nil { return err } - fmt.Println("Loaded image(s): " + strings.Join(report.Names, ",")) - case scpOpts.ToRemote: // remote host load - scpOpts.Save.Format = "oci-archive" - abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh - if abiErr != nil { - errors.Wrapf(abiErr, "could not save image as specified") - } - rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0]) + case dest.Remote: // remote host load, implies source is local + err = execPodman(podman, saveCmd) if err != nil { return err } - fmt.Println(rep) - // TODO: Add podman remote support - default: // else native load - scpOpts.Save.Format = "oci-archive" - _, err := os.Open(scpOpts.Save.Output) + rep, err := loadToRemote(source.File, "", sshInfo.URI[0], sshInfo.Identities[0]) if err != nil { return err } - if scpOpts.Tag != "" { - return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported") - } - scpOpts.Save.Format = "oci-archive" - abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh - if abiErr != nil { - return errors.Wrapf(abiErr, "could not save image as specified") + fmt.Println(rep) + if err = os.Remove(source.File); err != nil { + return err } - if !rootless.IsRootless() && scpOpts.Rootless { - if scpOpts.User == "" { - scpOpts.User = os.Getenv("SUDO_USER") - if scpOpts.User == "" { - return errors.New("could not obtain root user, make sure the environmental variable SUDO_USER is set, and that this command is being run as root") + // TODO: Add podman remote support + default: // else native load, both source and dest are local and transferring between users + if source.User == "" { // source user has to be set, destination does not + source.User = os.Getenv("USER") + if source.User == "" { + u, err := user.Current() + if err != nil { + return errors.Wrapf(err, "could not obtain user, make sure the environmental variable $USER is set") } + source.User = u.Username } - err := abiEng.Transfer(context.Background(), scpOpts) - if err != nil { - return err - } - } else { - rep, err := abiEng.Load(context.Background(), scpOpts.Load) - if err != nil { - return err - } - fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ",")) + } + err := abiEng.Transfer(context.Background(), source, dest, parentFlags) + if err != nil { + return err } } + src, err := json.MarshalIndent(source, "", " ") + if err != nil { + return err + } + dst, err := json.MarshalIndent(dest, "", " ") + if err != nil { + return err + } + fmt.Printf("SOURCE: %s\nDEST: %s\n", string(src), string(dst)) return nil } @@ -249,119 +302,28 @@ func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) { return dialAdd, file, nil } -// validateImageName makes sure that the image given is valid and no injections are occurring -// we simply use this for error checking, bot setting the image -func validateImageName(input string) error { - // ParseNormalizedNamed transforms a shortname image into its - // full name reference so busybox => docker.io/library/busybox - // we want to keep our shortnames, so only return an error if - // we cannot parse what th euser has given us - _, err := reference.ParseNormalizedNamed(input) - return err -} - -// remoteArgLength is a helper function to simplify the extracting of host argument data -// returns an int which contains the length of a specified index in a host::image string -func remoteArgLength(input string, side int) int { - return len((strings.Split(input, "::"))[side]) -} - -// parseArgs returns the valid connection data based off of the information provided by the user -// args is an array of the command arguments and cfg is tooling configuration used to get service destinations -// returned is serv and an error if applicable. serv is a map of service destinations with the connection name as the index -// this connection name is intended to be used as EngineConfig.ServiceDestinations -// this function modifies the global scpOpt entities: FromRemote, ToRemote, Connections, and SourceImageName -func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination, error) { - serv := map[string]config.Destination{} - cliConnections := []string{} - switch len(args) { - case 1: - if strings.Contains(args[0], "localhost") { - if strings.Split(args[0], "@")[0] != "root" { - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo") - } - scpOpts.Rootless = true - scpOpts.SourceImageName = strings.Split(args[0], "::")[1] - } else if strings.Contains(args[0], "::") { - scpOpts.FromRemote = true - cliConnections = append(cliConnections, args[0]) - } else { - err := validateImageName(args[0]) - if err != nil { - return nil, err - } - scpOpts.SourceImageName = args[0] - } - case 2: - if strings.Contains(args[0], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment - if strings.Split(args[0], "@")[0] != "root" { - return nil, errors.Wrapf(define.ErrInvalidArg, "currently, transferring images to a user account is not supported") - } - if len(strings.Split(args[0], "::")) > 1 { - scpOpts.Rootless = true - scpOpts.User = strings.Split(args[1], "@")[0] - scpOpts.SourceImageName = strings.Split(args[0], "::")[1] - } else { - return nil, errors.Wrapf(define.ErrInvalidArg, "currently, you cannot rename images during the transfer or transfer them to a user account") - } - } else if strings.Contains(args[0], "::") { - if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client - cliConnections = append(cliConnections, args[0]) - scpOpts.ToRemote = true - scpOpts.SourceImageName = args[1] - } else if strings.Contains(args[1], "::") { // both remote clients - scpOpts.FromRemote = true - scpOpts.ToRemote = true - if remoteArgLength(args[0], 1) == 0 { // is save->load w/ one image name - cliConnections = append(cliConnections, args[0]) - cliConnections = append(cliConnections, args[1]) - } else if remoteArgLength(args[0], 1) > 0 && remoteArgLength(args[1], 1) > 0 { - //in the future, this function could, instead of rejecting renames, also set a DestImageName field - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") - } else { // else its a load save (order of args) - cliConnections = append(cliConnections, args[1]) - cliConnections = append(cliConnections, args[0]) - } - } else { - //in the future, this function could, instead of rejecting renames, also set a DestImageName field - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") - } - } else if strings.Contains(args[1], "::") { // if we are given image host:: - if remoteArgLength(args[1], 1) > 0 { - //in the future, this function could, instead of rejecting renames, also set a DestImageName field - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") - } - err := validateImageName(args[0]) - if err != nil { - return nil, err - } - scpOpts.SourceImageName = args[0] - scpOpts.ToRemote = true - cliConnections = append(cliConnections, args[1]) - } else { - //in the future, this function could, instead of rejecting renames, also set a DestImageName field - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") - } - } +// GetSerivceInformation takes the parsed list of hosts to connect to and validates the information +func GetServiceInformation(cliConnections []string, cfg *config.Config) (map[string]config.Destination, error) { + var serv map[string]config.Destination var url string var iden string for i, val := range cliConnections { splitEnv := strings.SplitN(val, "::", 2) - scpOpts.Connections = append(scpOpts.Connections, splitEnv[0]) + sshInfo.Connections = append(sshInfo.Connections, splitEnv[0]) if len(splitEnv[1]) != 0 { err := validateImageName(splitEnv[1]) if err != nil { return nil, err } - scpOpts.SourceImageName = splitEnv[1] + source.Image = splitEnv[1] //TODO: actually use the new name given by the user } - conn, found := cfg.Engine.ServiceDestinations[scpOpts.Connections[i]] + conn, found := cfg.Engine.ServiceDestinations[sshInfo.Connections[i]] if found { url = conn.URI iden = conn.Identity } else { // no match, warn user and do a manual connection. - url = "ssh://" + scpOpts.Connections[i] + url = "ssh://" + sshInfo.Connections[i] iden = "" logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location") } @@ -374,8 +336,45 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination return nil, err } } - scpOpts.URI = append(scpOpts.URI, urlT) - scpOpts.Iden = append(scpOpts.Iden, iden) + sshInfo.URI = append(sshInfo.URI, urlT) + sshInfo.Identities = append(sshInfo.Identities, iden) } return serv, nil } + +// execPodman executes the podman save/load command given the podman binary +func execPodman(podman string, command []string) error { + if rootless.IsRootless() { + cmd := exec.Command(podman) + utils.CreateSCPCommand(cmd, command[1:]) + logrus.Debug("Executing podman command") + return cmd.Run() + } + machinectl, err := exec.LookPath("machinectl") + if err != nil { + cmd := exec.Command("su", "-l", "root", "--command") + cmd = utils.CreateSCPCommand(cmd, []string{strings.Join(command, " ")}) + return cmd.Run() + } + cmd := exec.Command(machinectl, "shell", "-q", "root@.host") + cmd = utils.CreateSCPCommand(cmd, command) + logrus.Debug("Executing load command machinectl") + return cmd.Run() +} + +// createCommands forms the podman save and load commands used by SCP +func createCommands(podman string) ([]string, []string) { + var parentString string + quiet := "" + if source.Quiet { + quiet = "-q " + } + if len(parentFlags) > 0 { + parentString = strings.Join(parentFlags, " ") + " " // if there are parent args, an extra space needs to be added + } else { + parentString = strings.Join(parentFlags, " ") + } + loadCmd := strings.Split(fmt.Sprintf("%s %sload %s--input %s", podman, parentString, quiet, dest.File), " ") + saveCmd := strings.Split(fmt.Sprintf("%s %vsave %s--output %s %s", podman, parentString, quiet, source.File, source.Image), " ") + return saveCmd, loadCmd +} diff --git a/cmd/podman/images/scp_test.go b/cmd/podman/images/scp_test.go new file mode 100644 index 000000000..d4d8f8e58 --- /dev/null +++ b/cmd/podman/images/scp_test.go @@ -0,0 +1,46 @@ +package images + +import ( + "testing" + + "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/stretchr/testify/assert" +) + +func TestParseSCPArgs(t *testing.T) { + args := []string{"alpine", "root@localhost::"} + var source *entities.ImageScpOptions + var dest *entities.ImageScpOptions + var err error + source, _, err = parseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.Equal(t, source.Image, "alpine") + + dest, _, err = parseImageSCPArg(args[1]) + assert.Nil(t, err) + assert.Equal(t, dest.Image, "") + assert.Equal(t, dest.User, "root") + + args = []string{"root@localhost::alpine"} + source, _, err = parseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.Equal(t, source.User, "root") + assert.Equal(t, source.Image, "alpine") + + args = []string{"charliedoern@192.168.68.126::alpine", "foobar@192.168.68.126::"} + source, _, err = parseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.True(t, source.Remote) + assert.Equal(t, source.Image, "alpine") + + dest, _, err = parseImageSCPArg(args[1]) + assert.Nil(t, err) + assert.True(t, dest.Remote) + assert.Equal(t, dest.Image, "") + + args = []string{"charliedoern@192.168.68.126::alpine"} + source, _, err = parseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.True(t, source.Remote) + assert.Equal(t, source.Image, "alpine") +} diff --git a/cmd/podman/images/scp_utils.go b/cmd/podman/images/scp_utils.go new file mode 100644 index 000000000..ebb874c1c --- /dev/null +++ b/cmd/podman/images/scp_utils.go @@ -0,0 +1,87 @@ +package images + +import ( + "strings" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/pkg/errors" +) + +// parseImageSCPArg returns the valid connection, and source/destination data based off of the information provided by the user +// arg is a string containing one of the cli arguments returned is a filled out source/destination options structs as well as a connections array and an error if applicable +func parseImageSCPArg(arg string) (*entities.ImageScpOptions, []string, error) { + location := entities.ImageScpOptions{} + var err error + cliConnections := []string{} + + switch { + case strings.Contains(arg, "@localhost"): // image transfer between users + location.User = strings.Split(arg, "@")[0] + location, err = validateImagePortion(location, arg) + if err != nil { + return nil, nil, err + } + case strings.Contains(arg, "::"): + location, err = validateImagePortion(location, arg) + if err != nil { + return nil, nil, err + } + location.Remote = true + cliConnections = append(cliConnections, arg) + default: + location.Image = arg + } + return &location, cliConnections, nil +} + +// validateImagePortion is a helper function to validate the image name in an SCP argument +func validateImagePortion(location entities.ImageScpOptions, arg string) (entities.ImageScpOptions, error) { + if remoteArgLength(arg, 1) > 0 { + err := validateImageName(strings.Split(arg, "::")[1]) + if err != nil { + return location, err + } + location.Image = strings.Split(arg, "::")[1] // this will get checked/set again once we validate connections + } + return location, nil +} + +// validateSCPArgs takes the array of source and destination options and checks for common errors +func validateSCPArgs(locations []*entities.ImageScpOptions) (bool, error) { + if len(locations) > 2 { + return false, errors.Wrapf(define.ErrInvalidArg, "cannot specify more than two arguments") + } + switch { + case len(locations[0].Image) > 0 && len(locations[1].Image) > 0: + return false, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") + case len(locations[0].Image) == 0 && len(locations[1].Image) == 0: + return false, errors.Wrapf(define.ErrInvalidArg, "a source image must be specified") + case len(locations[0].Image) == 0 && len(locations[1].Image) != 0: + if locations[0].Remote && locations[1].Remote { + return true, nil // we need to flip the cliConnections array so the save/load connections are in the right place + } + } + return false, nil +} + +// validateImageName makes sure that the image given is valid and no injections are occurring +// we simply use this for error checking, bot setting the image +func validateImageName(input string) error { + // ParseNormalizedNamed transforms a shortname image into its + // full name reference so busybox => docker.io/library/busybox + // we want to keep our shortnames, so only return an error if + // we cannot parse what the user has given us + _, err := reference.ParseNormalizedNamed(input) + return err +} + +// remoteArgLength is a helper function to simplify the extracting of host argument data +// returns an int which contains the length of a specified index in a host::image string +func remoteArgLength(input string, side int) int { + if strings.Contains(input, "::") { + return len((strings.Split(input, "::"))[side]) + } + return -1 +} diff --git a/cmd/podman/main.go b/cmd/podman/main.go index b7f5f1720..b38734617 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -52,14 +52,14 @@ func parseCommands() *cobra.Command { // Command cannot be run rootless _, found := c.Command.Annotations[registry.UnshareNSRequired] if found { - if rootless.IsRootless() && os.Getuid() != 0 { + if rootless.IsRootless() && os.Getuid() != 0 && c.Command.Name() != "scp" { c.Command.RunE = func(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot run command %q in rootless mode, must execute `podman unshare` first", cmd.CommandPath()) } } } else { _, found = c.Command.Annotations[registry.ParentNSRequired] - if rootless.IsRootless() && found { + if rootless.IsRootless() && found && c.Command.Name() != "scp" { c.Command.RunE = func(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath()) } diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh index cff8f4b3f..c7352106a 100644 --- a/contrib/cirrus/lib.sh +++ b/contrib/cirrus/lib.sh @@ -165,6 +165,7 @@ setup_rootless() { groupadd -g $rootless_gid $ROOTLESS_USER useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC" + echo "$ROOTLESS_USER ALL=(root) NOPASSWD: ALL" > /etc/sudoers.d/ci-rootless mkdir -p "$HOME/.ssh" "/home/$ROOTLESS_USER/.ssh" diff --git a/docs/source/markdown/podman-pod-create.1.md b/docs/source/markdown/podman-pod-create.1.md index 56c3e7d34..b2e16e051 100644 --- a/docs/source/markdown/podman-pod-create.1.md +++ b/docs/source/markdown/podman-pod-create.1.md @@ -231,6 +231,38 @@ NOTE: This cannot be modified once the pod is created. If another pod with the same name already exists, replace and remove it. The default is **false**. +#### **--security-opt**=*option* + +Security Options + +- `apparmor=unconfined` : Turn off apparmor confinement for the pod +- `apparmor=your-profile` : Set the apparmor confinement profile for the pod + +- `label=user:USER` : Set the label user for the pod processes +- `label=role:ROLE` : Set the label role for the pod processes +- `label=type:TYPE` : Set the label process type for the pod processes +- `label=level:LEVEL` : Set the label level for the pod processes +- `label=filetype:TYPE` : Set the label file type for the pod files +- `label=disable` : Turn off label separation for the pod + +Note: Labeling can be disabled for all pods/containers by setting label=false in the **containers.conf** (`/etc/containers/containers.conf` or `$HOME/.config/containers/containers.conf`) file. + +- `mask=/path/1:/path/2` : The paths to mask separated by a colon. A masked path + cannot be accessed inside the containers within the pod. + +- `no-new-privileges` : Disable container processes from gaining additional privileges + +- `seccomp=unconfined` : Turn off seccomp confinement for the pod +- `seccomp=profile.json` : Whitelisted syscalls seccomp Json file to be used as a seccomp filter + +- `proc-opts=OPTIONS` : Comma-separated list of options to use for the /proc mount. More details for the + possible mount options are specified in the **proc(5)** man page. + +- **unmask**=_ALL_ or _/path/1:/path/2_, or shell expanded paths (/proc/*): Paths to unmask separated by a colon. If set to **ALL**, it will unmask all the paths that are masked or made read only by default. + The default masked paths are **/proc/acpi, /proc/kcore, /proc/keys, /proc/latency_stats, /proc/sched_debug, /proc/scsi, /proc/timer_list, /proc/timer_stats, /sys/firmware, and /sys/fs/selinux.** The default paths that are read only are **/proc/asound, /proc/bus, /proc/fs, /proc/irq, /proc/sys, /proc/sysrq-trigger, /sys/fs/cgroup**. + +Note: Labeling can be disabled for all containers by setting label=false in the **containers.conf** (`/etc/containers/containers.conf` or `$HOME/.config/containers/containers.conf`) file. + #### **--share**=*namespace* A comma-separated list of kernel namespaces to share. If none or "" is specified, no namespaces will be shared. The namespaces to choose from are ipc, net, pid, uts. @@ -471,7 +503,7 @@ $ podman pod create --network net1:ip=10.89.1.5 --network net2:ip=10.89.10.10 ``` ## SEE ALSO -**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)** +**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **containers.conf(1)** ## HISTORY diff --git a/libpod/container_config.go b/libpod/container_config.go index a43fd632b..288524dbd 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -400,3 +400,14 @@ type ContainerMiscConfig struct { // and if so, what type: always or once are possible non-nil entries InitContainerType string `json:"init_container_type,omitempty"` } + +type InfraInherit struct { + InfraSecurity ContainerSecurityConfig + InfraLabels []string `json:"labelopts,omitempty"` + InfraVolumes []*ContainerNamedVolume `json:"namedVolumes,omitempty"` + InfraOverlay []*ContainerOverlayVolume `json:"overlayVolumes,omitempty"` + InfraImageVolumes []*ContainerImageVolume `json:"ctrImageVolumes,omitempty"` + InfraUserVolumes []string `json:"userVolumes,omitempty"` + InfraResources *spec.LinuxResources `json:"resources,omitempty"` + InfraDevices []spec.LinuxDevice `json:"device_host_src,omitempty"` +} diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index f72700ab6..792dfc58e 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -273,6 +273,27 @@ func (c *Container) GetInspectMounts(namedVolumes []*ContainerNamedVolume, image return inspectMounts, nil } +// GetSecurityOptions retrives and returns the security related annotations and process information upon inspection +func (c *Container) GetSecurityOptions() []string { + ctrSpec := c.config.Spec + SecurityOpt := []string{} + if ctrSpec.Process != nil { + if ctrSpec.Process.NoNewPrivileges { + SecurityOpt = append(SecurityOpt, "no-new-privileges") + } + } + if label, ok := ctrSpec.Annotations[define.InspectAnnotationLabel]; ok { + SecurityOpt = append(SecurityOpt, fmt.Sprintf("label=%s", label)) + } + if seccomp, ok := ctrSpec.Annotations[define.InspectAnnotationSeccomp]; ok { + SecurityOpt = append(SecurityOpt, fmt.Sprintf("seccomp=%s", seccomp)) + } + if apparmor, ok := ctrSpec.Annotations[define.InspectAnnotationApparmor]; ok { + SecurityOpt = append(SecurityOpt, fmt.Sprintf("apparmor=%s", apparmor)) + } + return SecurityOpt +} + // Parse mount options so we can populate them in the mount structure. // The mount passed in will be modified. func parseMountOptionsForInspect(options []string, mount *define.InspectMount) { @@ -422,16 +443,14 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named hostConfig.GroupAdd = make([]string, 0, len(c.config.Groups)) hostConfig.GroupAdd = append(hostConfig.GroupAdd, c.config.Groups...) - hostConfig.SecurityOpt = []string{} if ctrSpec.Process != nil { if ctrSpec.Process.OOMScoreAdj != nil { hostConfig.OomScoreAdj = *ctrSpec.Process.OOMScoreAdj } - if ctrSpec.Process.NoNewPrivileges { - hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, "no-new-privileges") - } } + hostConfig.SecurityOpt = c.GetSecurityOptions() + hostConfig.ReadonlyRootfs = ctrSpec.Root.Readonly hostConfig.ShmSize = c.config.ShmSize hostConfig.Runtime = "oci" @@ -456,15 +475,6 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named if ctrSpec.Annotations[define.InspectAnnotationInit] == define.InspectResponseTrue { hostConfig.Init = true } - if label, ok := ctrSpec.Annotations[define.InspectAnnotationLabel]; ok { - hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("label=%s", label)) - } - if seccomp, ok := ctrSpec.Annotations[define.InspectAnnotationSeccomp]; ok { - hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("seccomp=%s", seccomp)) - } - if apparmor, ok := ctrSpec.Annotations[define.InspectAnnotationApparmor]; ok { - hostConfig.SecurityOpt = append(hostConfig.SecurityOpt, fmt.Sprintf("apparmor=%s", apparmor)) - } } // Resource limits diff --git a/libpod/define/pod_inspect.go b/libpod/define/pod_inspect.go index 97e7ffdfb..e7adc8700 100644 --- a/libpod/define/pod_inspect.go +++ b/libpod/define/pod_inspect.go @@ -65,6 +65,8 @@ type InspectPodData struct { BlkioDeviceReadBps []InspectBlkioThrottleDevice `json:"device_read_bps,omitempty"` // VolumesFrom contains the containers that the pod inherits mounts from VolumesFrom []string `json:"volumes_from,omitempty"` + // SecurityOpt contains the specified security labels and related SELinux information + SecurityOpts []string `json:"security_opt,omitempty"` } // InspectPodInfraConfig contains the configuration of the pod's infra diff --git a/libpod/options.go b/libpod/options.go index 204f2a457..6edb9972b 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1816,6 +1816,25 @@ func WithSelectedPasswordManagement(passwd *bool) CtrCreateOption { } } +// WithInfraConfig allows for inheritance of compatible config entities from the infra container +func WithInfraConfig(compatibleOptions InfraInherit) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + compatMarshal, err := json.Marshal(compatibleOptions) + if err != nil { + return errors.New("Could not marshal compatible options") + } + + err = json.Unmarshal(compatMarshal, ctr.config) + if err != nil { + return errors.New("Could not unmarshal compatible options into contrainer config") + } + return nil + } +} + // Pod Creation Options // WithPodCreateCommand adds the full command plus arguments of the current diff --git a/libpod/pod_api.go b/libpod/pod_api.go index 95a82721e..526e0c28b 100644 --- a/libpod/pod_api.go +++ b/libpod/pod_api.go @@ -586,6 +586,7 @@ func (p *Pod) Inspect() (*define.InspectPodData, error) { var inspectMounts []define.InspectMount var devices []define.InspectDevice var deviceLimits []define.InspectBlkioThrottleDevice + var infraSecurity []string if p.state.InfraContainerID != "" { infra, err := p.runtime.GetContainer(p.state.InfraContainerID) if err != nil { @@ -603,6 +604,7 @@ func (p *Pod) Inspect() (*define.InspectPodData, error) { infraConfig.UserNS = p.UserNSMode() namedVolumes, mounts := infra.sortUserVolumes(infra.config.Spec) inspectMounts, err = infra.GetInspectMounts(namedVolumes, infra.config.ImageVolumes, mounts) + infraSecurity = infra.GetSecurityOptions() if err != nil { return nil, err } @@ -678,6 +680,7 @@ func (p *Pod) Inspect() (*define.InspectPodData, error) { Devices: devices, BlkioDeviceReadBps: deviceLimits, VolumesFrom: p.VolumesFrom(), + SecurityOpts: infraSecurity, } return &inspectData, nil diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 3d18406a5..1b29831b4 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -42,6 +42,7 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { infraOptions := entities.NewInfraContainerCreateOptions() // options for pulling the image and FillOutSpec infraOptions.Net = &entities.NetOptions{} infraOptions.Devices = psg.Devices + infraOptions.SecurityOpt = psg.SecurityOpt err = specgenutil.FillOutSpecGen(psg.InfraContainerSpec, &infraOptions, []string{}) // necessary for default values in many cases (userns, idmappings) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error filling out specgen")) diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index d72f64b5e..bec505163 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -27,7 +27,7 @@ type ImageEngine interface { ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error) Shutdown(ctx context.Context) Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error - Transfer(ctx context.Context, scpOpts ImageScpOptions) error + Transfer(ctx context.Context, source ImageScpOptions, dest ImageScpOptions, parentFlags []string) error Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error) Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error) Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 8b0fd2b85..62e7f67c8 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -311,30 +311,28 @@ type ImageSaveOptions struct { Quiet bool } -// ImageScpOptions provide options for securely copying images to podman remote +// ImageScpOptions provide options for securely copying images to and from a remote host type ImageScpOptions struct { - // SoureImageName is the image the user is providing to load on a remote machine - SourceImageName string - // Tag allows for a new image to be created under the given name - Tag string - // ToRemote specifies that we are loading to the remote host - ToRemote bool - // FromRemote specifies that we are loading from the remote host - FromRemote bool + // Remote determines if this entity is operating on a remote machine + Remote bool `json:"remote,omitempty"` + // File is the input/output file for the save and load Operation + File string `json:"file,omitempty"` + // Quiet Determines if the save and load operation will be done quietly + Quiet bool `json:"quiet,omitempty"` + // Image is the image the user is providing to save and load + Image string `json:"image,omitempty"` + // User is used in conjunction with Transfer to determine if a valid user was given to save from/load into + User string `json:"user,omitempty"` +} + +// ImageScpConnections provides the ssh related information used in remote image transfer +type ImageScpConnections struct { // Connections holds the raw string values for connections (ssh or unix) Connections []string // URI contains the ssh connection URLs to be used by the client URI []*url.URL - // Iden contains ssh identity keys to be used by the client - Iden []string - // Save Options used for first half of the scp operation - Save ImageSaveOptions - // Load options used for the second half of the scp operation - Load ImageLoadOptions - // Rootless determines whether we are loading locally from root storage to rootless storage - Rootless bool - // User is used in conjunction with Rootless to determine which user to use to obtain the uid - User string + // Identities contains ssh identity keys to be used by the client + Identities []string } // ImageTreeOptions provides options for ImageEngine.Tree() diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index f9850e5a8..1b5a1be51 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -138,6 +138,7 @@ type PodCreateOptions struct { Userns specgen.Namespace `json:"-"` Volume []string `json:"volume,omitempty"` VolumesFrom []string `json:"volumes_from,omitempty"` + SecurityOpt []string `json:"security_opt,omitempty"` } // PodLogsOptions describes the options to extract pod logs. @@ -230,7 +231,7 @@ type ContainerCreateOptions struct { Rm bool RootFS bool Secrets []string - SecurityOpt []string + SecurityOpt []string `json:"security_opt,omitempty"` SdNotifyMode string ShmSize string SignaturePolicy string @@ -312,6 +313,7 @@ func ToPodSpecGen(s specgen.PodSpecGenerator, p *PodCreateOptions) (*specgen.Pod s.Hostname = p.Hostname s.Labels = p.Labels s.Devices = p.Devices + s.SecurityOpt = p.SecurityOpt s.NoInfra = !p.Infra if p.InfraCommand != nil && len(*p.InfraCommand) > 0 { s.InfraCommand = strings.Split(*p.InfraCommand, " ") diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 4346182d6..84c83ea8e 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -28,6 +28,7 @@ import ( domainUtils "github.com/containers/podman/v3/pkg/domain/utils" "github.com/containers/podman/v3/pkg/errorhandling" "github.com/containers/podman/v3/pkg/rootless" + "github.com/containers/podman/v3/utils" "github.com/containers/storage" dockerRef "github.com/docker/distribution/reference" "github.com/opencontainers/go-digest" @@ -351,65 +352,19 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri return pushError } -// Transfer moves images from root to rootless storage so the user specified in the scp call can access and use the image modified by root -func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error { - if scpOpts.User == "" { +// Transfer moves images between root and rootless storage so the user specified in the scp call can access and use the image modified by root +func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error { + if source.User == "" { return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage") } - var u *user.User - scpOpts.User = strings.Split(scpOpts.User, ":")[0] // split in case provided with uid:gid - _, err := strconv.Atoi(scpOpts.User) - if err != nil { - u, err = user.Lookup(scpOpts.User) - if err != nil { - return err - } - } else { - u, err = user.LookupId(scpOpts.User) - if err != nil { - return err - } - } - uid, err := strconv.Atoi(u.Uid) - if err != nil { - return err - } - gid, err := strconv.Atoi(u.Gid) - if err != nil { - return err - } - err = os.Chown(scpOpts.Save.Output, uid, gid) // chown the output because was created by root so we need to give th euser read access - if err != nil { - return err - } - podman, err := os.Executable() if err != nil { return err } - machinectl, err := exec.LookPath("machinectl") - if err != nil { - logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available") - cmd := exec.Command("su", "-l", u.Username, "--command", podman+" --log-level="+logrus.GetLevel().String()+" --cgroup-manager=cgroupfs load --input="+scpOpts.Save.Output) // load the new image to the rootless storage - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - logrus.Debug("Executing load command su") - err = cmd.Run() - if err != nil { - return err - } - } else { - cmd := exec.Command(machinectl, "shell", "-q", u.Username+"@.host", podman, "--log-level="+logrus.GetLevel().String(), "--cgroup-manager=cgroupfs", "load", "--input", scpOpts.Save.Output) // load the new image to the rootless storage - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - logrus.Debug("Executing load command machinectl") - err = cmd.Run() - if err != nil { - return err - } + if rootless.IsRootless() && (len(dest.User) == 0 || dest.User == "root") { // if we are rootless and do not have a destination user we can just use sudo + return transferRootless(source, dest, podman, parentFlags) } - - return nil + return transferRootful(source, dest, podman, parentFlags) } func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error { @@ -786,3 +741,123 @@ func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStore } return nil } + +// TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users +func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error { + var cmdSave *exec.Cmd + saveCommand := parentFlags + saveCommand = append(saveCommand, []string{"save", "--output", source.File, source.Image}...) + + loadCommand := parentFlags + loadCommand = append(loadCommand, []string{"load", "--input", dest.File}...) + + if source.User == "root" { + cmdSave = exec.Command("sudo", podman) + } else { + cmdSave = exec.Command(podman) + } + cmdSave = utils.CreateSCPCommand(cmdSave, saveCommand) + logrus.Debug("Executing save command") + err := cmdSave.Run() + if err != nil { + return err + } + + var cmdLoad *exec.Cmd + if source.User != "root" { + cmdLoad = exec.Command("sudo", podman) + } else { + cmdLoad = exec.Command(podman) + } + cmdLoad = utils.CreateSCPCommand(cmdLoad, loadCommand) + logrus.Debug("Executing load command") + err = cmdLoad.Run() + if err != nil { + return err + } + return nil +} + +// TransferRootless creates new podman processes using exec.Command and su/machinectl, transferring images between the given source and destination users +func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error { + basicCommand := []string{podman} + basicCommand = append(basicCommand, parentFlags...) + saveCommand := append(basicCommand, []string{"save", "--output", source.File, source.Image}...) + loadCommand := append(basicCommand, []string{"load", "--input", dest.File}...) + save := []string{strings.Join(saveCommand, " ")} + load := []string{strings.Join(loadCommand, " ")} + + // if executing using sudo or transferring between two users, the TransferRootless approach will not work, default to using machinectl or su as necessary. + // the approach using sudo is preferable and more straightforward. There is no reason for using sudo in these situations + // since the feature is meant to transfer from root to rootless an vice versa without explicit sudo evocaiton. + var uSave *user.User + var uLoad *user.User + var err error + source.User = strings.Split(source.User, ":")[0] // split in case provided with uid:gid + dest.User = strings.Split(dest.User, ":")[0] + uSave, err = lookupUser(source.User) + if err != nil { + return err + } + switch { + case dest.User != "": // if we are given a destination user, check that first + uLoad, err = lookupUser(dest.User) + if err != nil { + return err + } + case uSave.Name != "root": // else if we have no destination user, and source is not root that means we should be root + uLoad, err = user.LookupId("0") + if err != nil { + return err + } + default: // else if we have no dest user, and source user IS root, we want to be the default user. + uString := os.Getenv("SUDO_USER") + if uString == "" { + return errors.New("$SUDO_USER must be defined to find the default rootless user") + } + uLoad, err = user.Lookup(uString) + if err != nil { + return err + } + } + machinectl, err := exec.LookPath("machinectl") + if err != nil { + logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available") + err = execSu(uSave, save) + if err != nil { + return err + } + return execSu(uLoad, load) + } + err = execMachine(uSave, saveCommand, machinectl) + if err != nil { + return err + } + return execMachine(uLoad, loadCommand, machinectl) +} + +func lookupUser(u string) (*user.User, error) { + if u, err := user.LookupId(u); err == nil { + return u, nil + } + return user.Lookup(u) +} + +func execSu(execUser *user.User, command []string) error { + cmd := exec.Command("su", "-l", execUser.Username, "--command") + cmd = utils.CreateSCPCommand(cmd, command) + logrus.Debug("Executing command su") + return cmd.Run() +} + +func execMachine(execUser *user.User, command []string, machinectl string) error { + var cmd *exec.Cmd + if execUser.Uid == "0" { + cmd = exec.Command("sudo", machinectl, "shell", "-q", execUser.Username+"@.host") + } else { + cmd = exec.Command(machinectl, "shell", "-q", execUser.Username+"@.host") + } + cmd = utils.CreateSCPCommand(cmd, command) + logrus.Debug("Executing command machinectl") + return cmd.Run() +} diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 2feb9d7ad..f26a489e6 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -123,7 +123,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities. return &entities.ImagePullReport{Images: pulledImages}, nil } -func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error { +func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error { return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage") } diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index 92f331ce4..94bd40f86 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -244,7 +244,7 @@ can_use_shortcut () if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 || strcmp (argv[argc], "image") == 0) && - strcmp (argv[argc+1], "mount") == 0) + (strcmp (argv[argc+1], "mount") == 0 || strcmp (argv[argc+1], "scp") == 0)) { ret = false; break; diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 7ab9d1b29..7d792b3b1 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -2,13 +2,14 @@ package generate import ( "context" - "fmt" + "encoding/json" "path/filepath" "strings" cdi "github.com/container-orchestrated-devices/container-device-interface/pkg" "github.com/containers/common/libimage" "github.com/containers/podman/v3/libpod" + "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/namespaces" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" @@ -29,43 +30,30 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener // If joining a pod, retrieve the pod for use, and its infra container var pod *libpod.Pod - var infraConfig *libpod.ContainerConfig + var infra *libpod.Container if s.Pod != "" { pod, err = rt.LookupPod(s.Pod) if err != nil { return nil, nil, nil, errors.Wrapf(err, "error retrieving pod %s", s.Pod) } if pod.HasInfraContainer() { - infra, err := pod.InfraContainer() + infra, err = pod.InfraContainer() if err != nil { return nil, nil, nil, err } - infraConfig = infra.Config() } } - if infraConfig != nil && (len(infraConfig.NamedVolumes) > 0 || len(infraConfig.UserVolumes) > 0 || len(infraConfig.ImageVolumes) > 0 || len(infraConfig.OverlayVolumes) > 0) { - s.VolumesFrom = append(s.VolumesFrom, infraConfig.ID) - } - - if infraConfig != nil && len(infraConfig.Spec.Linux.Devices) > 0 { - s.DevicesFrom = append(s.DevicesFrom, infraConfig.ID) - } - if infraConfig != nil && infraConfig.Spec.Linux.Resources != nil && infraConfig.Spec.Linux.Resources.BlockIO != nil && len(infraConfig.Spec.Linux.Resources.BlockIO.ThrottleReadBpsDevice) > 0 { - tempDev := make(map[string]spec.LinuxThrottleDevice) - for _, val := range infraConfig.Spec.Linux.Resources.BlockIO.ThrottleReadBpsDevice { - nodes, err := util.FindDeviceNodes() - if err != nil { - return nil, nil, nil, err - } - key := fmt.Sprintf("%d:%d", val.Major, val.Minor) - tempDev[nodes[key]] = spec.LinuxThrottleDevice{Rate: uint64(val.Rate)} - } - for i, dev := range s.ThrottleReadBpsDevice { - tempDev[i] = dev + options := []libpod.CtrCreateOption{} + compatibleOptions := &libpod.InfraInherit{} + var infraSpec *spec.Spec + if infra != nil { + options, infraSpec, compatibleOptions, err = Inherit(*infra) + if err != nil { + return nil, nil, nil, err } - s.ThrottleReadBpsDevice = tempDev } + if err := FinishThrottleDevices(s); err != nil { return nil, nil, nil, err } @@ -119,8 +107,6 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener s.CgroupNS = defaultNS } - options := []libpod.CtrCreateOption{} - if s.ContainerCreateCommand != nil { options = append(options, libpod.WithCreateCommand(s.ContainerCreateCommand)) } @@ -165,7 +151,8 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener return nil, nil, nil, err } - opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command) + infraVolumes := (len(compatibleOptions.InfraVolumes) > 0 || len(compatibleOptions.InfraUserVolumes) > 0 || len(compatibleOptions.InfraImageVolumes) > 0) + opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command, infraVolumes, *compatibleOptions) if err != nil { return nil, nil, nil, err } @@ -178,27 +165,29 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener logrus.Debugf("setting container name %s", s.Name) options = append(options, libpod.WithName(s.Name)) } - if len(s.DevicesFrom) > 0 { - for _, dev := range s.DevicesFrom { - ctr, err := rt.GetContainer(dev) - if err != nil { - return nil, nil, nil, err - } - devices := ctr.DeviceHostSrc() - s.Devices = append(s.Devices, devices...) - } - } if len(s.Devices) > 0 { - opts = extractCDIDevices(s) + opts = ExtractCDIDevices(s) options = append(options, opts...) } - runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts, pod, command) + runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts, pod, command, compatibleOptions) if err != nil { return nil, nil, nil, err } if len(s.HostDeviceList) > 0 { options = append(options, libpod.WithHostDevice(s.HostDeviceList)) } + if infraSpec != nil && infraSpec.Linux != nil { // if we are inheriting Linux info from a pod... + // Pass Security annotations + if len(infraSpec.Annotations[define.InspectAnnotationLabel]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationLabel]) == 0 { + runtimeSpec.Annotations[define.InspectAnnotationLabel] = infraSpec.Annotations[define.InspectAnnotationLabel] + } + if len(infraSpec.Annotations[define.InspectAnnotationSeccomp]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationSeccomp]) == 0 { + runtimeSpec.Annotations[define.InspectAnnotationSeccomp] = infraSpec.Annotations[define.InspectAnnotationSeccomp] + } + if len(infraSpec.Annotations[define.InspectAnnotationApparmor]) > 0 && len(runtimeSpec.Annotations[define.InspectAnnotationApparmor]) == 0 { + runtimeSpec.Annotations[define.InspectAnnotationApparmor] = infraSpec.Annotations[define.InspectAnnotationApparmor] + } + } return runtimeSpec, s, options, err } func ExecuteCreate(ctx context.Context, rt *libpod.Runtime, runtimeSpec *spec.Spec, s *specgen.SpecGenerator, infra bool, options ...libpod.CtrCreateOption) (*libpod.Container, error) { @@ -210,7 +199,7 @@ func ExecuteCreate(ctx context.Context, rt *libpod.Runtime, runtimeSpec *spec.Sp return ctr, rt.PrepareVolumeOnCreateContainer(ctx, ctr) } -func extractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption { +func ExtractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption { devs := make([]spec.LinuxDevice, 0, len(s.Devices)) var cdiDevs []string var options []libpod.CtrCreateOption @@ -224,19 +213,16 @@ func extractCDIDevices(s *specgen.SpecGenerator) []libpod.CtrCreateOption { cdiDevs = append(cdiDevs, device.Path) continue } - devs = append(devs, device) } - s.Devices = devs if len(cdiDevs) > 0 { options = append(options, libpod.WithCDI(cdiDevs)) } - return options } -func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string) ([]libpod.CtrCreateOption, error) { +func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string, infraVolumes bool, compatibleOptions libpod.InfraInherit) ([]libpod.CtrCreateOption, error) { var options []libpod.CtrCreateOption var err error @@ -317,7 +303,10 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. for _, imageVolume := range s.ImageVolumes { destinations = append(destinations, imageVolume.Destination) } - options = append(options, libpod.WithUserVolumes(destinations)) + + if len(destinations) > 0 || !infraVolumes { + options = append(options, libpod.WithUserVolumes(destinations)) + } if len(volumes) != 0 { var vols []*libpod.ContainerNamedVolume @@ -405,7 +394,7 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if len(s.SelinuxOpts) > 0 { options = append(options, libpod.WithSecLabels(s.SelinuxOpts)) } else { - if pod != nil { + if pod != nil && len(compatibleOptions.InfraLabels) == 0 { // duplicate the security options from the pod processLabel, err := pod.ProcessLabel() if err != nil { @@ -498,3 +487,33 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. return options, nil } + +func Inherit(infra libpod.Container) (opts []libpod.CtrCreateOption, infraS *spec.Spec, compat *libpod.InfraInherit, err error) { + options := []libpod.CtrCreateOption{} + compatibleOptions := &libpod.InfraInherit{} + infraConf := infra.Config() + infraSpec := infraConf.Spec + + config, err := json.Marshal(infraConf) + if err != nil { + return nil, nil, nil, err + } + err = json.Unmarshal(config, compatibleOptions) + if err != nil { + return nil, nil, nil, err + } + if infraSpec.Linux != nil && infraSpec.Linux.Resources != nil { + resources, err := json.Marshal(infraSpec.Linux.Resources) + if err != nil { + return nil, nil, nil, err + } + err = json.Unmarshal(resources, &compatibleOptions.InfraResources) + if err != nil { + return nil, nil, nil, err + } + } + if compatibleOptions != nil { + options = append(options, libpod.WithInfraConfig(*compatibleOptions)) + } + return options, infraSpec, compatibleOptions, nil +} diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index efac53104..ee3a990fc 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -2,6 +2,7 @@ package generate import ( "context" + "encoding/json" "path" "strings" @@ -174,7 +175,7 @@ func getCGroupPermissons(unmask []string) string { } // SpecGenToOCI returns the base configuration for the container. -func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, rtc *config.Config, newImage *libimage.Image, mounts []spec.Mount, pod *libpod.Pod, finalCmd []string) (*spec.Spec, error) { +func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, rtc *config.Config, newImage *libimage.Image, mounts []spec.Mount, pod *libpod.Pod, finalCmd []string, compatibleOptions *libpod.InfraInherit) (*spec.Spec, error) { cgroupPerm := getCGroupPermissons(s.Unmask) g, err := generate.New("linux") @@ -299,9 +300,32 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt g.AddAnnotation(key, val) } - g.Config.Linux.Resources = s.ResourceLimits + if compatibleOptions.InfraResources == nil && s.ResourceLimits != nil { + g.Config.Linux.Resources = s.ResourceLimits + } else if s.ResourceLimits != nil { // if we have predefined resource limits we need to make sure we keep the infra and container limits + originalResources, err := json.Marshal(s.ResourceLimits) + if err != nil { + return nil, err + } + infraResources, err := json.Marshal(compatibleOptions.InfraResources) + if err != nil { + return nil, err + } + err = json.Unmarshal(infraResources, s.ResourceLimits) // put infra's resource limits in the container + if err != nil { + return nil, err + } + err = json.Unmarshal(originalResources, s.ResourceLimits) // make sure we did not override anything + if err != nil { + return nil, err + } + g.Config.Linux.Resources = s.ResourceLimits + } else { + g.Config.Linux.Resources = compatibleOptions.InfraResources + } // Devices + var userDevices []spec.LinuxDevice if s.Privileged { // If privileged, we need to add all the host devices to the // spec. We do not add the user provided ones because we are @@ -316,14 +340,19 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt return nil, err } } + if len(compatibleOptions.InfraDevices) > 0 && len(s.Devices) == 0 { + userDevices = compatibleOptions.InfraDevices + } else { + userDevices = s.Devices + } // add default devices specified by caller - for _, device := range s.Devices { + for _, device := range userDevices { if err = DevicesFromPath(&g, device.Path); err != nil { return nil, err } } } - s.HostDeviceList = s.Devices + s.HostDeviceList = userDevices // set the devices cgroup when not running in a user namespace if !inUserNS && !s.Privileged { diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index e59d11c0a..33e8422fd 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -196,6 +196,7 @@ type PodSpecGenerator struct { PodCgroupConfig PodResourceConfig PodStorageConfig + PodSecurityConfig InfraContainerSpec *SpecGenerator `json:"-"` } @@ -210,6 +211,10 @@ type PodResourceConfig struct { ThrottleReadBpsDevice map[string]spec.LinuxThrottleDevice `json:"throttleReadBpsDevice,omitempty"` } +type PodSecurityConfig struct { + SecurityOpt []string `json:"security_opt,omitempty"` +} + // NewPodSpecGenerator creates a new pod spec func NewPodSpecGenerator() *PodSpecGenerator { return &PodSpecGenerator{} diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 6e1a62b99..bd744aa78 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -274,14 +274,32 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { } if remote { - uuid := stringid.GenerateNonCryptoID() + var pathPrefix string if !rootless.IsRootless() { - p.RemoteSocket = fmt.Sprintf("unix:/run/podman/podman-%s.sock", uuid) + pathPrefix = "/run/podman/podman" } else { runtimeDir := os.Getenv("XDG_RUNTIME_DIR") - socket := fmt.Sprintf("podman-%s.sock", uuid) - fqpath := filepath.Join(runtimeDir, socket) - p.RemoteSocket = fmt.Sprintf("unix:%s", fqpath) + pathPrefix = filepath.Join(runtimeDir, "podman") + } + // We want to avoid collisions in socket paths, but using the + // socket directly for a collision check doesn’t work; bind(2) on AF_UNIX + // creates the file, and we need to pass a unique path now before the bind(2) + // happens. So, use a podman-%s.sock-lock empty file as a marker. + tries := 0 + for { + uuid := stringid.GenerateNonCryptoID() + lockPath := fmt.Sprintf("%s-%s.sock-lock", pathPrefix, uuid) + lockFile, err := os.OpenFile(lockPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700) + if err == nil { + lockFile.Close() + p.RemoteSocketLock = lockPath + p.RemoteSocket = fmt.Sprintf("unix:%s-%s.sock", pathPrefix, uuid) + break + } + tries++ + if tries >= 1000 { + panic("Too many RemoteSocket collisions") + } } } diff --git a/test/e2e/image_scp_test.go b/test/e2e/image_scp_test.go index 6651a04b5..767b355d9 100644 --- a/test/e2e/image_scp_test.go +++ b/test/e2e/image_scp_test.go @@ -29,7 +29,6 @@ var _ = Describe("podman image scp", func() { panic(err) } os.Setenv("CONTAINERS_CONF", conf.Name()) - tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) @@ -52,38 +51,6 @@ var _ = Describe("podman image scp", func() { }) - It("podman image scp quiet flag", func() { - if IsRemote() { - Skip("this test is only for non-remote") - } - scp := podmanTest.Podman([]string{"image", "scp", "-q", ALPINE}) - scp.WaitWithDefaultTimeout() - Expect(scp).To(Exit(0)) - }) - - It("podman image scp root to rootless transfer", func() { - SkipIfNotRootless("this is a rootless only test, transferring from root to rootless using PodmanAsUser") - if IsRemote() { - Skip("this test is only for non-remote") - } - env := os.Environ() - img := podmanTest.PodmanAsUser([]string{"image", "pull", ALPINE}, 0, 0, "", env) // pull image to root - img.WaitWithDefaultTimeout() - Expect(img).To(Exit(0)) - scp := podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE, "1000:1000@localhost::"}, 0, 0, "", env) //transfer from root to rootless (us) - scp.WaitWithDefaultTimeout() - Expect(scp).To(Exit(0)) - - list := podmanTest.Podman([]string{"image", "list"}) // our image should now contain alpine loaded in from root - list.WaitWithDefaultTimeout() - Expect(list).To(Exit(0)) - Expect(list.OutputToStringArray()).To(ContainElement(HavePrefix("quay.io/libpod/alpine"))) - - scp = podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE}, 0, 0, "", env) //transfer from root to rootless (us) - scp.WaitWithDefaultTimeout() - Expect(scp).To(Exit(0)) - }) - It("podman image scp bogus image", func() { if IsRemote() { Skip("this test is only for non-remote") @@ -119,11 +86,8 @@ var _ = Describe("podman image scp", func() { scp.Wait(45) // exit with error because we cannot make an actual ssh connection // This tests that the input we are given is validated and prepared correctly - // Error: failed to connect: dial tcp: address foo: missing port in address + // The error given should either be a missing image (due to testing suite complications) or a i/o timeout on ssh Expect(scp).To(ExitWithError()) - Expect(scp.ErrorToString()).To(ContainSubstring( - "Error: failed to connect: dial tcp 66.151.147.142:2222: i/o timeout", - )) }) diff --git a/test/e2e/libpod_suite_remote_test.go b/test/e2e/libpod_suite_remote_test.go index d60383029..4644e3748 100644 --- a/test/e2e/libpod_suite_remote_test.go +++ b/test/e2e/libpod_suite_remote_test.go @@ -1,3 +1,4 @@ +//go:build remote // +build remote package integration @@ -143,6 +144,11 @@ func (p *PodmanTestIntegration) StopRemoteService() { if err := os.Remove(socket); err != nil { fmt.Println(err) } + if p.RemoteSocketLock != "" { + if err := os.Remove(p.RemoteSocketLock); err != nil { + fmt.Println(err) + } + } } //MakeOptions assembles all the podman main options diff --git a/test/e2e/pod_create_test.go b/test/e2e/pod_create_test.go index 41a017a52..fab107af8 100644 --- a/test/e2e/pod_create_test.go +++ b/test/e2e/pod_create_test.go @@ -9,6 +9,8 @@ import ( "strconv" "strings" + "github.com/containers/common/pkg/apparmor" + "github.com/containers/common/pkg/seccomp" "github.com/containers/common/pkg/sysinfo" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/pkg/util" @@ -16,6 +18,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" + "github.com/opencontainers/selinux/go-selinux" ) var _ = Describe("Podman pod create", func() { @@ -967,4 +970,63 @@ ENTRYPOINT ["sleep","99999"] Expect(inspect).Should(Exit(0)) Expect(inspect.OutputToString()).Should(Equal("host")) }) + + It("podman pod create --security-opt", func() { + if !selinux.GetEnabled() { + Skip("SELinux not enabled") + } + podCreate := podmanTest.Podman([]string{"pod", "create", "--security-opt", "label=type:spc_t", "--security-opt", "seccomp=unconfined"}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + + ctrCreate := podmanTest.Podman([]string{"container", "create", "--pod", podCreate.OutputToString(), ALPINE}) + ctrCreate.WaitWithDefaultTimeout() + Expect(ctrCreate).Should(Exit(0)) + + ctrInspect := podmanTest.InspectContainer(ctrCreate.OutputToString()) + Expect(ctrInspect[0].HostConfig.SecurityOpt).To(Equal([]string{"label=type:spc_t", "seccomp=unconfined"})) + + podCreate = podmanTest.Podman([]string{"pod", "create", "--security-opt", "label=disable"}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + + ctrCreate = podmanTest.Podman([]string{"container", "run", "-it", "--pod", podCreate.OutputToString(), ALPINE, "cat", "/proc/self/attr/current"}) + ctrCreate.WaitWithDefaultTimeout() + Expect(ctrCreate).Should(Exit(0)) + match, _ := ctrCreate.GrepString("spc_t") + Expect(match).Should(BeTrue()) + }) + + It("podman pod create --security-opt seccomp", func() { + if !seccomp.IsEnabled() { + Skip("seccomp is not enabled") + } + podCreate := podmanTest.Podman([]string{"pod", "create", "--security-opt", "seccomp=unconfined"}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + + ctrCreate := podmanTest.Podman([]string{"container", "create", "--pod", podCreate.OutputToString(), ALPINE}) + ctrCreate.WaitWithDefaultTimeout() + Expect(ctrCreate).Should(Exit(0)) + + ctrInspect := podmanTest.InspectContainer(ctrCreate.OutputToString()) + Expect(ctrInspect[0].HostConfig.SecurityOpt).To(Equal([]string{"seccomp=unconfined"})) + }) + + It("podman pod create --security-opt apparmor test", func() { + if !apparmor.IsEnabled() { + Skip("Apparmor is not enabled") + } + podCreate := podmanTest.Podman([]string{"pod", "create", "--security-opt", fmt.Sprintf("apparmor=%s", apparmor.Profile)}) + podCreate.WaitWithDefaultTimeout() + Expect(podCreate).Should(Exit(0)) + + ctrCreate := podmanTest.Podman([]string{"container", "create", "--pod", podCreate.OutputToString(), ALPINE}) + ctrCreate.WaitWithDefaultTimeout() + Expect(ctrCreate).Should(Exit(0)) + + inspect := podmanTest.InspectContainer(ctrCreate.OutputToString()) + Expect(inspect[0].AppArmorProfile).To(Equal(apparmor.Profile)) + + }) }) diff --git a/test/system/120-load.bats b/test/system/120-load.bats index a5508b2f4..541095764 100644 --- a/test/system/120-load.bats +++ b/test/system/120-load.bats @@ -78,6 +78,35 @@ verify_iid_and_name() { run_podman rmi $fqin } +@test "podman image scp transfer" { + skip_if_root_ubuntu "cannot create a new user successfully on ubuntu" + get_iid_and_name + if ! is_remote; then + if is_rootless; then + whoami=$(id -un) + run_podman image scp $whoami@localhost::$iid root@localhost:: + if [ "$status" -ne 0 ]; then + die "Command failed: podman image scp transfer" + fi + whoami=$(id -un) + run_podman image scp -q $whoami@localhost::$iid root@localhost:: + if [ "$status" -ne 0 ]; then + die "Command failed: podman image scp quiet transfer failed" + fi + fi + if ! is_rootless; then + id -u 1000 &>/dev/null || useradd -u 1000 -g 1000 testingUsr + if [ "$status" -ne 0 ]; then + die "Command failed: useradd 1000" + fi + run_podman image scp root@localhost::$iid 1000:1000@localhost:: + if [ "$status" -ne 0 ]; then + die "Command failed: podman image scp transfer" + fi + fi + fi +} + @test "podman load - by image ID" { # FIXME: how to build a simple archive instead? diff --git a/test/system/520-checkpoint.bats b/test/system/520-checkpoint.bats index 723a20cc4..046dfd126 100644 --- a/test/system/520-checkpoint.bats +++ b/test/system/520-checkpoint.bats @@ -11,7 +11,7 @@ function setup() { # TL;DR they keep fixing it then breaking it again. There's a test we # could run to see if it's fixed, but it's way too complicated. Since # integration tests also skip checkpoint tests on Ubuntu, do the same here. - if grep -qiw ubuntu /etc/os-release; then + if is_ubuntu; then skip "FIXME: checkpointing broken in Ubuntu 2004, 2104, 2110, ..." fi diff --git a/test/system/helpers.bash b/test/system/helpers.bash index 8f6033309..dcf7cf0a7 100644 --- a/test/system/helpers.bash +++ b/test/system/helpers.bash @@ -318,6 +318,10 @@ function wait_for_port() { # BEGIN miscellaneous tools # Shortcuts for common needs: +function is_ubuntu() { + grep -qiw ubuntu /etc/os-release +} + function is_rootless() { [ "$(id -u)" -ne 0 ] } @@ -459,6 +463,16 @@ function skip_if_journald_unavailable { fi } +function skip_if_root_ubuntu { + if is_ubuntu; then + if ! is_remote; then + if ! is_rootless; then + skip "Cannot run this test on rootful ubuntu, usually due to user errors" + fi + fi + fi +} + ######### # die # Abort with helpful message ######### diff --git a/test/utils/utils.go b/test/utils/utils.go index f41024072..1f5067950 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -41,6 +41,7 @@ type PodmanTest struct { RemotePodmanBinary string RemoteSession *os.Process RemoteSocket string + RemoteSocketLock string // If not "", should be removed _after_ RemoteSocket is removed RemoteCommand *exec.Cmd ImageCacheDir string ImageCacheFS string @@ -469,10 +470,6 @@ func Containerized() bool { return strings.Contains(string(b), "docker") } -func init() { - rand.Seed(GinkgoRandomSeed()) -} - var randomLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") // RandomString returns a string of given length composed of random characters diff --git a/utils/utils.go b/utils/utils.go index 45cec2c5f..4c04b939d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -229,3 +229,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 +} |