diff options
Diffstat (limited to 'cmd/podman')
-rw-r--r-- | cmd/podman/common/completion.go | 4 | ||||
-rw-r--r-- | cmd/podman/common/create.go | 9 | ||||
-rw-r--r-- | cmd/podman/common/create_opts.go | 9 | ||||
-rw-r--r-- | cmd/podman/common/specgen.go | 105 | ||||
-rw-r--r-- | cmd/podman/containers/cp.go | 177 | ||||
-rw-r--r-- | cmd/podman/containers/create.go | 2 | ||||
-rw-r--r-- | cmd/podman/containers/logs.go | 14 | ||||
-rw-r--r-- | cmd/podman/containers/restore.go | 6 | ||||
-rw-r--r-- | cmd/podman/images/import.go | 19 | ||||
-rw-r--r-- | cmd/podman/images/scp.go | 348 | ||||
-rw-r--r-- | cmd/podman/login.go | 1 | ||||
-rw-r--r-- | cmd/podman/logout.go | 1 | ||||
-rw-r--r-- | cmd/podman/parse/net.go | 7 | ||||
-rw-r--r-- | cmd/podman/registry/config.go | 3 | ||||
-rw-r--r-- | cmd/podman/root.go | 11 | ||||
-rw-r--r-- | cmd/podman/system/connection/add.go | 126 | ||||
-rw-r--r-- | cmd/podman/system/connection/shared.go | 28 | ||||
-rw-r--r-- | cmd/podman/system/migrate.go | 5 |
18 files changed, 744 insertions, 131 deletions
diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 177d094aa..08b2f6235 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -46,7 +46,9 @@ func setupContainerEngine(cmd *cobra.Command) (entities.ContainerEngine, error) return nil, err } if !registry.IsRemote() && rootless.IsRootless() { - err := containerEngine.SetupRootless(registry.Context(), cmd) + _, noMoveProcess := cmd.Annotations[registry.NoMoveProcess] + + err := containerEngine.SetupRootless(registry.Context(), noMoveProcess) if err != nil { return nil, err } diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index 64d1956eb..96414add4 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -655,15 +655,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *ContainerCLIOpts) { ) _ = cmd.RegisterFlagCompletionFunc(stopTimeoutFlagName, completion.AutocompleteNone) - storageOptFlagName := "storage-opt" - createFlags.StringSliceVar( - &cf.StorageOpt, - storageOptFlagName, []string{}, - "Storage driver options per container", - ) - //FIXME: What should we suggest here? The flag is not in the man page. - _ = cmd.RegisterFlagCompletionFunc(storageOptFlagName, completion.AutocompleteNone) - subgidnameFlagName := "subgidname" createFlags.StringVar( &cf.SubUIDName, diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index 66778f519..42e0efe5d 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -517,7 +517,14 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c cliOpts.OOMKillDisable = *cc.HostConfig.OomKillDisable } if cc.Config.Healthcheck != nil { - cliOpts.HealthCmd = strings.Join(cc.Config.Healthcheck.Test, " ") + finCmd := "" + for _, str := range cc.Config.Healthcheck.Test { + finCmd = finCmd + str + " " + } + if len(finCmd) > 1 { + finCmd = finCmd[:len(finCmd)-1] + } + cliOpts.HealthCmd = finCmd cliOpts.HealthInterval = cc.Config.Healthcheck.Interval.String() cliOpts.HealthRetries = uint(cc.Config.Healthcheck.Retries) cliOpts.HealthStartPeriod = cc.Config.Healthcheck.StartPeriod.String() diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 2f45e559d..42f515ace 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -516,7 +516,6 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string if len(con) != 2 { return fmt.Errorf("invalid --security-opt 1: %q", opt) } - switch con[0] { case "apparmor": s.ContainerSecurityConfig.ApparmorProfile = con[1] @@ -566,6 +565,14 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.Devices = append(s.Devices, specs.LinuxDevice{Path: dev}) } + for _, rule := range c.DeviceCGroupRule { + dev, err := parseLinuxResourcesDeviceAccess(rule) + if err != nil { + return err + } + s.DeviceCGroupRule = append(s.DeviceCGroupRule, dev) + } + s.Init = c.Init s.InitPath = c.InitPath s.Stdin = c.Interactive @@ -656,25 +663,40 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string } func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string) (*manifest.Schema2HealthConfig, error) { + cmdArr := []string{} + isArr := true + err := json.Unmarshal([]byte(inCmd), &cmdArr) // array unmarshalling + if err != nil { + cmdArr = strings.SplitN(inCmd, " ", 2) // default for compat + isArr = false + } // Every healthcheck requires a command - if len(inCmd) == 0 { + if len(cmdArr) == 0 { return nil, errors.New("Must define a healthcheck command for all healthchecks") } - - // first try to parse option value as JSON array of strings... - cmd := []string{} - - if inCmd == "none" { - cmd = []string{"NONE"} - } else { - err := json.Unmarshal([]byte(inCmd), &cmd) - if err != nil { - // ...otherwise pass it to "/bin/sh -c" inside the container - cmd = []string{"CMD-SHELL", inCmd} + concat := "" + if cmdArr[0] == "CMD" || cmdArr[0] == "none" { // this is for compat, we are already split properly for most compat cases + cmdArr = strings.Fields(inCmd) + } else if cmdArr[0] != "CMD-SHELL" { // this is for podman side of things, wont contain the keywords + if isArr && len(cmdArr) > 1 { // an array of consecutive commands + cmdArr = append([]string{"CMD"}, cmdArr...) + } else { // one singular command + if len(cmdArr) == 1 { + concat = cmdArr[0] + } else { + concat = strings.Join(cmdArr[0:], " ") + } + cmdArr = append([]string{"CMD-SHELL"}, concat) } } + + if cmdArr[0] == "none" { // if specified to remove healtcheck + cmdArr = []string{"NONE"} + } + + // healthcheck is by default an array, so we simply pass the user input hc := manifest.Schema2HealthConfig{ - Test: cmd, + Test: cmdArr, } if interval == "disable" { @@ -885,3 +907,58 @@ func parseSecrets(secrets []string) ([]specgen.Secret, map[string]string, error) } return mount, envs, nil } + +var cgroupDeviceType = map[string]bool{ + "a": true, // all + "b": true, // block device + "c": true, // character device +} + +var cgroupDeviceAccess = map[string]bool{ + "r": true, //read + "w": true, //write + "m": true, //mknod +} + +// parseLinuxResourcesDeviceAccess parses the raw string passed with the --device-access-add flag +func parseLinuxResourcesDeviceAccess(device string) (specs.LinuxDeviceCgroup, error) { + var devType, access string + var major, minor *int64 + + value := strings.Split(device, " ") + if len(value) != 3 { + return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device cgroup rule requires type, major:Minor, and access rules: %q", device) + } + + devType = value[0] + if !cgroupDeviceType[devType] { + return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device type in device-access-add: %s", devType) + } + + number := strings.SplitN(value[1], ":", 2) + i, err := strconv.ParseInt(number[0], 10, 64) + if err != nil { + return specs.LinuxDeviceCgroup{}, err + } + major = &i + if len(number) == 2 && number[1] != "*" { + i, err := strconv.ParseInt(number[1], 10, 64) + if err != nil { + return specs.LinuxDeviceCgroup{}, err + } + minor = &i + } + access = value[2] + for _, c := range strings.Split(access, "") { + if !cgroupDeviceAccess[c] { + return specs.LinuxDeviceCgroup{}, fmt.Errorf("invalid device access in device-access-add: %s", c) + } + } + return specs.LinuxDeviceCgroup{ + Allow: true, + Type: devType, + Major: major, + Minor: minor, + Access: access, + }, nil +} diff --git a/cmd/podman/containers/cp.go b/cmd/podman/containers/cp.go index c1f1e27f5..7b5846a35 100644 --- a/cmd/podman/containers/cp.go +++ b/cmd/podman/containers/cp.go @@ -82,7 +82,9 @@ func cp(cmd *cobra.Command, args []string) error { return err } - if len(sourceContainerStr) > 0 { + if len(sourceContainerStr) > 0 && len(destContainerStr) > 0 { + return copyContainerToContainer(sourceContainerStr, sourcePath, destContainerStr, destPath) + } else if len(sourceContainerStr) > 0 { return copyFromContainer(sourceContainerStr, sourcePath, destPath) } @@ -115,6 +117,84 @@ func doCopy(funcA func() error, funcB func() error) error { return errorhandling.JoinErrors(copyErrors) } +func copyContainerToContainer(sourceContainer string, sourcePath string, destContainer string, destPath string) error { + if err := containerMustExist(sourceContainer); err != nil { + return err + } + + if err := containerMustExist(destContainer); err != nil { + return err + } + + sourceContainerInfo, err := registry.ContainerEngine().ContainerStat(registry.GetContext(), sourceContainer, sourcePath) + if err != nil { + return errors.Wrapf(err, "%q could not be found on container %s", sourcePath, sourceContainer) + } + + destContainerBaseName, destContainerInfo, destResolvedToParentDir, err := resolvePathOnDestinationContainer(destContainer, destPath, false) + if err != nil { + return err + } + + if sourceContainerInfo.IsDir && !destContainerInfo.IsDir { + return errors.New("destination must be a directory when copying a directory") + } + + sourceContainerTarget := sourceContainerInfo.LinkTarget + destContainerTarget := destContainerInfo.LinkTarget + if !destContainerInfo.IsDir { + destContainerTarget = filepath.Dir(destPath) + } + + // If we copy a directory via the "." notation and the container path + // does not exist, we need to make sure that the destination on the + // container gets created; otherwise the contents of the source + // directory will be written to the destination's parent directory. + // + // Hence, whenever "." is the source and the destination does not + // exist, we copy the source's parent and let the copier package create + // the destination via the Rename option. + if destResolvedToParentDir && sourceContainerInfo.IsDir && strings.HasSuffix(sourcePath, ".") { + sourceContainerTarget = filepath.Dir(sourceContainerTarget) + } + + reader, writer := io.Pipe() + + sourceContainerCopy := func() error { + defer writer.Close() + copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.GetContext(), sourceContainer, sourceContainerTarget, writer) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return errors.Wrap(err, "error copying from container") + } + return nil + } + + destContainerCopy := func() error { + defer reader.Close() + + copyOptions := entities.CopyOptions{Chown: chown} + if (!sourceContainerInfo.IsDir && !destContainerInfo.IsDir) || destResolvedToParentDir { + // If we're having a file-to-file copy, make sure to + // rename accordingly. + copyOptions.Rename = map[string]string{filepath.Base(sourceContainerTarget): destContainerBaseName} + } + + copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), destContainer, destContainerTarget, reader, copyOptions) + if err != nil { + return err + } + if err := copyFunc(); err != nil { + return errors.Wrap(err, "error copying to container") + } + return nil + } + + return doCopy(sourceContainerCopy, destContainerCopy) +} + // copyFromContainer copies from the containerPath on the container to hostPath. func copyFromContainer(container string, containerPath string, hostPath string) error { if err := containerMustExist(container); err != nil { @@ -133,6 +213,7 @@ func copyFromContainer(container string, containerPath string, hostPath string) } var hostBaseName string + var resolvedToHostParentDir bool hostInfo, hostInfoErr := copy.ResolveHostPath(hostPath) if hostInfoErr != nil { if strings.HasSuffix(hostPath, "/") { @@ -148,6 +229,7 @@ func copyFromContainer(container string, containerPath string, hostPath string) // it'll be created while copying. Hence, we use it as the // base path. hostBaseName = filepath.Base(hostPath) + resolvedToHostParentDir = true } else { // If the specified path exists on the host, we must use its // base path as it may have changed due to symlink evaluations. @@ -175,7 +257,7 @@ func copyFromContainer(container string, containerPath string, hostPath string) // we copy the source's parent and let the copier package create the // destination via the Rename option. containerTarget := containerInfo.LinkTarget - if hostInfoErr != nil && containerInfo.IsDir && strings.HasSuffix(containerTarget, ".") { + if resolvedToHostParentDir && containerInfo.IsDir && strings.HasSuffix(containerTarget, ".") { containerTarget = filepath.Dir(containerTarget) } @@ -216,7 +298,7 @@ func copyFromContainer(container string, containerPath string, hostPath string) ChownFiles: &idPair, IgnoreDevices: true, } - if (!containerInfo.IsDir && !hostInfo.IsDir) || hostInfoErr != nil { + if (!containerInfo.IsDir && !hostInfo.IsDir) || resolvedToHostParentDir { // If we're having a file-to-file copy, make sure to // rename accordingly. putOptions.Rename = map[string]string{filepath.Base(containerTarget): hostBaseName} @@ -263,42 +345,9 @@ func copyToContainer(container string, containerPath string, hostPath string) er return errors.Wrapf(err, "%q could not be found on the host", hostPath) } - // If the path on the container does not exist. We need to make sure - // that it's parent directory exists. The destination may be created - // while copying. - var containerBaseName string - containerInfo, containerInfoErr := registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) - if containerInfoErr != nil { - if strings.HasSuffix(containerPath, "/") { - return errors.Wrapf(containerInfoErr, "%q could not be found on container %s", containerPath, container) - } - if isStdin { - return errors.New("destination must be a directory when copying from stdin") - } - // NOTE: containerInfo may actually be set. That happens when - // the container path is a symlink into nirvana. In that case, - // we must use the symlinked path instead. - path := containerPath - if containerInfo != nil { - containerBaseName = filepath.Base(containerInfo.LinkTarget) - path = containerInfo.LinkTarget - } else { - containerBaseName = filepath.Base(containerPath) - } - - parentDir, err := containerParentDir(container, path) - if err != nil { - return errors.Wrapf(err, "could not determine parent dir of %q on container %s", path, container) - } - containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir) - if err != nil { - return errors.Wrapf(err, "%q could not be found on container %s", containerPath, container) - } - } else { - // If the specified path exists on the container, we must use - // its base path as it may have changed due to symlink - // evaluations. - containerBaseName = filepath.Base(containerInfo.LinkTarget) + containerBaseName, containerInfo, containerResolvedToParentDir, err := resolvePathOnDestinationContainer(container, containerPath, isStdin) + if err != nil { + return err } // If we copy a directory via the "." notation and the container path @@ -310,7 +359,7 @@ func copyToContainer(container string, containerPath string, hostPath string) er // exist, we copy the source's parent and let the copier package create // the destination via the Rename option. hostTarget := hostInfo.LinkTarget - if containerInfoErr != nil && hostInfo.IsDir && strings.HasSuffix(hostTarget, ".") { + if containerResolvedToParentDir && hostInfo.IsDir && strings.HasSuffix(hostTarget, ".") { hostTarget = filepath.Dir(hostTarget) } @@ -362,7 +411,7 @@ func copyToContainer(container string, containerPath string, hostPath string) er // copy the base directory. KeepDirectoryNames: hostInfo.IsDir && filepath.Base(hostTarget) != ".", } - if (!hostInfo.IsDir && !containerInfo.IsDir) || containerInfoErr != nil { + if (!hostInfo.IsDir && !containerInfo.IsDir) || containerResolvedToParentDir { // If we're having a file-to-file copy, make sure to // rename accordingly. getOptions.Rename = map[string]string{filepath.Base(hostTarget): containerBaseName} @@ -393,6 +442,52 @@ func copyToContainer(container string, containerPath string, hostPath string) er return doCopy(hostCopy, containerCopy) } +// resolvePathOnDestinationContainer resolves the specified path on the +// container. If the path does not exist, it attempts to use the parent +// directory. +func resolvePathOnDestinationContainer(container string, containerPath string, isStdin bool) (baseName string, containerInfo *entities.ContainerStatReport, resolvedToParentDir bool, err error) { + containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, containerPath) + if err == nil { + baseName = filepath.Base(containerInfo.LinkTarget) + return + } + + if strings.HasSuffix(containerPath, "/") { + err = errors.Wrapf(err, "%q could not be found on container %s", containerPath, container) + return + } + if isStdin { + err = errors.New("destination must be a directory when copying from stdin") + return + } + + // NOTE: containerInfo may actually be set. That happens when + // the container path is a symlink into nirvana. In that case, + // we must use the symlinked path instead. + path := containerPath + if containerInfo != nil { + baseName = filepath.Base(containerInfo.LinkTarget) + path = containerInfo.LinkTarget + } else { + baseName = filepath.Base(containerPath) + } + + parentDir, err := containerParentDir(container, path) + if err != nil { + err = errors.Wrapf(err, "could not determine parent dir of %q on container %s", path, container) + return + } + + containerInfo, err = registry.ContainerEngine().ContainerStat(registry.GetContext(), container, parentDir) + if err != nil { + err = errors.Wrapf(err, "%q could not be found on container %s", containerPath, container) + return + } + + resolvedToParentDir = true + return baseName, containerInfo, resolvedToParentDir, nil +} + // containerParentDir returns the parent directory of the specified path on the // container. If the path is relative, it will be resolved relative to the // container's working directory (or "/" if the work dir isn't set). diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index df0fa6f9d..c63c074f7 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -146,6 +146,8 @@ func replaceContainer(name string) error { } func createInit(c *cobra.Command) error { + cliVals.StorageOpt = registry.PodmanConfig().StorageOpts + if c.Flag("shm-size").Changed { cliVals.ShmSize = c.Flag("shm-size").Value.String() } diff --git a/cmd/podman/containers/logs.go b/cmd/podman/containers/logs.go index 0d745291e..00a8d4b52 100644 --- a/cmd/podman/containers/logs.go +++ b/cmd/podman/containers/logs.go @@ -19,6 +19,8 @@ type logsOptionsWrapper struct { entities.ContainerLogsOptions SinceRaw string + + UntilRaw string } var ( @@ -101,6 +103,10 @@ func logsFlags(cmd *cobra.Command) { flags.StringVar(&logsOptions.SinceRaw, sinceFlagName, "", "Show logs since TIMESTAMP") _ = cmd.RegisterFlagCompletionFunc(sinceFlagName, completion.AutocompleteNone) + untilFlagName := "until" + flags.StringVar(&logsOptions.UntilRaw, untilFlagName, "", "Show logs until TIMESTAMP") + _ = cmd.RegisterFlagCompletionFunc(untilFlagName, completion.AutocompleteNone) + tailFlagName := "tail" flags.Int64Var(&logsOptions.Tail, tailFlagName, -1, "Output the specified number of LINES at the end of the logs. Defaults to -1, which prints all lines") _ = cmd.RegisterFlagCompletionFunc(tailFlagName, completion.AutocompleteNone) @@ -120,6 +126,14 @@ func logs(_ *cobra.Command, args []string) error { } logsOptions.Since = since } + if logsOptions.UntilRaw != "" { + // parse time, error out if something is wrong + until, err := util.ParseInputTime(logsOptions.UntilRaw) + if err != nil { + return errors.Wrapf(err, "error parsing --until %q", logsOptions.UntilRaw) + } + logsOptions.Until = until + } logsOptions.StdoutWriter = os.Stdout logsOptions.StderrWriter = os.Stderr return registry.ContainerEngine().ContainerLogs(registry.GetContext(), args, logsOptions.ContainerLogsOptions) diff --git a/cmd/podman/containers/restore.go b/cmd/podman/containers/restore.go index b908ea493..3b6f74efa 100644 --- a/cmd/podman/containers/restore.go +++ b/cmd/podman/containers/restore.go @@ -71,6 +71,9 @@ func init() { ) _ = restoreCommand.RegisterFlagCompletionFunc("publish", completion.AutocompleteNone) + flags.StringVar(&restoreOptions.Pod, "pod", "", "Restore container into existing Pod (only works with --import)") + _ = restoreCommand.RegisterFlagCompletionFunc("pod", common.AutocompletePodsRunning) + validate.AddLatestFlag(restoreCommand, &restoreOptions.Latest) } @@ -91,6 +94,9 @@ func restore(cmd *cobra.Command, args []string) error { if restoreOptions.Import == "" && restoreOptions.Name != "" { return errors.Errorf("--name can only be used with --import") } + if restoreOptions.Import == "" && restoreOptions.Pod != "" { + return errors.Errorf("--pod can only be used with --import") + } if restoreOptions.Name != "" && restoreOptions.TCPEstablished { return errors.Errorf("--tcp-established cannot be used with --name") } diff --git a/cmd/podman/images/import.go b/cmd/podman/images/import.go index bed2d4105..bc80417cc 100644 --- a/cmd/podman/images/import.go +++ b/cmd/podman/images/import.go @@ -3,6 +3,9 @@ package images import ( "context" "fmt" + "io" + "io/ioutil" + "os" "strings" "github.com/containers/common/pkg/completion" @@ -97,6 +100,22 @@ func importCon(cmd *cobra.Command, args []string) error { default: return errors.Errorf("too many arguments. Usage TARBALL [REFERENCE]") } + + if source == "-" { + outFile, err := ioutil.TempFile("", "podman") + if err != nil { + return errors.Errorf("error creating file %v", err) + } + defer os.Remove(outFile.Name()) + defer outFile.Close() + + _, err = io.Copy(outFile, os.Stdin) + if err != nil { + return errors.Errorf("error copying file %v", err) + } + source = outFile.Name() + } + errFileName := parse.ValidateFileName(source) errURL := parse.ValidURL(source) if errURL == nil { diff --git a/cmd/podman/images/scp.go b/cmd/podman/images/scp.go new file mode 100644 index 000000000..a47d01995 --- /dev/null +++ b/cmd/podman/images/scp.go @@ -0,0 +1,348 @@ +package images + +import ( + "context" + "fmt" + "io/ioutil" + urlP "net/url" + "os" + "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/docker/distribution/reference" + scpD "github.com/dtylman/scp" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" +) + +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}, + Long: saveScpDescription, + Short: "securely copy images", + RunE: scp, + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: common.AutocompleteImages, + Example: `podman image scp myimage:latest otherhost::`, + } +) + +var ( + scpOpts entities.ImageScpOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: imageScpCommand, + Parent: imageCmd, + }) + scpFlags(imageScpCommand) +} + +func scpFlags(cmd *cobra.Command) { + flags := cmd.Flags() + flags.BoolVarP(&scpOpts.Save.Quiet, "quiet", "q", false, "Suppress the output") +} + +func scp(cmd *cobra.Command, args []string) (finalErr error) { + var ( + // TODO add tag support for images + err error + ) + if scpOpts.Save.Quiet { // set quiet for both load and save + scpOpts.Load.Quiet = true + } + f, err := ioutil.TempFile("", "podman") // open temp file for load/save output + 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 { + 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 + } + + cfg, err := config.ReadCustomConfig() // get ready to set ssh destination if necessary + if err != nil { + return err + } + serv, err := parseArgs(args, cfg) // parses connection data and "which way" we are loading and saving + 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 + 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]) + 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 err != nil { + return err + } + fmt.Println(rep) + break + } + report, err := abiEng.Load(context.Background(), scpOpts.Load) + 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]) + if err != nil { + return err + } + fmt.Println(rep) + // TODO: Add podman remote support + default: // else native load + 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 { + errors.Wrapf(abiErr, "could not save image as specified") + } + rep, err := abiEng.Load(context.Background(), scpOpts.Load) + if err != nil { + return err + } + fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ",")) + } + return nil +} + +// loadToRemote takes image and remote connection information. it connects to the specified client +// and copies the saved image dir over to the remote host and then loads it onto the machine +// returns a string containing output or an error +func loadToRemote(localFile string, tag string, url *urlP.URL, iden string) (string, error) { + dial, remoteFile, err := createConnection(url, iden) + if err != nil { + return "", err + } + defer dial.Close() + + n, err := scpD.CopyTo(dial, localFile, remoteFile) + if err != nil { + errOut := (strconv.Itoa(int(n)) + " Bytes copied before error") + return " ", errors.Wrapf(err, errOut) + } + run := "" + if tag != "" { + return "", errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported") + } + podman := os.Args[0] + run = podman + " image load --input=" + remoteFile + ";rm " + remoteFile // run ssh image load of the file copied via scp + out, err := connection.ExecRemoteCommand(dial, run) + if err != nil { + return "", err + } + return strings.TrimSuffix(out, "\n"), nil +} + +// saveToRemote takes image information and remote connection information. it connects to the specified client +// and saves the specified image on the remote machine and then copies it to the specified local location +// returns an error if one occurs. +func saveToRemote(image, localFile string, tag string, uri *urlP.URL, iden string) error { + dial, remoteFile, err := createConnection(uri, iden) + + if err != nil { + return err + } + defer dial.Close() + + if tag != "" { + return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported") + } + podman := os.Args[0] + run := podman + " image save " + image + " --format=oci-archive --output=" + remoteFile // run ssh image load of the file copied via scp. Files are reverse in thie case... + _, err = connection.ExecRemoteCommand(dial, run) + if err != nil { + return nil + } + n, err := scpD.CopyFrom(dial, remoteFile, localFile) + connection.ExecRemoteCommand(dial, "rm "+remoteFile) + if err != nil { + errOut := (strconv.Itoa(int(n)) + " Bytes copied before error") + return errors.Wrapf(err, errOut) + } + return nil +} + +// makeRemoteFile creates the necessary remote file on the host to +// save or load the image to. returns a string with the file name or an error +func makeRemoteFile(dial *ssh.Client) (string, error) { + run := "mktemp" + remoteFile, err := connection.ExecRemoteCommand(dial, run) + if err != nil { + return "", err + } + remoteFile = strings.TrimSuffix(remoteFile, "\n") + if err != nil { + return "", err + } + return remoteFile, nil +} + +// createConnections takes a boolean determining which ssh client to dial +// and returns the dials client, its newly opened remote file, and an error if applicable. +func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) { + cfg, err := connection.ValidateAndConfigure(url, iden) + if err != nil { + return nil, "", err + } + dialAdd, err := ssh.Dial("tcp", url.Host, cfg) // dial the client + if err != nil { + return nil, "", errors.Wrapf(err, "failed to connect") + } + file, err := makeRemoteFile(dialAdd) + if err != nil { + return nil, "", err + } + + 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], "::") { + 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], "::") { + if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client + cliConnections = append(cliConnections, args[0]) + scpOpts.ToRemote = true + scpOpts.SourceImageName = args[1] + } else if strings.Contains(args[1], "::") { // both remote clients + scpOpts.FromRemote = true + scpOpts.ToRemote = true + if remoteArgLength(args[0], 1) == 0 { // is save->load w/ one image name + cliConnections = append(cliConnections, args[0]) + cliConnections = append(cliConnections, args[1]) + } else if remoteArgLength(args[0], 1) > 0 && remoteArgLength(args[1], 1) > 0 { + //in the future, this function could, instead of rejecting renames, also set a DestImageName field + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") + } else { // else its a load save (order of args) + cliConnections = append(cliConnections, args[1]) + cliConnections = append(cliConnections, args[0]) + } + } else { + //in the future, this function could, instead of rejecting renames, also set a DestImageName field + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") + } + } else if strings.Contains(args[1], "::") { // if we are given image host:: + if remoteArgLength(args[1], 1) > 0 { + //in the future, this function could, instead of rejecting renames, also set a DestImageName field + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") + } + err := validateImageName(args[0]) + if err != nil { + return nil, err + } + scpOpts.SourceImageName = args[0] + scpOpts.ToRemote = true + cliConnections = append(cliConnections, args[1]) + } else { + //in the future, this function could, instead of rejecting renames, also set a DestImageName field + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") + } + } + var url string + var iden string + for i, val := range cliConnections { + splitEnv := strings.SplitN(val, "::", 2) + scpOpts.Connections = append(scpOpts.Connections, splitEnv[0]) + if len(splitEnv[1]) != 0 { + err := validateImageName(splitEnv[1]) + if err != nil { + return nil, err + } + scpOpts.SourceImageName = splitEnv[1] + //TODO: actually use the new name given by the user + } + conn, found := cfg.Engine.ServiceDestinations[scpOpts.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] + iden = "" + logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location") + } + urlT, err := urlP.Parse(url) // create an actual url to pass to exec command + if err != nil { + return nil, err + } + if urlT.User.Username() == "" { + if urlT.User, err = connection.GetUserInfo(urlT); err != nil { + return nil, err + } + } + scpOpts.URI = append(scpOpts.URI, urlT) + scpOpts.Iden = append(scpOpts.Iden, iden) + } + return serv, nil +} diff --git a/cmd/podman/login.go b/cmd/podman/login.go index a8dadf5cd..7e853b38d 100644 --- a/cmd/podman/login.go +++ b/cmd/podman/login.go @@ -52,6 +52,7 @@ func init() { loginOptions.Stdin = os.Stdin loginOptions.Stdout = os.Stdout loginOptions.AcceptUnspecifiedRegistry = true + loginOptions.AcceptRepositories = true } // Implementation of podman-login. diff --git a/cmd/podman/logout.go b/cmd/podman/logout.go index 0ee134635..f44b13a1f 100644 --- a/cmd/podman/logout.go +++ b/cmd/podman/logout.go @@ -43,6 +43,7 @@ func init() { logoutOptions.Stdout = os.Stdout logoutOptions.AcceptUnspecifiedRegistry = true + logoutOptions.AcceptRepositories = true } // Implementation of podman-logout. diff --git a/cmd/podman/parse/net.go b/cmd/podman/parse/net.go index f93c4ab1e..870690db3 100644 --- a/cmd/podman/parse/net.go +++ b/cmd/podman/parse/net.go @@ -180,9 +180,12 @@ func ValidateFileName(filename string) error { // ValidURL checks a string urlStr is a url or not func ValidURL(urlStr string) error { - _, err := url.ParseRequestURI(urlStr) + url, err := url.ParseRequestURI(urlStr) if err != nil { - return errors.Wrapf(err, "invalid url path: %q", urlStr) + return errors.Wrapf(err, "invalid url %q", urlStr) + } + if url.Scheme == "" { + return errors.Errorf("invalid url %q: missing scheme", urlStr) } return nil } diff --git a/cmd/podman/registry/config.go b/cmd/podman/registry/config.go index 25139a3de..b512ba341 100644 --- a/cmd/podman/registry/config.go +++ b/cmd/podman/registry/config.go @@ -15,6 +15,9 @@ import ( ) const ( + // NoMoveProcess used as cobra.Annotation when command doesn't need Podman to be moved to a separate cgroup + NoMoveProcess = "NoMoveProcess" + // ParentNSRequired used as cobra.Annotation when command requires root access ParentNSRequired = "ParentNSRequired" diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 9e5d2a236..dc4ebb952 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -208,7 +208,8 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error { // 3) command doesn't require Parent Namespace _, found := cmd.Annotations[registry.ParentNSRequired] if !registry.IsRemote() && rootless.IsRootless() && !found { - err := registry.ContainerEngine().SetupRootless(registry.Context(), cmd) + _, noMoveProcess := cmd.Annotations[registry.NoMoveProcess] + err := registry.ContainerEngine().SetupRootless(registry.Context(), noMoveProcess) if err != nil { return err } @@ -342,10 +343,6 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { pFlags.StringVar(&opts.StorageDriver, storageDriverFlagName, "", "Select which storage driver is used to manage storage of images and containers (default is overlay)") _ = cmd.RegisterFlagCompletionFunc(storageDriverFlagName, completion.AutocompleteNone) //TODO: what can we recommend here? - storageOptFlagName := "storage-opt" - pFlags.StringArrayVar(&opts.StorageOpts, storageOptFlagName, []string{}, "Used to pass an option to the storage driver") - _ = cmd.RegisterFlagCompletionFunc(storageOptFlagName, completion.AutocompleteNone) - tmpdirFlagName := "tmpdir" pFlags.StringVar(&opts.Engine.TmpDir, tmpdirFlagName, "", "Path to the tmp directory for libpod state content.\n\nNote: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.\n") _ = cmd.RegisterFlagCompletionFunc(tmpdirFlagName, completion.AutocompleteDefault) @@ -365,6 +362,10 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { } } } + storageOptFlagName := "storage-opt" + pFlags.StringArrayVar(&opts.StorageOpts, storageOptFlagName, []string{}, "Used to pass an option to the storage driver") + _ = cmd.RegisterFlagCompletionFunc(storageOptFlagName, completion.AutocompleteNone) + // Override default --help information of `--help` global flag var dummyHelp bool pFlags.BoolVar(&dummyHelp, "help", false, "Help for podman") diff --git a/cmd/podman/system/connection/add.go b/cmd/podman/system/connection/add.go index 912193d0b..290b3c245 100644 --- a/cmd/podman/system/connection/add.go +++ b/cmd/podman/system/connection/add.go @@ -1,7 +1,6 @@ package connection import ( - "bytes" "encoding/json" "fmt" "net" @@ -9,6 +8,7 @@ import ( "os" "os/user" "regexp" + "time" "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/config" @@ -83,7 +83,6 @@ func add(cmd *cobra.Command, args []string) error { } else if !match { dest = "ssh://" + dest } - uri, err := url.Parse(dest) if err != nil { return err @@ -96,7 +95,7 @@ func add(cmd *cobra.Command, args []string) error { switch uri.Scheme { case "ssh": if uri.User.Username() == "" { - if uri.User, err = getUserInfo(uri); err != nil { + if uri.User, err = GetUserInfo(uri); err != nil { return err } } @@ -108,9 +107,12 @@ func add(cmd *cobra.Command, args []string) error { if uri.Port() == "" { uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").DefValue) } - + iden := "" + if cmd.Flags().Changed("identity") { + iden = cOpts.Identity + } if uri.Path == "" || uri.Path == "/" { - if uri.Path, err = getUDS(cmd, uri); err != nil { + if uri.Path, err = getUDS(cmd, uri, iden); err != nil { return err } } @@ -178,7 +180,7 @@ func add(cmd *cobra.Command, args []string) error { return cfg.Write() } -func getUserInfo(uri *url.URL) (*url.Userinfo, error) { +func GetUserInfo(uri *url.URL) (*url.Userinfo, error) { var ( usr *user.User err error @@ -202,30 +204,74 @@ func getUserInfo(uri *url.URL) (*url.Userinfo, error) { return url.User(usr.Username), nil } -func getUDS(cmd *cobra.Command, uri *url.URL) (string, error) { - var signers []ssh.Signer +func getUDS(cmd *cobra.Command, uri *url.URL, iden string) (string, error) { + cfg, err := ValidateAndConfigure(uri, iden) + if err != nil { + return "", errors.Wrapf(err, "failed to validate") + } + dial, err := ssh.Dial("tcp", uri.Host, cfg) + if err != nil { + return "", errors.Wrapf(err, "failed to connect") + } + defer dial.Close() + + session, err := dial.NewSession() + if err != nil { + return "", errors.Wrapf(err, "failed to create new ssh session on %q", uri.Host) + } + defer session.Close() + + // Override podman binary for testing etc + podman := "podman" + if v, found := os.LookupEnv("PODMAN_BINARY"); found { + podman = v + } + run := podman + " info --format=json" + out, err := ExecRemoteCommand(dial, run) + if err != nil { + return "", err + } + infoJSON, err := json.Marshal(out) + if err != nil { + return "", err + } + + var info define.Info + if err := json.Unmarshal(infoJSON, &info); err != nil { + return "", errors.Wrapf(err, "failed to parse 'podman info' results") + } + + if info.Host.RemoteSocket == nil || len(info.Host.RemoteSocket.Path) == 0 { + return "", errors.Errorf("remote podman %q failed to report its UDS socket", uri.Host) + } + return info.Host.RemoteSocket.Path, nil +} +// ValidateAndConfigure will take a ssh url and an identity key (rsa and the like) and ensure the information given is valid +// iden iden can be blank to mean no identity key +// once the function validates the information it creates and returns an ssh.ClientConfig +func ValidateAndConfigure(uri *url.URL, iden string) (*ssh.ClientConfig, error) { + var signers []ssh.Signer passwd, passwdSet := uri.User.Password() - if cmd.Flags().Changed("identity") { - value := cmd.Flag("identity").Value.String() + if iden != "" { // iden might be blank if coming from image scp or if no validation is needed + value := iden s, err := terminal.PublicKey(value, []byte(passwd)) if err != nil { - return "", errors.Wrapf(err, "failed to read identity %q", value) + return nil, errors.Wrapf(err, "failed to read identity %q", value) } signers = append(signers, s) logrus.Debugf("SSH Ident Key %q %s %s", value, ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type()) } - - if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { + if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { // validate ssh information, specifically the unix file socket used by the ssh agent. logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock) c, err := net.Dial("unix", sock) if err != nil { - return "", err + return nil, err } agentSigners, err := agent.NewClient(c).Signers() if err != nil { - return "", err + return nil, err } signers = append(signers, agentSigners...) @@ -236,11 +282,9 @@ func getUDS(cmd *cobra.Command, uri *url.URL) (string, error) { } } } - - var authMethods []ssh.AuthMethod + var authMethods []ssh.AuthMethod // now we validate and check for the authorization methods, most notaibly public key authorization if len(signers) > 0 { var dedup = make(map[string]ssh.Signer) - // Dedup signers based on fingerprint, ssh-agent keys override CONTAINER_SSHKEY for _, s := range signers { fp := ssh.FingerprintSHA256(s.PublicKey()) if _, found := dedup[fp]; found { @@ -253,60 +297,28 @@ func getUDS(cmd *cobra.Command, uri *url.URL) (string, error) { for _, s := range dedup { uniq = append(uniq, s) } - authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { return uniq, nil })) } - - if passwdSet { + if passwdSet { // if password authentication is given and valid, add to the list authMethods = append(authMethods, ssh.Password(passwd)) } - if len(authMethods) == 0 { authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { pass, err := terminal.ReadPassword(fmt.Sprintf("%s's login password:", uri.User.Username())) return string(pass), err })) } - + tick, err := time.ParseDuration("40s") + if err != nil { + return nil, err + } cfg := &ssh.ClientConfig{ User: uri.User.Username(), Auth: authMethods, HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: tick, } - dial, err := ssh.Dial("tcp", uri.Host, cfg) - if err != nil { - return "", errors.Wrapf(err, "failed to connect") - } - defer dial.Close() - - session, err := dial.NewSession() - if err != nil { - return "", errors.Wrapf(err, "failed to create new ssh session on %q", uri.Host) - } - defer session.Close() - - // Override podman binary for testing etc - podman := "podman" - if v, found := os.LookupEnv("PODMAN_BINARY"); found { - podman = v - } - run := podman + " info --format=json" - - var buffer bytes.Buffer - session.Stdout = &buffer - if err := session.Run(run); err != nil { - return "", err - } - - var info define.Info - if err := json.Unmarshal(buffer.Bytes(), &info); err != nil { - return "", errors.Wrapf(err, "failed to parse 'podman info' results") - } - - if info.Host.RemoteSocket == nil || len(info.Host.RemoteSocket.Path) == 0 { - return "", errors.Errorf("remote podman %q failed to report its UDS socket", uri.Host) - } - return info.Host.RemoteSocket.Path, nil + return cfg, nil } diff --git a/cmd/podman/system/connection/shared.go b/cmd/podman/system/connection/shared.go new file mode 100644 index 000000000..3fd7c59fb --- /dev/null +++ b/cmd/podman/system/connection/shared.go @@ -0,0 +1,28 @@ +package connection + +import ( + "bytes" + + "github.com/pkg/errors" + "golang.org/x/crypto/ssh" +) + +// ExecRemoteCommand takes a ssh client connection and a command to run and executes the +// command on the specified client. The function returns the Stdout from the client or the Stderr +func ExecRemoteCommand(dial *ssh.Client, run string) (string, error) { + sess, err := dial.NewSession() // new ssh client session + if err != nil { + return "", err + } + defer sess.Close() + + var buffer bytes.Buffer + var bufferErr bytes.Buffer + sess.Stdout = &buffer // output from client funneled into buffer + sess.Stderr = &bufferErr // err form client funneled into buffer + if err := sess.Run(run); err != nil { // run the command on the ssh client + return "", errors.Wrapf(err, bufferErr.String()) + } + out := buffer.String() // output from command + return out, nil +} diff --git a/cmd/podman/system/migrate.go b/cmd/podman/system/migrate.go index 9940cd063..b9dc272d7 100644 --- a/cmd/podman/system/migrate.go +++ b/cmd/podman/system/migrate.go @@ -22,7 +22,10 @@ var ( ` migrateCommand = &cobra.Command{ - Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + Annotations: map[string]string{ + registry.EngineMode: registry.ABIMode, + registry.NoMoveProcess: registry.NoMoveProcess, + }, Use: "migrate [options]", Args: validate.NoArgs, Short: "Migrate containers", |