diff options
73 files changed, 1574 insertions, 608 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/common/netflags.go b/cmd/podman/common/netflags.go index ba8ab7a8b..425d85c9d 100644 --- a/cmd/podman/common/netflags.go +++ b/cmd/podman/common/netflags.go @@ -53,6 +53,13 @@ func DefineNetFlags(cmd *cobra.Command) { ) _ = cmd.RegisterFlagCompletionFunc(ipFlagName, completion.AutocompleteNone) + ip6FlagName := "ip6" + netFlags.String( + ip6FlagName, "", + "Specify a static IPv6 address for the container", + ) + _ = cmd.RegisterFlagCompletionFunc(ip6FlagName, completion.AutocompleteNone) + macAddressFlagName := "mac-address" netFlags.String( macAddressFlagName, "", @@ -185,7 +192,7 @@ func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet) (*enti opts.Networks = networks } - if flags.Changed("ip") || flags.Changed("mac-address") || flags.Changed("network-alias") { + if flags.Changed("ip") || flags.Changed("ip6") || flags.Changed("mac-address") || flags.Changed("network-alias") { // if there is no network we add the default if len(opts.Networks) == 0 { opts.Networks = map[string]types.PerNetworkOptions{ @@ -193,29 +200,31 @@ func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet) (*enti } } - ip, err := flags.GetString("ip") - if err != nil { - return nil, err - } - if ip != "" { - // if pod create --infra=false - if infra, err := flags.GetBool("infra"); err == nil && !infra { - return nil, errors.Wrap(define.ErrInvalidArg, "cannot set --ip without infra container") + for _, ipFlagName := range []string{"ip", "ip6"} { + ip, err := flags.GetString(ipFlagName) + if err != nil { + return nil, err } + if ip != "" { + // if pod create --infra=false + if infra, err := flags.GetBool("infra"); err == nil && !infra { + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot set --%s without infra container", ipFlagName) + } - staticIP := net.ParseIP(ip) - if staticIP == nil { - return nil, errors.Errorf("%s is not an ip address", ip) - } - if !opts.Network.IsBridge() && !opts.Network.IsDefault() { - return nil, errors.Wrap(define.ErrInvalidArg, "--ip can only be set when the network mode is bridge") - } - if len(opts.Networks) != 1 { - return nil, errors.Wrap(define.ErrInvalidArg, "--ip can only be set for a single network") - } - for name, netOpts := range opts.Networks { - netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) - opts.Networks[name] = netOpts + staticIP := net.ParseIP(ip) + if staticIP == nil { + return nil, errors.Errorf("%q is not an ip address", ip) + } + if !opts.Network.IsBridge() && !opts.Network.IsDefault() { + return nil, errors.Wrapf(define.ErrInvalidArg, "--%s can only be set when the network mode is bridge", ipFlagName) + } + if len(opts.Networks) != 1 { + return nil, errors.Wrapf(define.ErrInvalidArg, "--%s can only be set for a single network", ipFlagName) + } + for name, netOpts := range opts.Networks { + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + opts.Networks[name] = netOpts + } } } 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/contrib/cirrus/setup_environment.sh b/contrib/cirrus/setup_environment.sh index 43c709228..ee80a209c 100755 --- a/contrib/cirrus/setup_environment.sh +++ b/contrib/cirrus/setup_environment.sh @@ -121,8 +121,9 @@ case "$OS_RELEASE_ID" in # Force a crun version that has this fix: https://github.com/containers/crun/pull/819 # FIXME: Remove once a fixed crun made its way into Fedora if test "$OS_RELEASE_VER" == "35"; then - yum upgrade -y https://kojipkgs.fedoraproject.org//work/tasks/684/80280684/crun-1.3-2.fc35.x86_64.rpm + yum upgrade -y crun fi + if ((CONTAINER==0)); then # All SELinux distros need this for systemd-in-a-container msg "Enabling container_manage_cgroup" diff --git a/docs/source/_static/api.html b/docs/source/_static/api.html index fbc945d87..6d467d099 100644 --- a/docs/source/_static/api.html +++ b/docs/source/_static/api.html @@ -18,7 +18,7 @@ </style> </head> <body> - <redoc spec-url='https://storage.googleapis.com/libpod-master-releases/swagger-latest.yaml' sort-props-alphabetically></redoc> + <redoc spec-url='https://storage.googleapis.com/libpod-master-releases/swagger-latest.yaml' sort-props-alphabetically sort-operations-alphabetically></redoc> <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script> </body> </html> diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 3d4b867d4..e3647b194 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -474,19 +474,24 @@ Path to the container-init binary. Keep STDIN open even if not attached. The default is *false*. -#### **--ip6**=*ip* +#### **--ip**=*ipv4* -Not implemented - -#### **--ip**=*ip* - -Specify a static IP address for the container, for example **10.88.64.128**. +Specify a static IPv4 address for the container, for example **10.88.64.128**. This option can only be used if the container is joined to only a single network - i.e., **--network=network-name** is used at most once - and if the container is not joining another container's network namespace via **--network=container:_id_**. The address must be within the network's IP address pool (default **10.88.0.0/16**). To specify multiple static IP addresses per container, set multiple networks using the **--network** option with a static IP address specified for each using the `ip` mode for that option. +#### **--ip6**=*ipv6* + +Specify a static IPv6 address for the container, for example **fd46:db93:aa76:ac37::10**. +This option can only be used if the container is joined to only a single network - i.e., **--network=network-name** is used at most once - +and if the container is not joining another container's network namespace via **--network=container:_id_**. +The address must be within the network's IPv6 address pool. + +To specify multiple static IPv6 addresses per container, set multiple networks using the **--network** option with a static IPv6 address specified for each using the `ip6` mode for that option. + #### **--ipc**=*ipc* diff --git a/docs/source/markdown/podman-pod-create.1.md b/docs/source/markdown/podman-pod-create.1.md index b1b029429..b2e16e051 100644 --- a/docs/source/markdown/podman-pod-create.1.md +++ b/docs/source/markdown/podman-pod-create.1.md @@ -127,6 +127,15 @@ The address must be within the network's IP address pool (default **10.88.0.0/16 To specify multiple static IP addresses per pod, set multiple networks using the **--network** option with a static IP address specified for each using the `ip` mode for that option. +#### **--ip6**=*ipv6* + +Specify a static IPv6 address for the pod, for example **fd46:db93:aa76:ac37::10**. +This option can only be used if the pod is joined to only a single network - i.e., **--network=network-name** is used at most once - +and if the pod is not joining another container's network namespace via **--network=container:_id_**. +The address must be within the network's IPv6 address pool. + +To specify multiple static IPv6 addresses per pod, set multiple networks using the **--network** option with a static IPv6 address specified for each using the `ip6` mode for that option. + #### **--label**=*label*, **-l** Add metadata to a pod (e.g., --label com.example.key=value). @@ -222,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. @@ -462,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/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 6c565e3d1..b98e563ef 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -497,19 +497,24 @@ Path to the container-init binary. When set to **true**, keep stdin open even if not attached. The default is **false**. -#### **--ip6**=*ip* +#### **--ip**=*ipv4* -Not implemented. - -#### **--ip**=*ip* - -Specify a static IP address for the container, for example **10.88.64.128**. +Specify a static IPv4 address for the container, for example **10.88.64.128**. This option can only be used if the container is joined to only a single network - i.e., **--network=network-name** is used at most once - and if the container is not joining another container's network namespace via **--network=container:_id_**. The address must be within the network's IP address pool (default **10.88.0.0/16**). To specify multiple static IP addresses per container, set multiple networks using the **--network** option with a static IP address specified for each using the `ip` mode for that option. +#### **--ip6**=*ipv6* + +Specify a static IPv6 address for the container, for example **fd46:db93:aa76:ac37::10**. +This option can only be used if the container is joined to only a single network - i.e., **--network=network-name** is used at most once - +and if the container is not joining another container's network namespace via **--network=container:_id_**. +The address must be within the network's IPv6 address pool. + +To specify multiple static IPv6 addresses per container, set multiple networks using the **--network** option with a static IPv6 address specified for each using the `ip6` mode for that option. + #### **--ipc**=*mode* Set the IPC namespace mode for a container. The default is to create diff --git a/docs/source/markdown/podman-search.1.md b/docs/source/markdown/podman-search.1.md index 9e166fcc2..9c075a1e0 100644 --- a/docs/source/markdown/podman-search.1.md +++ b/docs/source/markdown/podman-search.1.md @@ -62,7 +62,7 @@ Valid placeholders for the Go template are listed below: | --------------- | ---------------------------- | | .Index | Registry | | .Name | Image name | -| .Descriptions | Image description | +| .Description | Image description | | .Stars | Star count of image | | .Official | "[OK]" if image is official | | .Automated | "[OK]" if image is automated | diff --git a/docs/tutorials/mac_experimental.md b/docs/tutorials/mac_experimental.md index 8df64dc99..b5b815fe5 100644 --- a/docs/tutorials/mac_experimental.md +++ b/docs/tutorials/mac_experimental.md @@ -90,7 +90,7 @@ that you were given. It will be used in two of the steps below. ## Test podman -1. podman machine init --image-path /path/to/image +1. podman machine init --image-path /path/to/image --cpus 2 2. podman machine start 3. podman images 4. git clone http://github.com/baude/alpine_nginx && cd alpine_nginx 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/container_internal.go b/libpod/container_internal.go index 7ae9daefa..2d12a90d1 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -762,7 +762,7 @@ func (c *Container) export(path string) error { if !c.state.Mounted { containerMount, err := c.runtime.store.Mount(c.ID(), c.config.MountLabel) if err != nil { - return errors.Wrapf(err, "error mounting container %q", c.ID()) + return errors.Wrapf(err, "mounting container %q", c.ID()) } mountPoint = containerMount defer func() { 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/healthcheck_linux.go b/libpod/healthcheck_linux.go index 2c19e0a61..a1f3e8491 100644 --- a/libpod/healthcheck_linux.go +++ b/libpod/healthcheck_linux.go @@ -73,6 +73,16 @@ func (c *Container) removeTransientFiles(ctx context.Context) error { defer conn.Close() timerFile := fmt.Sprintf("%s.timer", c.ID()) serviceFile := fmt.Sprintf("%s.service", c.ID()) + + // If the service has failed (the healthcheck has failed), then + // the .service file is not removed on stopping the unit file. If + // we check the properties of the service, it will automatically + // reset the state. But checking the state takes msecs vs usecs to + // blindly call reset. + if err := conn.ResetFailedUnitContext(ctx, serviceFile); err != nil { + logrus.Debugf("failed to reset unit file: %q", err) + } + // We want to ignore errors where the timer unit and/or service unit has already // been removed. The error return is generic so we have to check against the // string in the error diff --git a/libpod/network/internal/util/util.go b/libpod/network/internal/util/util.go index bf9d70aba..d9b9a8dc0 100644 --- a/libpod/network/internal/util/util.go +++ b/libpod/network/internal/util/util.go @@ -78,7 +78,7 @@ func GetUsedSubnets(n NetUtil) ([]*net.IPNet, error) { return append(subnets, liveSubnets...), nil } -// GetFreeIPv6NetworkSubnet returns a unused ipv4 subnet +// GetFreeIPv4NetworkSubnet returns a unused ipv4 subnet func GetFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { // the default podman network is 10.88.0.0/16 // start locking for free /24 networks 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/libpod/runtime_volume_linux.go b/libpod/runtime_volume_linux.go index ed3cc971c..d4d9a4438 100644 --- a/libpod/runtime_volume_linux.go +++ b/libpod/runtime_volume_linux.go @@ -35,7 +35,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) volume := newVolume(r) for _, option := range options { if err := option(volume); err != nil { - return nil, errors.Wrapf(err, "error running volume create option") + return nil, errors.Wrapf(err, "running volume create option") } } @@ -50,7 +50,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) // Check if volume with given name exists. exists, err := r.state.HasVolume(volume.config.Name) if err != nil { - return nil, errors.Wrapf(err, "error checking if volume with name %s exists", volume.config.Name) + return nil, errors.Wrapf(err, "checking if volume with name %s exists", volume.config.Name) } if exists { return nil, errors.Wrapf(define.ErrVolumeExists, "volume with name %s already exists", volume.config.Name) @@ -67,9 +67,15 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) if volume.config.Driver == define.VolumeDriverLocal { logrus.Debugf("Validating options for local driver") // Validate options - for key := range volume.config.Options { - switch key { - case "device", "o", "type", "UID", "GID", "SIZE", "INODES": + for key, val := range volume.config.Options { + switch strings.ToLower(key) { + case "device": + if strings.ToLower(volume.config.Options["type"]) == "bind" { + if _, err := os.Stat(val); err != nil { + return nil, errors.Wrapf(err, "invalid volume option %s for driver 'local'", key) + } + } + case "o", "type", "uid", "gid", "size", "inodes": // Do nothing, valid keys default: return nil, errors.Wrapf(define.ErrInvalidArg, "invalid mount option %s for driver 'local'", key) @@ -92,17 +98,17 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) // Create the mountpoint of this volume volPathRoot := filepath.Join(r.config.Engine.VolumePath, volume.config.Name) if err := os.MkdirAll(volPathRoot, 0700); err != nil { - return nil, errors.Wrapf(err, "error creating volume directory %q", volPathRoot) + return nil, errors.Wrapf(err, "creating volume directory %q", volPathRoot) } if err := os.Chown(volPathRoot, volume.config.UID, volume.config.GID); err != nil { - return nil, errors.Wrapf(err, "error chowning volume directory %q to %d:%d", volPathRoot, volume.config.UID, volume.config.GID) + return nil, errors.Wrapf(err, "chowning volume directory %q to %d:%d", volPathRoot, volume.config.UID, volume.config.GID) } fullVolPath := filepath.Join(volPathRoot, "_data") if err := os.MkdirAll(fullVolPath, 0755); err != nil { - return nil, errors.Wrapf(err, "error creating volume directory %q", fullVolPath) + return nil, errors.Wrapf(err, "creating volume directory %q", fullVolPath) } if err := os.Chown(fullVolPath, volume.config.UID, volume.config.GID); err != nil { - return nil, errors.Wrapf(err, "error chowning volume directory %q to %d:%d", fullVolPath, volume.config.UID, volume.config.GID) + return nil, errors.Wrapf(err, "chowning volume directory %q to %d:%d", fullVolPath, volume.config.UID, volume.config.GID) } if err := LabelVolumePath(fullVolPath); err != nil { return nil, err @@ -132,7 +138,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) lock, err := r.lockManager.AllocateLock() if err != nil { - return nil, errors.Wrapf(err, "error allocating lock for new volume") + return nil, errors.Wrapf(err, "allocating lock for new volume") } volume.lock = lock volume.config.LockID = volume.lock.ID() @@ -149,7 +155,7 @@ func (r *Runtime) newVolume(ctx context.Context, options ...VolumeCreateOption) // Add the volume to state if err := r.state.AddVolume(volume); err != nil { - return nil, errors.Wrapf(err, "error adding volume to state") + return nil, errors.Wrapf(err, "adding volume to state") } defer volume.newVolumeEvent(events.Create) return volume, nil @@ -181,7 +187,7 @@ func makeVolumeInPluginIfNotExist(name string, options map[string]string, plugin createReq.Name = name createReq.Options = options if err := plugin.CreateVolume(createReq); err != nil { - return errors.Wrapf(err, "error creating volume %q in plugin %s", name, plugin.Name) + return errors.Wrapf(err, "creating volume %q in plugin %s", name, plugin.Name) } } @@ -225,13 +231,13 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo continue } - return errors.Wrapf(err, "error removing container %s that depends on volume %s", dep, v.Name()) + return errors.Wrapf(err, "removing container %s that depends on volume %s", dep, v.Name()) } logrus.Debugf("Removing container %s (depends on volume %q)", ctr.ID(), v.Name()) if err := r.removeContainer(ctx, ctr, force, false, false, timeout); err != nil { - return errors.Wrapf(err, "error removing container %s that depends on volume %s", ctr.ID(), v.Name()) + return errors.Wrapf(err, "removing container %s that depends on volume %s", ctr.ID(), v.Name()) } } } @@ -244,7 +250,7 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo // them. logrus.Errorf("Unmounting volume %s: %v", v.Name(), err) } else { - return errors.Wrapf(err, "error unmounting volume %s", v.Name()) + return errors.Wrapf(err, "unmounting volume %s", v.Name()) } } @@ -288,13 +294,13 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo if removalErr != nil { logrus.Errorf("Removing volume %s from plugin %s: %v", v.Name(), v.Driver(), removalErr) } - return errors.Wrapf(err, "error removing volume %s", v.Name()) + return errors.Wrapf(err, "removing volume %s", v.Name()) } // Free the volume's lock if err := v.lock.Free(); err != nil { if removalErr == nil { - removalErr = errors.Wrapf(err, "error freeing lock for volume %s", v.Name()) + removalErr = errors.Wrapf(err, "freeing lock for volume %s", v.Name()) } else { logrus.Errorf("Freeing lock for volume %q: %v", v.Name(), err) } @@ -304,7 +310,7 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo // from /var/lib/containers/storage/volumes if err := v.teardownStorage(); err != nil { if removalErr == nil { - removalErr = errors.Wrapf(err, "error cleaning up volume storage for %q", v.Name()) + removalErr = errors.Wrapf(err, "cleaning up volume storage for %q", v.Name()) } else { logrus.Errorf("Cleaning up volume storage for volume %q: %v", v.Name(), err) } diff --git a/libpod/volume_internal.go b/libpod/volume_internal.go index f69f1c044..f9e1ea87d 100644 --- a/libpod/volume_internal.go +++ b/libpod/volume_internal.go @@ -81,7 +81,7 @@ func (v *Volume) save() error { func (v *Volume) refresh() error { lock, err := v.runtime.lockManager.AllocateAndRetrieveLock(v.config.LockID) if err != nil { - return errors.Wrapf(err, "error acquiring lock %d for volume %s", v.config.LockID, v.Name()) + return errors.Wrapf(err, "acquiring lock %d for volume %s", v.config.LockID, v.Name()) } v.lock = lock diff --git a/libpod/volume_internal_linux.go b/libpod/volume_internal_linux.go index 45cd22385..abd31df0f 100644 --- a/libpod/volume_internal_linux.go +++ b/libpod/volume_internal_linux.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/pkg/rootless" pluginapi "github.com/docker/go-plugins-helpers/volume" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -32,13 +31,6 @@ func (v *Volume) mount() error { return nil } - // We cannot mount 'local' volumes as rootless. - if !v.UsesVolumeDriver() && rootless.IsRootless() { - // This check should only be applied to 'local' driver - // so Volume Drivers must be excluded - return errors.Wrapf(define.ErrRootless, "cannot mount volumes without root privileges") - } - // Update the volume from the DB to get an accurate mount counter. if err := v.update(); err != nil { return err @@ -90,22 +82,27 @@ func (v *Volume) mount() error { // TODO: might want to cache this path in the runtime? mountPath, err := exec.LookPath("mount") if err != nil { - return errors.Wrapf(err, "error locating 'mount' binary") + return errors.Wrapf(err, "locating 'mount' binary") } mountArgs := []string{} if volOptions != "" { mountArgs = append(mountArgs, "-o", volOptions) } - if volType != "" { + switch volType { + case "": + case "bind": + mountArgs = append(mountArgs, "-o", volType) + default: mountArgs = append(mountArgs, "-t", volType) } + mountArgs = append(mountArgs, volDevice, v.config.MountPoint) mountCmd := exec.Command(mountPath, mountArgs...) logrus.Debugf("Running mount command: %s %s", mountPath, strings.Join(mountArgs, " ")) if output, err := mountCmd.CombinedOutput(); err != nil { logrus.Debugf("Mount %v failed with %v", mountCmd, err) - return errors.Wrapf(errors.Errorf(string(output)), "error mounting volume %s", v.Name()) + return errors.Errorf(string(output)) } logrus.Debugf("Mounted volume %s", v.Name()) @@ -139,20 +136,6 @@ func (v *Volume) unmount(force bool) error { return nil } - // We cannot unmount 'local' volumes as rootless. - if !v.UsesVolumeDriver() && rootless.IsRootless() { - // If force is set, just clear the counter and bail without - // error, so we can remove volumes from the state if they are in - // an awkward configuration. - if force { - logrus.Errorf("Volume %s is mounted despite being rootless - state is not sane", v.Name()) - v.state.MountCount = 0 - return v.save() - } - - return errors.Wrapf(define.ErrRootless, "cannot mount or unmount volumes without root privileges") - } - if !force { v.state.MountCount-- } else { @@ -184,7 +167,7 @@ func (v *Volume) unmount(force bool) error { // Ignore EINVAL - the mount no longer exists. return nil } - return errors.Wrapf(err, "error unmounting volume %s", v.Name()) + return errors.Wrapf(err, "unmounting volume %s", v.Name()) } logrus.Debugf("Unmounted volume %s", v.Name()) } diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 5a06722ec..ad341c3ab 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -356,6 +356,15 @@ func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error return nil, err } + m, err := json.Marshal(inspect.Mounts) + if err != nil { + return nil, err + } + mounts := []types.MountPoint{} + if err := json.Unmarshal(m, &mounts); err != nil { + return nil, err + } + return &handlers.Container{Container: types.Container{ ID: l.ID(), Names: []string{fmt.Sprintf("/%s", l.Name())}, @@ -374,7 +383,7 @@ func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error }{ "host"}, NetworkSettings: &networkSettings, - Mounts: nil, + Mounts: mounts, }, ContainerCreateConfig: types.ContainerCreateConfig{}, }, nil diff --git a/pkg/api/handlers/compat/events.go b/pkg/api/handlers/compat/events.go index 901acdac4..bc31a36c4 100644 --- a/pkg/api/handlers/compat/events.go +++ b/pkg/api/handlers/compat/events.go @@ -91,6 +91,8 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { e := entities.ConvertToEntitiesEvent(*evt) if !utils.IsLibpodRequest(r) && e.Status == "died" { e.Status = "die" + e.Action = "die" + e.Actor.Attributes["exitCode"] = e.Actor.Attributes["containerExitCode"] } if err := coder.Encode(e); err != nil { diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go index 4533fddeb..c1cc99da4 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -270,9 +270,9 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { return } - authConf, authfile, key, err := auth.GetCredentials(r) + authConf, authfile, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) + utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index 45e4543a9..0fcac5330 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -453,10 +453,10 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { } } - creds, authfile, key, err := auth.GetCredentials(r) + creds, authfile, err := auth.GetCredentials(r) if err != nil { // Credential value(s) not returned as their value is not human readable - utils.BadRequest(w, key.String(), "n/a", err) + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/compat/images_push.go b/pkg/api/handlers/compat/images_push.go index 3a84b5799..04cad204d 100644 --- a/pkg/api/handlers/compat/images_push.go +++ b/pkg/api/handlers/compat/images_push.go @@ -85,9 +85,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) { return } - authconf, authfile, key, err := auth.GetCredentials(r) + authconf, authfile, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) + utils.Error(w, "Something went wrong.", http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/compat/images_search.go b/pkg/api/handlers/compat/images_search.go index e9cc3e2b6..f6ad86a04 100644 --- a/pkg/api/handlers/compat/images_search.go +++ b/pkg/api/handlers/compat/images_search.go @@ -34,9 +34,9 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { return } - _, authfile, key, err := auth.GetCredentials(r) + _, authfile, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) + utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index f2f93434a..6e23845f0 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -497,9 +497,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) { return } - authconf, authfile, key, err := auth.GetCredentials(r) + authconf, authfile, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) + utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/libpod/images_pull.go b/pkg/api/handlers/libpod/images_pull.go index fabdb326b..518e7cc65 100644 --- a/pkg/api/handlers/libpod/images_pull.go +++ b/pkg/api/handlers/libpod/images_pull.go @@ -68,9 +68,9 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { } // Do the auth dance. - authConf, authfile, key, err := auth.GetCredentials(r) + authConf, authfile, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) + utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index 869c83fa3..eb0b6827f 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -176,9 +176,9 @@ func ManifestPush(w http.ResponseWriter, r *http.Request) { } source := utils.GetName(r) - authconf, authfile, key, err := auth.GetCredentials(r) + authconf, authfile, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) + utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) diff --git a/pkg/api/handlers/libpod/play.go b/pkg/api/handlers/libpod/play.go index 312aa32de..6ef83ad92 100644 --- a/pkg/api/handlers/libpod/play.go +++ b/pkg/api/handlers/libpod/play.go @@ -86,9 +86,9 @@ func PlayKube(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error closing temporary file")) return } - authConf, authfile, key, err := auth.GetCredentials(r) + authConf, authfile, err := auth.GetCredentials(r) if err != nil { - utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, errors.Wrapf(err, "failed to parse %q header for %s", key, r.URL.String())) + utils.Error(w, "failed to retrieve repository credentials", http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) 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/api/server/docs.go b/pkg/api/server/docs.go index 83d9ef160..2127e7d82 100644 --- a/pkg/api/server/docs.go +++ b/pkg/api/server/docs.go @@ -1,4 +1,4 @@ -// Package api Provides an API for the Libpod library +// Package api Provides an API for the Libpod library // // This documentation describes the Podman v2.0 RESTful API. // It replaces the Podman v1.0 API and was initially delivered @@ -45,7 +45,7 @@ // Schemes: http, https // Host: podman.io // BasePath: / -// Version: 3.2.0 +// Version: 4.0.0 // License: Apache-2.0 https://opensource.org/licenses/Apache-2.0 // Contact: Podman <podman@lists.podman.io> https://podman.io/community/ // diff --git a/pkg/api/server/register_networks.go b/pkg/api/server/register_networks.go index 344486299..77e8a80fd 100644 --- a/pkg/api/server/register_networks.go +++ b/pkg/api/server/register_networks.go @@ -101,7 +101,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // parameters: // - in: body // name: create - // description: attributes for creating a container + // description: attributes for creating a network // schema: // $ref: "#/definitions/NetworkCreateRequest" // responses: @@ -312,7 +312,7 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // parameters: // - in: body // name: create - // description: attributes for creating a container + // description: attributes for creating a network // schema: // $ref: "#/definitions/NetworkCreateLibpod" // responses: diff --git a/pkg/api/server/register_volumes.go b/pkg/api/server/register_volumes.go index fb02cffcf..d1c1d5024 100644 --- a/pkg/api/server/register_volumes.go +++ b/pkg/api/server/register_volumes.go @@ -17,7 +17,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // parameters: // - in: body // name: create - // description: attributes for creating a container + // description: attributes for creating a volume // schema: // $ref: "#/definitions/VolumeCreate" // produces: @@ -188,7 +188,7 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error { // - in: body // name: create // description: | - // attributes for creating a container. + // attributes for creating a volume. // Note: If a volume by the same name exists, a 201 response with that volume's information will be generated. // schema: // $ref: "#/definitions/DockerVolumeCreate" diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 070e222ad..f423c011d 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -3,7 +3,6 @@ package auth import ( "encoding/base64" "encoding/json" - "fmt" "io/ioutil" "net/http" "os" @@ -16,52 +15,70 @@ import ( "github.com/sirupsen/logrus" ) -type HeaderAuthName string - -func (h HeaderAuthName) String() string { return string(h) } - -// XRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header. -// This header supports one registry per header occurrence. To support N registries provided N headers, one per registry. +// xRegistryAuthHeader is the key to the encoded registry authentication configuration in an http-request header. +// This header supports one registry per header occurrence. To support N registries provide N headers, one per registry. // As of Docker API 1.40 and Libpod API 1.0.0, this header is supported by all endpoints. -const XRegistryAuthHeader HeaderAuthName = "X-Registry-Auth" +const xRegistryAuthHeader = "X-Registry-Auth" -// XRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header. +// xRegistryConfigHeader is the key to the encoded registry authentication configuration in an http-request header. // This header supports N registries in one header via a Base64 encoded, JSON map. // As of Docker API 1.40 and Libpod API 2.0.0, this header is supported by build endpoints. -const XRegistryConfigHeader HeaderAuthName = "X-Registry-Config" +const xRegistryConfigHeader = "X-Registry-Config" // GetCredentials queries the http.Request for X-Registry-.* headers and extracts -// the necessary authentication information for libpod operations -func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, HeaderAuthName, error) { - has := func(key HeaderAuthName) bool { hdr, found := r.Header[string(key)]; return found && len(hdr) > 0 } - switch { - case has(XRegistryConfigHeader): - c, f, err := getConfigCredentials(r) - return c, f, XRegistryConfigHeader, err - case has(XRegistryAuthHeader): - c, f, err := getAuthCredentials(r) - return c, f, XRegistryAuthHeader, err - } - return nil, "", "", nil +// the necessary authentication information for libpod operations, possibly +// creating a config file. If that is the case, the caller must call RemoveAuthFile. +func GetCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { + nonemptyHeaderValue := func(key string) ([]string, bool) { + hdr := r.Header.Values(key) + return hdr, len(hdr) > 0 + } + var override *types.DockerAuthConfig + var fileContents map[string]types.DockerAuthConfig + var headerName string + var err error + if hdr, ok := nonemptyHeaderValue(xRegistryConfigHeader); ok { + headerName = xRegistryConfigHeader + override, fileContents, err = getConfigCredentials(r, hdr) + } else if hdr, ok := nonemptyHeaderValue(xRegistryAuthHeader); ok { + headerName = xRegistryAuthHeader + override, fileContents, err = getAuthCredentials(hdr) + } else { + return nil, "", nil + } + if err != nil { + return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String()) + } + + var authFile string + if fileContents == nil { + authFile = "" + } else { + authFile, err = authConfigsToAuthFile(fileContents) + if err != nil { + return nil, "", errors.Wrapf(err, "failed to parse %q header for %s", headerName, r.URL.String()) + } + } + return override, authFile, nil } -// getConfigCredentials extracts one or more docker.AuthConfig from the request's -// header. An empty key will be used as default while a named registry will be +// getConfigCredentials extracts one or more docker.AuthConfig from a request and its +// xRegistryConfigHeader value. An empty key will be used as default while a named registry will be // returned as types.DockerAuthConfig -func getConfigCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { +func getConfigCredentials(r *http.Request, headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) { var auth *types.DockerAuthConfig configs := make(map[string]types.DockerAuthConfig) - for _, h := range r.Header[string(XRegistryConfigHeader)] { + for _, h := range headers { param, err := base64.URLEncoding.DecodeString(h) if err != nil { - return nil, "", errors.Wrapf(err, "failed to decode %q", XRegistryConfigHeader) + return nil, nil, errors.Wrapf(err, "failed to decode %q", xRegistryConfigHeader) } ac := make(map[string]dockerAPITypes.AuthConfig) err = json.Unmarshal(param, &ac) if err != nil { - return nil, "", errors.Wrapf(err, "failed to unmarshal %q", XRegistryConfigHeader) + return nil, nil, errors.Wrapf(err, "failed to unmarshal %q", xRegistryConfigHeader) } for k, v := range ac { @@ -91,79 +108,45 @@ func getConfigCredentials(r *http.Request) (*types.DockerAuthConfig, string, err if auth == nil { logrus.Debugf("%q header found in request, but \"registry=%v\" query parameter not provided", - XRegistryConfigHeader, registries) + xRegistryConfigHeader, registries) } else { - logrus.Debugf("%q header found in request for username %q", XRegistryConfigHeader, auth.Username) + logrus.Debugf("%q header found in request for username %q", xRegistryConfigHeader, auth.Username) } } - authfile, err := authConfigsToAuthFile(configs) - return auth, authfile, err + return auth, configs, nil } -// getAuthCredentials extracts one or more DockerAuthConfigs from the request's -// header. The header could specify a single-auth config in which case the +// getAuthCredentials extracts one or more DockerAuthConfigs from an xRegistryAuthHeader +// value. The header could specify a single-auth config in which case the // first return value is set. In case of a multi-auth header, the contents are -// stored in a temporary auth file (2nd return value). Note that the auth file -// should be removed after usage. -func getAuthCredentials(r *http.Request) (*types.DockerAuthConfig, string, error) { +// returned in the second return value. +func getAuthCredentials(headers []string) (*types.DockerAuthConfig, map[string]types.DockerAuthConfig, error) { + authHeader := headers[0] + // First look for a multi-auth header (i.e., a map). - authConfigs, err := multiAuthHeader(r) + authConfigs, err := parseMultiAuthHeader(authHeader) if err == nil { - authfile, err := authConfigsToAuthFile(authConfigs) - return nil, authfile, err + return nil, authConfigs, nil } // Fallback to looking for a single-auth header (i.e., one config). - authConfigs, err = singleAuthHeader(r) - if err != nil { - return nil, "", err - } - var conf *types.DockerAuthConfig - for k := range authConfigs { - c := authConfigs[k] - conf = &c - break - } - return conf, "", nil -} - -// Header builds the requested Authentication Header -func Header(sys *types.SystemContext, headerName HeaderAuthName, authfile, username, password string) (map[string]string, error) { - var ( - content string - err error - ) - switch headerName { - case XRegistryAuthHeader: - content, err = headerAuth(sys, authfile, username, password) - case XRegistryConfigHeader: - content, err = headerConfig(sys, authfile, username, password) - default: - err = fmt.Errorf("unsupported authentication header: %q", headerName) - } + authConfig, err := parseSingleAuthHeader(authHeader) if err != nil { - return nil, err + return nil, nil, err } - - if len(content) > 0 { - return map[string]string{string(headerName): content}, nil - } - return nil, nil + return &authConfig, nil, nil } -// headerConfig returns a map with the XRegistryConfigHeader set which can +// MakeXRegistryConfigHeader returns a map with the "X-Registry-Config" header set, which can // conveniently be used in the http stack. -func headerConfig(sys *types.SystemContext, authfile, username, password string) (string, error) { +func MakeXRegistryConfigHeader(sys *types.SystemContext, username, password string) (map[string]string, error) { if sys == nil { sys = &types.SystemContext{} } - if authfile != "" { - sys.AuthFilePath = authfile - } authConfigs, err := imageAuth.GetAllCredentials(sys) if err != nil { - return "", err + return nil, err } if username != "" { @@ -174,29 +157,38 @@ func headerConfig(sys *types.SystemContext, authfile, username, password string) } if len(authConfigs) == 0 { - return "", nil + return nil, nil } - return encodeMultiAuthConfigs(authConfigs) + content, err := encodeMultiAuthConfigs(authConfigs) + if err != nil { + return nil, err + } + return map[string]string{xRegistryConfigHeader: content}, nil } -// headerAuth returns a base64 encoded map with the XRegistryAuthHeader set which can +// MakeXRegistryAuthHeader returns a map with the "X-Registry-Auth" header set, which can // conveniently be used in the http stack. -func headerAuth(sys *types.SystemContext, authfile, username, password string) (string, error) { +func MakeXRegistryAuthHeader(sys *types.SystemContext, username, password string) (map[string]string, error) { if username != "" { - return encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password}) + content, err := encodeSingleAuthConfig(types.DockerAuthConfig{Username: username, Password: password}) + if err != nil { + return nil, err + } + return map[string]string{xRegistryAuthHeader: content}, nil } if sys == nil { sys = &types.SystemContext{} } - if authfile != "" { - sys.AuthFilePath = authfile - } authConfigs, err := imageAuth.GetAllCredentials(sys) if err != nil { - return "", err + return nil, err + } + content, err := encodeMultiAuthConfigs(authConfigs) + if err != nil { + return nil, err } - return encodeMultiAuthConfigs(authConfigs) + return map[string]string{xRegistryAuthHeader: content}, nil } // RemoveAuthfile is a convenience function that is meant to be called in a @@ -258,34 +250,38 @@ func authConfigsToAuthFile(authConfigs map[string]types.DockerAuthConfig) (strin // Now use the c/image packages to store the credentials. It's battle // tested, and we make sure to use the same code as the image backend. sys := types.SystemContext{AuthFilePath: authFilePath} - for server, config := range authConfigs { - server = normalize(server) + for authFileKey, config := range authConfigs { + key := normalizeAuthFileKey(authFileKey) // Note that we do not validate the credentials here. We assume // that all credentials are valid. They'll be used on demand // later. - if err := imageAuth.SetAuthentication(&sys, server, config.Username, config.Password); err != nil { - return "", errors.Wrapf(err, "error storing credentials in temporary auth file (server: %q, user: %q)", server, config.Username) + if err := imageAuth.SetAuthentication(&sys, key, config.Username, config.Password); err != nil { + return "", errors.Wrapf(err, "error storing credentials in temporary auth file (key: %q / %q, user: %q)", authFileKey, key, config.Username) } } return authFilePath, nil } -// normalize takes a server and removes the leading "http[s]://" prefix as well -// as removes path suffixes from docker registries. -func normalize(server string) string { - stripped := strings.TrimPrefix(server, "http://") +// normalizeAuthFileKey takes an auth file key and converts it into a new-style credential key +// in the canonical format, as interpreted by c/image/pkg/docker/config. +func normalizeAuthFileKey(authFileKey string) string { + stripped := strings.TrimPrefix(authFileKey, "http://") stripped = strings.TrimPrefix(stripped, "https://") - /// Normalize docker registries - if strings.HasPrefix(stripped, "index.docker.io/") || - strings.HasPrefix(stripped, "registry-1.docker.io/") || - strings.HasPrefix(stripped, "docker.io/") { + if stripped != authFileKey { // URLs are interpreted to mean complete registries stripped = strings.SplitN(stripped, "/", 2)[0] } - return stripped + // Only non-namespaced registry names (or URLs) need to be normalized; repo namespaces + // always use the simple format. + switch stripped { + case "registry-1.docker.io", "index.docker.io": + return "docker.io" + default: + return stripped + } } // dockerAuthToImageAuth converts a docker auth config to one we're using @@ -309,28 +305,26 @@ func imageAuthToDockerAuth(authConfig types.DockerAuthConfig) dockerAPITypes.Aut } } -// singleAuthHeader extracts a DockerAuthConfig from the request's header. +// parseSingleAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value. // The header content is a single DockerAuthConfig. -func singleAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) { - authHeader := r.Header.Get(string(XRegistryAuthHeader)) - authConfig := dockerAPITypes.AuthConfig{} +func parseSingleAuthHeader(authHeader string) (types.DockerAuthConfig, error) { // Accept "null" and handle it as empty value for compatibility reason with Docker. // Some java docker clients pass this value, e.g. this one used in Eclipse. - if len(authHeader) > 0 && authHeader != "null" { - authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader)) - if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil { - return nil, err - } + if len(authHeader) == 0 || authHeader == "null" { + return types.DockerAuthConfig{}, nil } - authConfigs := make(map[string]types.DockerAuthConfig) - authConfigs["0"] = dockerAuthToImageAuth(authConfig) - return authConfigs, nil + + authConfig := dockerAPITypes.AuthConfig{} + authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authHeader)) + if err := json.NewDecoder(authJSON).Decode(&authConfig); err != nil { + return types.DockerAuthConfig{}, err + } + return dockerAuthToImageAuth(authConfig), nil } -// multiAuthHeader extracts a DockerAuthConfig from the request's header. +// parseMultiAuthHeader extracts a DockerAuthConfig from an xRegistryAuthHeader value. // The header content is a map[string]DockerAuthConfigs. -func multiAuthHeader(r *http.Request) (map[string]types.DockerAuthConfig, error) { - authHeader := r.Header.Get(string(XRegistryAuthHeader)) +func parseMultiAuthHeader(authHeader string) (map[string]types.DockerAuthConfig, error) { // Accept "null" and handle it as empty value for compatibility reason with Docker. // Some java docker clients pass this value, e.g. this one used in Eclipse. if len(authHeader) == 0 || authHeader == "null" { diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go index da2d9a5c5..f7e6e4ef6 100644 --- a/pkg/auth/auth_test.go +++ b/pkg/auth/auth_test.go @@ -1,13 +1,302 @@ package auth import ( + "encoding/base64" + "encoding/json" "io/ioutil" + "net/http" + "os" "testing" + "github.com/containers/image/v5/pkg/docker/config" "github.com/containers/image/v5/types" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +const largeAuthFile = `{"auths":{ + "docker.io/vendor": {"auth": "ZG9ja2VyOnZlbmRvcg=="}, + "https://index.docker.io/v1": {"auth": "ZG9ja2VyOnRvcA=="}, + "quay.io/libpod": {"auth": "cXVheTpsaWJwb2Q="}, + "quay.io": {"auth": "cXVheTp0b3A="} +}}` + +// Semantics of largeAuthFile +var largeAuthFileValues = map[string]types.DockerAuthConfig{ + "docker.io/vendor": {Username: "docker", Password: "vendor"}, + "docker.io": {Username: "docker", Password: "top"}, + "quay.io/libpod": {Username: "quay", Password: "libpod"}, + "quay.io": {Username: "quay", Password: "top"}, +} + +// systemContextForAuthFile returns a types.SystemContext with AuthFilePath pointing +// to a temporary file with fileContents, or nil if fileContents is empty; and a cleanup +// function the calle rmust arrange to call. +func systemContextForAuthFile(t *testing.T, fileContents string) (*types.SystemContext, func()) { + if fileContents == "" { + return nil, func() {} + } + + f, err := ioutil.TempFile("", "auth.json") + require.NoError(t, err) + path := f.Name() + err = ioutil.WriteFile(path, []byte(fileContents), 0700) + require.NoError(t, err) + return &types.SystemContext{AuthFilePath: path}, func() { os.Remove(path) } +} + +// Test that GetCredentials() correctly parses what MakeXRegistryConfigHeader() produces +func TestMakeXRegistryConfigHeaderGetCredentialsRoundtrip(t *testing.T) { + for _, tc := range []struct { + name string + fileContents string + username, password string + expectedOverride *types.DockerAuthConfig + expectedFileValues map[string]types.DockerAuthConfig + }{ + { + name: "no data", + fileContents: "", + username: "", + password: "", + expectedOverride: nil, + expectedFileValues: nil, + }, + { + name: "file data", + fileContents: largeAuthFile, + username: "", + password: "", + expectedOverride: nil, + expectedFileValues: largeAuthFileValues, + }, + { + name: "file data + override", + fileContents: largeAuthFile, + username: "override-user", + password: "override-pass", + expectedOverride: &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"}, + expectedFileValues: largeAuthFileValues, + }, + } { + sys, cleanup := systemContextForAuthFile(t, tc.fileContents) + defer cleanup() + headers, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPost, "/", nil) + require.NoError(t, err, tc.name) + for k, v := range headers { + req.Header.Set(k, v) + } + + override, resPath, err := GetCredentials(req) + require.NoError(t, err, tc.name) + defer RemoveAuthfile(resPath) + if tc.expectedOverride == nil { + assert.Nil(t, override, tc.name) + } else { + require.NotNil(t, override, tc.name) + assert.Equal(t, *tc.expectedOverride, *override, tc.name) + } + for key, expectedAuth := range tc.expectedFileValues { + auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key) + require.NoError(t, err, tc.name) + assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key) + } + } +} + +// Test that GetCredentials() correctly parses what MakeXRegistryAuthHeader() produces +func TestMakeXRegistryAuthHeaderGetCredentialsRoundtrip(t *testing.T) { + for _, tc := range []struct { + name string + fileContents string + username, password string + expectedOverride *types.DockerAuthConfig + expectedFileValues map[string]types.DockerAuthConfig + }{ + { + name: "override", + fileContents: "", + username: "override-user", + password: "override-pass", + expectedOverride: &types.DockerAuthConfig{Username: "override-user", Password: "override-pass"}, + expectedFileValues: nil, + }, + { + name: "file data", + fileContents: largeAuthFile, + username: "", + password: "", + expectedFileValues: largeAuthFileValues, + }, + } { + sys, cleanup := systemContextForAuthFile(t, tc.fileContents) + defer cleanup() + headers, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPost, "/", nil) + require.NoError(t, err, tc.name) + for k, v := range headers { + req.Header.Set(k, v) + } + + override, resPath, err := GetCredentials(req) + require.NoError(t, err, tc.name) + defer RemoveAuthfile(resPath) + if tc.expectedOverride == nil { + assert.Nil(t, override, tc.name) + } else { + require.NotNil(t, override, tc.name) + assert.Equal(t, *tc.expectedOverride, *override, tc.name) + } + for key, expectedAuth := range tc.expectedFileValues { + auth, err := config.GetCredentials(&types.SystemContext{AuthFilePath: resPath}, key) + require.NoError(t, err, tc.name) + assert.Equal(t, expectedAuth, auth, "%s, key %s", tc.name, key) + } + } +} + +func TestMakeXRegistryConfigHeader(t *testing.T) { + for _, tc := range []struct { + name string + fileContents string + username, password string + shouldErr bool + expectedContents string + }{ + { + name: "no data", + fileContents: "", + username: "", + password: "", + expectedContents: "", + }, + { + name: "invalid JSON", + fileContents: "@invalid JSON", + username: "", + password: "", + shouldErr: true, + }, + { + name: "file data", + fileContents: largeAuthFile, + username: "", + password: "", + expectedContents: `{ + "docker.io/vendor": {"username": "docker", "password": "vendor"}, + "docker.io": {"username": "docker", "password": "top"}, + "quay.io/libpod": {"username": "quay", "password": "libpod"}, + "quay.io": {"username": "quay", "password": "top"} + }`, + }, + { + name: "file data + override", + fileContents: largeAuthFile, + username: "override-user", + password: "override-pass", + expectedContents: `{ + "docker.io/vendor": {"username": "docker", "password": "vendor"}, + "docker.io": {"username": "docker", "password": "top"}, + "quay.io/libpod": {"username": "quay", "password": "libpod"}, + "quay.io": {"username": "quay", "password": "top"}, + "": {"username": "override-user", "password": "override-pass"} + }`, + }, + } { + sys, cleanup := systemContextForAuthFile(t, tc.fileContents) + defer cleanup() + res, err := MakeXRegistryConfigHeader(sys, tc.username, tc.password) + if tc.shouldErr { + assert.Error(t, err, tc.name) + } else { + require.NoError(t, err, tc.name) + if tc.expectedContents == "" { + assert.Empty(t, res, tc.name) + } else { + require.Len(t, res, 1, tc.name) + header, ok := res[xRegistryConfigHeader] + require.True(t, ok, tc.name) + decodedHeader, err := base64.URLEncoding.DecodeString(header) + require.NoError(t, err, tc.name) + // Don't test for a specific JSON representation, just for the expected contents. + expected := map[string]interface{}{} + actual := map[string]interface{}{} + err = json.Unmarshal([]byte(tc.expectedContents), &expected) + require.NoError(t, err, tc.name) + err = json.Unmarshal(decodedHeader, &actual) + require.NoError(t, err, tc.name) + assert.Equal(t, expected, actual, tc.name) + } + } + } +} + +func TestMakeXRegistryAuthHeader(t *testing.T) { + for _, tc := range []struct { + name string + fileContents string + username, password string + shouldErr bool + expectedContents string + }{ + { + name: "override", + fileContents: "", + username: "override-user", + password: "override-pass", + expectedContents: `{"username": "override-user", "password": "override-pass"}`, + }, + { + name: "invalid JSON", + fileContents: "@invalid JSON", + username: "", + password: "", + shouldErr: true, + }, + { + name: "file data", + fileContents: largeAuthFile, + username: "", + password: "", + expectedContents: `{ + "docker.io/vendor": {"username": "docker", "password": "vendor"}, + "docker.io": {"username": "docker", "password": "top"}, + "quay.io/libpod": {"username": "quay", "password": "libpod"}, + "quay.io": {"username": "quay", "password": "top"} + }`, + }, + } { + sys, cleanup := systemContextForAuthFile(t, tc.fileContents) + defer cleanup() + res, err := MakeXRegistryAuthHeader(sys, tc.username, tc.password) + if tc.shouldErr { + assert.Error(t, err, tc.name) + } else { + require.NoError(t, err, tc.name) + if tc.expectedContents == "" { + assert.Empty(t, res, tc.name) + } else { + require.Len(t, res, 1, tc.name) + header, ok := res[xRegistryAuthHeader] + require.True(t, ok, tc.name) + decodedHeader, err := base64.URLEncoding.DecodeString(header) + require.NoError(t, err, tc.name) + // Don't test for a specific JSON representation, just for the expected contents. + expected := map[string]interface{}{} + actual := map[string]interface{}{} + err = json.Unmarshal([]byte(tc.expectedContents), &expected) + require.NoError(t, err, tc.name) + err = json.Unmarshal(decodedHeader, &actual) + require.NoError(t, err, tc.name) + assert.Equal(t, expected, actual, tc.name) + } + } + } +} + func TestAuthConfigsToAuthFile(t *testing.T) { for _, tc := range []struct { name string @@ -22,28 +311,28 @@ func TestAuthConfigsToAuthFile(t *testing.T) { expectedContains: "{}", }, { - name: "registry with prefix", + name: "registry with a namespace prefix", server: "my-registry.local/username", shouldErr: false, expectedContains: `"my-registry.local/username":`, }, { - name: "normalize https:// prefix", + name: "URLs are interpreted as full registries", server: "http://my-registry.local/username", shouldErr: false, - expectedContains: `"my-registry.local/username":`, + expectedContains: `"my-registry.local":`, }, { - name: "normalize docker registry with https prefix", + name: "the old-style docker registry URL is normalized", server: "http://index.docker.io/v1/", shouldErr: false, - expectedContains: `"index.docker.io":`, + expectedContains: `"docker.io":`, }, { - name: "normalize docker registry without https prefix", - server: "docker.io/v2/", + name: "docker.io vendor namespace", + server: "docker.io/vendor", shouldErr: false, - expectedContains: `"docker.io":`, + expectedContains: `"docker.io/vendor":`, }, } { configs := map[string]types.DockerAuthConfig{} @@ -54,13 +343,79 @@ func TestAuthConfigsToAuthFile(t *testing.T) { filePath, err := authConfigsToAuthFile(configs) if tc.shouldErr { - assert.NotNil(t, err) + assert.Error(t, err) assert.Empty(t, filePath) } else { - assert.Nil(t, err) + assert.NoError(t, err) content, err := ioutil.ReadFile(filePath) - assert.Nil(t, err) + require.NoError(t, err) assert.Contains(t, string(content), tc.expectedContains) + os.Remove(filePath) + } + } +} + +func TestParseSingleAuthHeader(t *testing.T) { + for _, tc := range []struct { + input string + shouldErr bool + expected types.DockerAuthConfig + }{ + { + input: "", // An empty (or missing) header + expected: types.DockerAuthConfig{}, + }, + { + input: "null", + expected: types.DockerAuthConfig{}, + }, + // Invalid JSON + {input: "@", shouldErr: true}, + // Success + { + input: base64.URLEncoding.EncodeToString([]byte(`{"username":"u1","password":"p1"}`)), + expected: types.DockerAuthConfig{Username: "u1", Password: "p1"}, + }, + } { + res, err := parseSingleAuthHeader(tc.input) + if tc.shouldErr { + assert.Error(t, err, tc.input) + } else { + require.NoError(t, err, tc.input) + assert.Equal(t, tc.expected, res, tc.input) + } + } +} + +func TestParseMultiAuthHeader(t *testing.T) { + for _, tc := range []struct { + input string + shouldErr bool + expected map[string]types.DockerAuthConfig + }{ + // Empty header + {input: "", expected: nil}, + // "null" + {input: "null", expected: nil}, + // Invalid JSON + {input: "@", shouldErr: true}, + // Success + { + input: base64.URLEncoding.EncodeToString([]byte( + `{"https://index.docker.io/v1/":{"username":"u1","password":"p1"},` + + `"quay.io/libpod":{"username":"u2","password":"p2"}}`)), + expected: map[string]types.DockerAuthConfig{ + "https://index.docker.io/v1/": {Username: "u1", Password: "p1"}, + "quay.io/libpod": {Username: "u2", Password: "p2"}, + }, + }, + } { + res, err := parseMultiAuthHeader(tc.input) + if tc.shouldErr { + assert.Error(t, err, tc.input) + } else { + require.NoError(t, err, tc.input) + assert.Equal(t, tc.expected, res, tc.input) } } } diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index be6e5ab55..7bca43132 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -293,14 +293,10 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO headers map[string]string err error ) - if options.SystemContext == nil { - headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, "", "", "") + if options.SystemContext != nil && options.SystemContext.DockerAuthConfig != nil { + headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password) } else { - if options.SystemContext.DockerAuthConfig != nil { - headers, err = auth.Header(options.SystemContext, auth.XRegistryAuthHeader, options.SystemContext.AuthFilePath, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password) - } else { - headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, options.SystemContext.AuthFilePath, "", "") - } + headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "") } if err != nil { return nil, err diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index dfb500772..152ff0cde 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -8,6 +8,7 @@ import ( "net/url" "strconv" + imageTypes "github.com/containers/image/v5/types" "github.com/containers/podman/v3/pkg/api/handlers/types" "github.com/containers/podman/v3/pkg/auth" "github.com/containers/podman/v3/pkg/bindings" @@ -280,7 +281,7 @@ func Push(ctx context.Context, source string, destination string, options *PushO return err } // TODO: have a global system context we can pass around (1st argument) - header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword()) + header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword()) if err != nil { return err } @@ -329,7 +330,7 @@ func Search(ctx context.Context, term string, options *SearchOptions) ([]entitie } // TODO: have a global system context we can pass around (1st argument) - header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), "", "") + header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, "", "") if err != nil { return nil, err } diff --git a/pkg/bindings/images/pull.go b/pkg/bindings/images/pull.go index be21aa593..ac583973f 100644 --- a/pkg/bindings/images/pull.go +++ b/pkg/bindings/images/pull.go @@ -10,6 +10,7 @@ import ( "os" "strconv" + "github.com/containers/image/v5/types" "github.com/containers/podman/v3/pkg/auth" "github.com/containers/podman/v3/pkg/bindings" "github.com/containers/podman/v3/pkg/domain/entities" @@ -42,7 +43,7 @@ func Pull(ctx context.Context, rawImage string, options *PullOptions) ([]string, } // TODO: have a global system context we can pass around (1st argument) - header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword()) + header, err := auth.MakeXRegistryAuthHeader(&types.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword()) if err != nil { return nil, err } diff --git a/pkg/bindings/play/play.go b/pkg/bindings/play/play.go index 2cd7c3997..111a25cac 100644 --- a/pkg/bindings/play/play.go +++ b/pkg/bindings/play/play.go @@ -6,6 +6,7 @@ import ( "os" "strconv" + "github.com/containers/image/v5/types" "github.com/containers/podman/v3/pkg/auth" "github.com/containers/podman/v3/pkg/bindings" "github.com/containers/podman/v3/pkg/domain/entities" @@ -40,7 +41,7 @@ func Kube(ctx context.Context, path string, options *KubeOptions) (*entities.Pla } // TODO: have a global system context we can pass around (1st argument) - header, err := auth.Header(nil, auth.XRegistryAuthHeader, options.GetAuthfile(), options.GetUsername(), options.GetPassword()) + header, err := auth.MakeXRegistryAuthHeader(&types.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword()) if err != nil { return nil, err } 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/machine/ignition.go b/pkg/machine/ignition.go index 139318977..84d3be296 100644 --- a/pkg/machine/ignition.go +++ b/pkg/machine/ignition.go @@ -7,7 +7,10 @@ import ( "fmt" "io/ioutil" "net/url" + "os" "path/filepath" + + "github.com/sirupsen/logrus" ) /* @@ -355,6 +358,56 @@ machine_enabled=true }, }) + // get certs for current user + userHome, err := os.UserHomeDir() + if err != nil { + logrus.Warnf("Unable to copy certs via ignition %s", err.Error()) + return files + } + + certFiles := getCerts(filepath.Join(userHome, ".config/containers/certs.d")) + files = append(files, certFiles...) + + certFiles = getCerts(filepath.Join(userHome, ".config/docker/certs.d")) + files = append(files, certFiles...) + + return files +} + +func getCerts(certsDir string) []File { + var ( + files []File + ) + + certs, err := ioutil.ReadDir(certsDir) + if err == nil { + for _, cert := range certs { + b, err := ioutil.ReadFile(filepath.Join(certsDir, cert.Name())) + if err != nil { + logrus.Warnf("Unable to read cert file %s", err.Error()) + continue + } + files = append(files, File{ + Node: Node{ + Group: getNodeGrp("root"), + Path: filepath.Join("/etc/containers/certs.d/", cert.Name()), + User: getNodeUsr("root"), + }, + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: encodeDataURLPtr(string(b)), + }, + Mode: intToPtr(0644), + }, + }) + } + } else { + if !os.IsNotExist(err) { + logrus.Warnf("Unable to copy certs via ignition, error while reading certs from %s: %s", certsDir, err.Error()) + } + } + return files } 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/pkg/util/utils.go b/pkg/util/utils.go index 390057c32..11edf265f 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -665,8 +665,8 @@ func CreateCidFile(cidfile string, id string) error { return nil } -// DefaultCPUPeriod is the default CPU period is 100us, which is the same default -// as Kubernetes. +// DefaultCPUPeriod is the default CPU period (100ms) in microseconds, which is +// the same default as Kubernetes. const DefaultCPUPeriod uint64 = 100000 // CoresToPeriodAndQuota converts a fraction of cores to the equivalent diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index 5a02ca3cb..554a905d4 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -18,7 +18,7 @@ podman rm -a -f &>/dev/null t GET "libpod/containers/json (at start: clean slate)" 200 length=0 -podman run $IMAGE true +podman run -v /tmp:/tmp $IMAGE true t GET libpod/containers/json 200 length=0 @@ -33,6 +33,7 @@ t GET libpod/containers/json?all=true 200 \ .[0].Command[0]="true" \ .[0].State~\\\(exited\\\|stopped\\\) \ .[0].ExitCode=0 \ + .[0].Mounts~.*/tmp \ .[0].IsInfra=false # Test compat API for Network Settings (.Network is N/A when rootless) @@ -44,6 +45,7 @@ t GET /containers/json?all=true 200 \ length=1 \ .[0].Id~[0-9a-f]\\{64\\} \ .[0].Image=$IMAGE \ + .[0].Mounts~.*/tmp \ $network_expect # compat API imageid with sha256: prefix diff --git a/test/apiv2/27-containersEvents.at b/test/apiv2/27-containersEvents.at new file mode 100644 index 000000000..a86f2e353 --- /dev/null +++ b/test/apiv2/27-containersEvents.at @@ -0,0 +1,27 @@ +# -*- sh -*- +# +# test container-related events +# + +podman pull $IMAGE &>/dev/null + +# Ensure clean slate +podman rm -a -f &>/dev/null + +START=$(date +%s) + +podman run $IMAGE false || true + +# libpod api +t GET "libpod/events?stream=false&since=$START" 200 \ + 'select(.status | contains("start")).Action=start' \ + 'select(.status | contains("died")).Action=died' \ + 'select(.status | contains("died")).Actor.Attributes.containerExitCode=1' + +# compat api, uses status=die (#12643) +t GET "events?stream=false&since=$START" 200 \ + 'select(.status | contains("start")).Action=start' \ + 'select(.status | contains("die")).Action=die' \ + 'select(.status | contains("die")).Actor.Attributes.exitCode=1' + +# vim: filetype=sh 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/e2e/pod_initcontainers_test.go b/test/e2e/pod_initcontainers_test.go index 11e7ca400..e73f28a7a 100644 --- a/test/e2e/pod_initcontainers_test.go +++ b/test/e2e/pod_initcontainers_test.go @@ -135,7 +135,7 @@ var _ = Describe("Podman init containers", func() { filename := filepath.Join("/dev/shm", RandomString(12)) // Write the date to a file - session := podmanTest.Podman([]string{"create", "--init-ctr", "always", "--pod", "new:foobar", ALPINE, "bin/sh", "-c", fmt.Sprintf("date > %s", filename)}) + session := podmanTest.Podman([]string{"create", "--init-ctr", "always", "--pod", "new:foobar", fedoraMinimal, "bin/sh", "-c", "date +%T.%N > " + filename}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) verify := podmanTest.Podman([]string{"create", "--pod", "foobar", "-t", ALPINE, "top"}) diff --git a/test/e2e/run_staticip_test.go b/test/e2e/run_staticip_test.go index eb7dc9d11..2f3c3025a 100644 --- a/test/e2e/run_staticip_test.go +++ b/test/e2e/run_staticip_test.go @@ -7,6 +7,7 @@ import ( "time" . "github.com/containers/podman/v3/test/utils" + "github.com/containers/storage/pkg/stringid" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" @@ -65,6 +66,20 @@ var _ = Describe("Podman run with --ip flag", func() { Expect(result.OutputToString()).To(ContainSubstring(ip + "/16")) }) + It("Podman run with specified static IPv6 has correct IP", func() { + netName := "ipv6-" + stringid.GenerateNonCryptoID() + ipv6 := "fd46:db93:aa76:ac37::10" + net := podmanTest.Podman([]string{"network", "create", "--subnet", "fd46:db93:aa76:ac37::/64", netName}) + net.WaitWithDefaultTimeout() + defer podmanTest.removeCNINetwork(netName) + Expect(net).To(Exit(0)) + + result := podmanTest.Podman([]string{"run", "-ti", "--network", netName, "--ip6", ipv6, ALPINE, "ip", "addr"}) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + Expect(result.OutputToString()).To(ContainSubstring(ipv6 + "/64")) + }) + It("Podman run with --network bridge:ip=", func() { ip := GetRandomIPAddress() result := podmanTest.Podman([]string{"run", "-ti", "--network", "bridge:ip=" + ip, ALPINE, "ip", "addr"}) diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 8db23080e..e98f2c999 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -1315,7 +1315,7 @@ USER mail`, BB) Expect(err).To(BeNil()) file.Close() - session := podmanTest.Podman([]string{"run", "-dt", "--restart", "always", "-v", fmt.Sprintf("%s:/tmp/runroot:Z", testDir), ALPINE, "sh", "-c", "date +%N > /tmp/runroot/ran && while test -r /tmp/runroot/running; do sleep 0.1s; done"}) + session := podmanTest.Podman([]string{"run", "-dt", "--restart", "always", "-v", fmt.Sprintf("%s:/tmp/runroot:Z", testDir), ALPINE, "sh", "-c", "touch /tmp/runroot/ran && while test -r /tmp/runroot/running; do sleep 0.1s; done"}) found := false testFile := filepath.Join(testDir, "ran") diff --git a/test/system/030-run.bats b/test/system/030-run.bats index 130cf5492..d81a0758c 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -778,6 +778,25 @@ EOF is "$output" "1.2.3.4 foo.com.*" "users can add hosts even without /etc/hosts" } +# rhbz#1854566 : $IMAGE has incorrect permission 555 on the root '/' filesystem +@test "podman run image with filesystem permission" { + # make sure the IMAGE image have permissiong of 555 like filesystem RPM expects + run_podman run --rm $IMAGE stat -c %a / + is "$output" "555" "directory permissions on /" +} + +# rhbz#1763007 : the --log-opt for podman run does not work as expected +@test "podman run with log-opt option" { + # Pseudorandom size of the form N.NNN. The '| 1' handles '0.NNN' or 'N.NN0', + # which podman displays as 'NNN kB' or 'N.NN MB' respectively. + size=$(printf "%d.%03d" $(($RANDOM % 10 | 1)) $(($RANDOM % 100 | 1))) + run_podman run -d --rm --log-opt max-size=${size}m $IMAGE sleep 5 + cid=$output + run_podman inspect --format "{{ .HostConfig.LogConfig.Size }}" $cid + is "$output" "${size}MB" + run_podman rm -t 0 -f $cid +} + @test "podman run --kernel-memory warning" { # Not sure what situations this fails in, but want to make sure warning shows. run_podman '?' run --rm --kernel-memory 100 $IMAGE false 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/160-volumes.bats b/test/system/160-volumes.bats index 43462de36..1271b7c0b 100644 --- a/test/system/160-volumes.bats +++ b/test/system/160-volumes.bats @@ -319,5 +319,30 @@ EOF is "$output" "" "no more volumes to prune" } +@test "podman volume type=bind" { + myvoldir=${PODMAN_TMPDIR}/volume_$(random_string) + mkdir $myvoldir + touch $myvoldir/myfile + + myvolume=myvol$(random_string) + run_podman 125 volume create -o type=bind -o device=/bogus $myvolume + is "$output" "Error: invalid volume option device for driver 'local': stat /bogus: no such file or directory" "should fail with bogus directory not existing" + + run_podman volume create -o type=bind -o device=/$myvoldir $myvolume + is "$output" "$myvolume" "should successfully create myvolume" + + run_podman run --rm -v $myvolume:/vol:z $IMAGE \ + stat -c "%u:%s" /vol/myfile + is "$output" "0:0" "w/o keep-id: stat(file in container) == root" +} + +@test "podman volume type=tmpfs" { + myvolume=myvol$(random_string) + run_podman volume create -o type=tmpfs -o device=tmpfs $myvolume + is "$output" "$myvolume" "should successfully create myvolume" + + run_podman run --rm -v $myvolume:/vol $IMAGE stat -f -c "%T" /vol + is "$output" "tmpfs" "volume should be tmpfs" +} # vim: filetype=sh diff --git a/test/system/180-blkio.bats b/test/system/180-blkio.bats index 68449681a..7999c9ec5 100644 --- a/test/system/180-blkio.bats +++ b/test/system/180-blkio.bats @@ -8,7 +8,7 @@ load helpers function teardown() { lofile=${PODMAN_TMPDIR}/disk.img if [ -f ${lofile} ]; then - run_podman '?' rm -t 0 --all --force + run_podman '?' rm -t 0 --all --force --ignore while read path dev; do if [[ "$path" == "$lofile" ]]; then 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 415c9010e..36a88fc10 100644 --- a/test/system/helpers.bash +++ b/test/system/helpers.bash @@ -56,14 +56,14 @@ fi # Setup helper: establish a test environment with exactly the images needed function basic_setup() { # Clean up all containers - run_podman rm -t 0 --all --force + run_podman rm -t 0 --all --force --ignore # ...including external (buildah) ones run_podman ps --all --external --format '{{.ID}} {{.Names}}' for line in "${lines[@]}"; do set $line echo "# setup(): removing stray external container $1 ($2)" >&3 - run_podman rm $1 + run_podman rm -f $1 done # Clean up all images except those desired @@ -109,8 +109,8 @@ function basic_setup() { # Basic teardown: remove all pods and containers function basic_teardown() { echo "# [teardown]" >&2 - run_podman '?' pod rm -t 0 --all --force - run_podman '?' rm -t 0 --all --force + run_podman '?' pod rm -t 0 --all --force --ignore + run_podman '?' rm -t 0 --all --force --ignore command rm -rf $PODMAN_TMPDIR } @@ -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 +} |