diff options
38 files changed, 737 insertions, 325 deletions
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/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 754d61f1f..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). 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/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/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 4f101ce84..ad341c3ab 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -356,11 +356,20 @@ 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())}, Image: imageName, - ImageID: imageID, + ImageID: "sha256:" + imageID, Command: strings.Join(l.Command(), " "), Created: l.CreatedTime().Unix(), Ports: ports, @@ -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/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/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/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/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 e931ceebe..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,8 +45,13 @@ 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 +t GET containers/json?limit=1 200 \ + .[0].ImageID~sha256:[0-9a-f]\\{64\\} + # Make sure `limit` works. t GET libpod/containers/json?limit=1 200 \ length=1 \ 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_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/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..958a0e87c 100644 --- a/test/system/helpers.bash +++ b/test/system/helpers.bash @@ -318,6 +318,10 @@ function wait_for_port() { # BEGIN miscellaneous tools # Shortcuts for common needs: +function is_ubuntu() { + grep -qiw ubuntu /etc/os-release +} + function is_rootless() { [ "$(id -u)" -ne 0 ] } @@ -459,6 +463,16 @@ function skip_if_journald_unavailable { fi } +function skip_if_root_ubuntu { + if is_ubuntu; then + if ! is_remote; then + if ! is_rootless; then + skip "Cannot run this test on rootful ubuntu, usually due to user errors" + fi + fi + fi +} + ######### # die # Abort with helpful message ######### diff --git a/test/utils/utils.go b/test/utils/utils.go index f41024072..1f5067950 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -41,6 +41,7 @@ type PodmanTest struct { RemotePodmanBinary string RemoteSession *os.Process RemoteSocket string + RemoteSocketLock string // If not "", should be removed _after_ RemoteSocket is removed RemoteCommand *exec.Cmd ImageCacheDir string ImageCacheFS string @@ -469,10 +470,6 @@ func Containerized() bool { return strings.Contains(string(b), "docker") } -func init() { - rand.Seed(GinkgoRandomSeed()) -} - var randomLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") // RandomString returns a string of given length composed of random characters diff --git a/utils/utils.go b/utils/utils.go index 45cec2c5f..4c04b939d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -229,3 +229,12 @@ func MovePauseProcessToScope(pausePidPath string) { } } } + +// CreateSCPCommand takes an existing command, appends the given arguments and returns a configured podman command for image scp +func CreateSCPCommand(cmd *exec.Cmd, command []string) *exec.Cmd { + cmd.Args = append(cmd.Args, command...) + cmd.Env = os.Environ() + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd +} |