aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/podman/common/completion.go4
-rw-r--r--cmd/podman/common/create.go9
-rw-r--r--cmd/podman/common/create_opts.go9
-rw-r--r--cmd/podman/common/specgen.go105
-rw-r--r--cmd/podman/containers/cp.go177
-rw-r--r--cmd/podman/containers/create.go2
-rw-r--r--cmd/podman/containers/logs.go14
-rw-r--r--cmd/podman/containers/restore.go6
-rw-r--r--cmd/podman/images/import.go19
-rw-r--r--cmd/podman/images/scp.go348
-rw-r--r--cmd/podman/login.go1
-rw-r--r--cmd/podman/logout.go1
-rw-r--r--cmd/podman/parse/net.go7
-rw-r--r--cmd/podman/registry/config.go3
-rw-r--r--cmd/podman/root.go11
-rw-r--r--cmd/podman/system/connection/add.go126
-rw-r--r--cmd/podman/system/connection/shared.go28
-rw-r--r--cmd/podman/system/migrate.go5
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",