diff options
28 files changed, 684 insertions, 328 deletions
diff --git a/cmd/podman/images/scp.go b/cmd/podman/images/scp.go index c89a090bf..67a531e6b 100644 --- a/cmd/podman/images/scp.go +++ b/cmd/podman/images/scp.go @@ -16,6 +16,7 @@ import ( "github.com/containers/podman/v3/cmd/podman/system/connection" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/containers/podman/v3/pkg/rootless" "github.com/docker/distribution/reference" scpD "github.com/dtylman/scp" "github.com/pkg/errors" @@ -125,6 +126,11 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { fmt.Println(rep) // TODO: Add podman remote support default: // else native load + scpOpts.Save.Format = "oci-archive" + _, err := os.Open(scpOpts.Save.Output) + if err != nil { + return err + } if scpOpts.Tag != "" { return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported") } @@ -133,12 +139,20 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { 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 + if !rootless.IsRootless() && scpOpts.Rootless { + err := abiEng.Transfer(context.Background(), scpOpts) + if err != nil { + return err + } + } else { + rep, err := abiEng.Load(context.Background(), scpOpts.Load) + if err != nil { + return err + } + fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ",")) } - fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ",")) } + return nil } @@ -154,7 +168,7 @@ func loadToRemote(localFile string, tag string, url *urlP.URL, iden string) (str n, err := scpD.CopyTo(dial, localFile, remoteFile) if err != nil { - errOut := (strconv.Itoa(int(n)) + " Bytes copied before error") + errOut := strconv.Itoa(int(n)) + " Bytes copied before error" return " ", errors.Wrapf(err, errOut) } run := "" @@ -167,7 +181,7 @@ func loadToRemote(localFile string, tag string, url *urlP.URL, iden string) (str if err != nil { return "", err } - return strings.TrimSuffix(out, "\n"), nil + return strings.TrimSuffix(string(out), "\n"), nil } // saveToRemote takes image information and remote connection information. it connects to the specified client @@ -193,7 +207,7 @@ func saveToRemote(image, localFile string, tag string, uri *urlP.URL, iden strin n, err := scpD.CopyFrom(dial, remoteFile, localFile) connection.ExecRemoteCommand(dial, "rm "+remoteFile) if err != nil { - errOut := (strconv.Itoa(int(n)) + " Bytes copied before error") + errOut := strconv.Itoa(int(n)) + " Bytes copied before error" return errors.Wrapf(err, errOut) } return nil @@ -207,11 +221,7 @@ func makeRemoteFile(dial *ssh.Client) (string, error) { if err != nil { return "", err } - remoteFile = strings.TrimSuffix(remoteFile, "\n") - if err != nil { - return "", err - } - return remoteFile, nil + return strings.TrimSuffix(string(remoteFile), "\n"), nil } // createConnections takes a boolean determining which ssh client to dial @@ -271,7 +281,14 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination scpOpts.SourceImageName = args[0] } case 2: - if strings.Contains(args[0], "::") { + if strings.Contains(args[0], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment + scpOpts.Rootless = true + scpOpts.User = strings.Split(args[1], "@")[0] + scpOpts.SourceImageName = strings.Split(args[0], "::")[1] + if strings.Split(args[0], "@")[0] != "root" { + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo") + } + } else if strings.Contains(args[0], "::") { if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client cliConnections = append(cliConnections, args[0]) scpOpts.ToRemote = true diff --git a/cmd/podman/system/connection/add.go b/cmd/podman/system/connection/add.go index 290b3c245..ee237d7d0 100644 --- a/cmd/podman/system/connection/add.go +++ b/cmd/podman/system/connection/add.go @@ -226,12 +226,7 @@ func getUDS(cmd *cobra.Command, uri *url.URL, iden string) (string, error) { 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) + infoJSON, err := ExecRemoteCommand(dial, podman+" info --format=json") if err != nil { return "", err } diff --git a/cmd/podman/system/connection/remove.go b/cmd/podman/system/connection/remove.go index 73bae4994..ffbea76c5 100644 --- a/cmd/podman/system/connection/remove.go +++ b/cmd/podman/system/connection/remove.go @@ -5,14 +5,14 @@ import ( "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/system" + "github.com/pkg/errors" "github.com/spf13/cobra" ) var ( // Skip creating engines since this command will obtain connection information to said engines rmCmd = &cobra.Command{ - Use: "remove NAME", - Args: cobra.ExactArgs(1), + Use: "remove [options] NAME", Aliases: []string{"rm"}, Long: `Delete named destination from podman configuration`, Short: "Delete named destination", @@ -21,6 +21,10 @@ var ( Example: `podman system connection remove devl podman system connection rm devl`, } + + rmOpts = struct { + All bool + }{} ) func init() { @@ -28,14 +32,31 @@ func init() { Command: rmCmd, Parent: system.ConnectionCmd, }) + + flags := rmCmd.Flags() + flags.BoolVarP(&rmOpts.All, "all", "a", false, "Remove all connections") } -func rm(_ *cobra.Command, args []string) error { +func rm(cmd *cobra.Command, args []string) error { cfg, err := config.ReadCustomConfig() if err != nil { return err } + if rmOpts.All { + if cfg.Engine.ServiceDestinations != nil { + for k := range cfg.Engine.ServiceDestinations { + delete(cfg.Engine.ServiceDestinations, k) + } + } + cfg.Engine.ActiveService = "" + return cfg.Write() + } + + if len(args) != 1 { + return errors.New("accepts 1 arg(s), received 0") + } + if cfg.Engine.ServiceDestinations != nil { delete(cfg.Engine.ServiceDestinations, args[0]) } diff --git a/cmd/podman/system/connection/shared.go b/cmd/podman/system/connection/shared.go index 3fd7c59fb..714ae827d 100644 --- a/cmd/podman/system/connection/shared.go +++ b/cmd/podman/system/connection/shared.go @@ -9,10 +9,10 @@ import ( // 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) { +func ExecRemoteCommand(dial *ssh.Client, run string) ([]byte, error) { sess, err := dial.NewSession() // new ssh client session if err != nil { - return "", err + return nil, err } defer sess.Close() @@ -21,8 +21,7 @@ func ExecRemoteCommand(dial *ssh.Client, run string) (string, error) { 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()) + return nil, errors.Wrapf(err, bufferErr.String()) } - out := buffer.String() // output from command - return out, nil + return buffer.Bytes(), nil } diff --git a/cmd/podman/validate/args.go b/cmd/podman/validate/args.go index fc07a6acc..6b5425a69 100644 --- a/cmd/podman/validate/args.go +++ b/cmd/podman/validate/args.go @@ -27,7 +27,8 @@ func SubCommandExists(cmd *cobra.Command, args []string) error { } return errors.Errorf("unrecognized command `%[1]s %[2]s`\n\nDid you mean this?\n\t%[3]s\n\nTry '%[1]s --help' for more information.", cmd.CommandPath(), args[0], strings.Join(suggestions, "\n\t")) } - return errors.Errorf("missing command '%[1]s COMMAND'\nTry '%[1]s --help' for more information.", cmd.CommandPath()) + cmd.Help() + return errors.Errorf("missing command '%[1]s COMMAND'", cmd.CommandPath()) } // IDOrLatestArgs used to validate a nameOrId was provided or the "--latest" flag diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh index 9b7c613f5..cff8f4b3f 100644 --- a/contrib/cirrus/lib.sh +++ b/contrib/cirrus/lib.sh @@ -166,30 +166,42 @@ setup_rootless() { useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC" - msg "creating ssh key pair for $USER" + mkdir -p "$HOME/.ssh" "/home/$ROOTLESS_USER/.ssh" + + msg "Creating ssh key pairs" [[ -r "$HOME/.ssh/id_rsa" ]] || \ - ssh-keygen -P "" -f "$HOME/.ssh/id_rsa" + ssh-keygen -t rsa -P "" -f "$HOME/.ssh/id_rsa" + ssh-keygen -t ed25519 -P "" -f "/home/$ROOTLESS_USER/.ssh/id_ed25519" + ssh-keygen -t rsa -P "" -f "/home/$ROOTLESS_USER/.ssh/id_rsa" - msg "Allowing ssh key for $ROOTLESS_USER" - akfilepath="/home/$ROOTLESS_USER/.ssh/authorized_keys" - (umask 077 && mkdir "/home/$ROOTLESS_USER/.ssh") - chown -R $ROOTLESS_USER:$ROOTLESS_USER "/home/$ROOTLESS_USER/.ssh" - install -o $ROOTLESS_USER -g $ROOTLESS_USER -m 0600 \ - "$HOME/.ssh/id_rsa.pub" "$akfilepath" - # Makes debugging easier - cat /root/.ssh/authorized_keys >> "$akfilepath" + msg "Setup authorized_keys" + cat $HOME/.ssh/*.pub /home/$ROOTLESS_USER/.ssh/*.pub >> $HOME/.ssh/authorized_keys + cat $HOME/.ssh/*.pub /home/$ROOTLESS_USER/.ssh/*.pub >> /home/$ROOTLESS_USER/.ssh/authorized_keys msg "Ensure the ssh daemon is up and running within 5 minutes" systemctl start sshd - sshcmd="ssh $ROOTLESS_USER@localhost - -o UserKnownHostsFile=/dev/null - -o StrictHostKeyChecking=no - -o CheckHostIP=no" - lilto $sshcmd true # retry until sshd is up - - msg "Configuring rootless user self-access to ssh to localhost" - $sshcmd ssh-keygen -P '""' -f "/home/$ROOTLESS_USER/.ssh/id_rsa" - cat "/home/$ROOTLESS_USER/.ssh/id_rsa" >> "$akfilepath" + lilto systemctl is-active sshd + + msg "Configure ssh file permissions" + chmod -R 700 "$HOME/.ssh" + chmod -R 700 "/home/$ROOTLESS_USER/.ssh" + chown -R $ROOTLESS_USER:$ROOTLESS_USER "/home/$ROOTLESS_USER/.ssh" + + msg " setup known_hosts for $USER" + ssh -q root@localhost \ + -o UserKnownHostsFile=/root/.ssh/known_hosts \ + -o UpdateHostKeys=yes \ + -o StrictHostKeyChecking=no \ + -o CheckHostIP=no \ + true + + msg " setup known_hosts for $ROOTLESS_USER" + su $ROOTLESS_USER -c "ssh -q $ROOTLESS_USER@localhost \ + -o UserKnownHostsFile=/home/$ROOTLESS_USER/.ssh/known_hosts \ + -o UpdateHostKeys=yes \ + -o StrictHostKeyChecking=no \ + -o CheckHostIP=no \ + true" } install_test_configs() { diff --git a/contrib/cirrus/pr-should-include-tests b/contrib/cirrus/pr-should-include-tests index 4b6329311..8103df41d 100755 --- a/contrib/cirrus/pr-should-include-tests +++ b/contrib/cirrus/pr-should-include-tests @@ -12,9 +12,6 @@ fi if [[ "${CIRRUS_CHANGE_MESSAGE}" =~ NO.NEW.TESTS.NEEDED ]]; then exit 0 fi -if [[ "${CIRRUS_CHANGE_MESSAGE}" =~ NO.TESTS.NEEDED ]]; then - exit 0 -fi # HEAD should be good enough, but the CIRRUS envariable allows us to test head=${CIRRUS_CHANGE_IN_REPO:-HEAD} @@ -52,14 +49,11 @@ if [[ -z "$filtered_changes" ]]; then exit 0 fi -# One last chance: perhaps the developer included the magic '[NO (NEW) TESTS NEEDED]' +# One last chance: perhaps the developer included the magic '[NO NEW TESTS NEEDED]' # string in an amended commit. if git log --format=%B ${base}..${head} | fgrep '[NO NEW TESTS NEEDED]'; then exit 0 fi -if git log --format=%B ${base}..${head} | fgrep '[NO TESTS NEEDED]'; then - exit 0 -fi cat <<EOF $(basename $0): PR does not include changes in the 'tests' directory diff --git a/docs/source/markdown/podman-image-scp.1.md b/docs/source/markdown/podman-image-scp.1.md index 420452a4d..4dd79f3d2 100644 --- a/docs/source/markdown/podman-image-scp.1.md +++ b/docs/source/markdown/podman-image-scp.1.md @@ -8,7 +8,7 @@ podman-image-scp - Securely copy an image from one host to another ## DESCRIPTION **podman image scp** copies container images between hosts on a network. You can load to the remote host or from the remote host as well as in between two remote hosts. -Note: `::` is used to specify the image name depending on if you are saving or loading. +Note: `::` is used to specify the image name depending on if you are saving or loading. Images can also be transferred from rootful to rootless storage on the same machine without using sshd. This feature is not supported on the remote client. **podman image scp [GLOBAL OPTIONS]** @@ -62,6 +62,22 @@ Storing signatures Loaded image(s): docker.io/library/alpine:latest ``` +``` +$ sudo podman image scp root@localhost::alpine username@localhost:: +Copying blob e2eb06d8af82 done +Copying config 696d33ca15 done +Writing manifest to image destination +Storing signatures +Run Directory Obtained: /run/user/1000/ +[Run Root: /var/tmp/containers-user-1000/containers Graph Root: /root/.local/share/containers/storage DB Path: /root/.local/share/containers/storage/libpod/bolt_state.db] +Getting image source signatures +Copying blob 5eb901baf107 skipped: already exists +Copying config 696d33ca15 done +Writing manifest to image destination +Storing signatures +Loaded image(s): docker.io/library/alpine:latest +``` + ## SEE ALSO podman(1), podman-load(1), podman-save(1), podman-remote(1), podman-system-connection-add(1), containers.conf(5), containers-transports(5) diff --git a/docs/source/markdown/podman-system-connection-remove.1.md b/docs/source/markdown/podman-system-connection-remove.1.md index faa767176..0af05649c 100644 --- a/docs/source/markdown/podman-system-connection-remove.1.md +++ b/docs/source/markdown/podman-system-connection-remove.1.md @@ -4,11 +4,17 @@ podman\-system\-connection\-remove - Delete named destination ## SYNOPSIS -**podman system connection remove** *name* +**podman system connection remove** [*options*] *name* ## DESCRIPTION Delete named ssh destination. +## OPTIONS + +#### **--all**=*false*, **-a** + +Remove all connections. + ## EXAMPLE ``` $ podman system connection remove production diff --git a/hack/install_catatonit.sh b/hack/install_catatonit.sh index 8837db3a8..0a02b75ab 100755 --- a/hack/install_catatonit.sh +++ b/hack/install_catatonit.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash BASE_PATH="/usr/libexec/podman" CATATONIT_PATH="${BASE_PATH}/catatonit" -CATATONIT_VERSION="v0.1.4" +CATATONIT_VERSION="v0.1.7" set -e if [ -f $CATATONIT_PATH ]; then diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 19b48e14b..fbc2c1f38 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -496,9 +496,27 @@ func (c *Container) setupStorage(ctx context.Context) error { c.setupStorageMapping(&options.IDMappingOptions, &c.config.IDMappings) - containerInfo, err := c.runtime.storageService.CreateContainerStorage(ctx, c.runtime.imageContext, c.config.RootfsImageName, c.config.RootfsImageID, c.config.Name, c.config.ID, options) - if err != nil { - return errors.Wrapf(err, "error creating container storage") + // Unless the user has specified a name, use a randomly generated one. + // Note that name conflicts may occur (see #11735), so we need to loop. + generateName := c.config.Name == "" + var containerInfo ContainerInfo + var containerInfoErr error + for { + if generateName { + name, err := c.runtime.generateName() + if err != nil { + return err + } + c.config.Name = name + } + containerInfo, containerInfoErr = c.runtime.storageService.CreateContainerStorage(ctx, c.runtime.imageContext, c.config.RootfsImageName, c.config.RootfsImageID, c.config.Name, c.config.ID, options) + + if !generateName || errors.Cause(containerInfoErr) != storage.ErrDuplicateName { + break + } + } + if containerInfoErr != nil { + return errors.Wrapf(containerInfoErr, "error creating container storage") } // only reconfig IDMappings if layer was mounted from storage diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 0a7db33f1..114bf9315 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -326,15 +326,6 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai } } - if ctr.config.Name == "" { - name, err := r.generateName() - if err != nil { - return nil, err - } - - ctr.config.Name = name - } - // Check CGroup parent sanity, and set it if it was not set. // Only if we're actually configuring CGroups. if !ctr.config.NoCgroups { diff --git a/libpod/runtime_pod_linux.go b/libpod/runtime_pod_linux.go index 7d7fef4d1..15050ef48 100644 --- a/libpod/runtime_pod_linux.go +++ b/libpod/runtime_pod_linux.go @@ -43,18 +43,6 @@ func (r *Runtime) NewPod(ctx context.Context, p specgen.PodSpecGenerator, option } } - if pod.config.Name == "" { - name, err := r.generateName() - if err != nil { - return nil, err - } - pod.config.Name = name - } - - if p.InfraContainerSpec != nil && p.InfraContainerSpec.Hostname == "" { - p.InfraContainerSpec.Hostname = pod.config.Name - } - // Allocate a lock for the pod lock, err := r.lockManager.AllocateLock() if err != nil { @@ -131,9 +119,33 @@ func (r *Runtime) NewPod(ctx context.Context, p specgen.PodSpecGenerator, option logrus.Infof("Pod has an infra container, but shares no namespaces") } - if err := r.state.AddPod(pod); err != nil { - return nil, errors.Wrapf(err, "error adding pod to state") + // Unless the user has specified a name, use a randomly generated one. + // Note that name conflicts may occur (see #11735), so we need to loop. + generateName := pod.config.Name == "" + var addPodErr error + for { + if generateName { + name, err := r.generateName() + if err != nil { + return nil, err + } + pod.config.Name = name + } + + if p.InfraContainerSpec != nil && p.InfraContainerSpec.Hostname == "" { + p.InfraContainerSpec.Hostname = pod.config.Name + } + if addPodErr = r.state.AddPod(pod); addPodErr == nil { + return pod, nil + } + if !generateName || (errors.Cause(addPodErr) != define.ErrPodExists && errors.Cause(addPodErr) != define.ErrCtrExists) { + break + } + } + if addPodErr != nil { + return nil, errors.Wrapf(addPodErr, "error adding pod to state") } + return pod, nil } diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index b0f9ae408..d72f64b5e 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -27,6 +27,7 @@ type ImageEngine interface { ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error) Shutdown(ctx context.Context) Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error + Transfer(ctx context.Context, scpOpts ImageScpOptions) error Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error) Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error) Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 38cdc8f2f..7583ce442 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -329,6 +329,10 @@ type ImageScpOptions struct { Save ImageSaveOptions // Load options used for the second half of the scp operation Load ImageLoadOptions + // Rootless determines whether we are loading locally from root storage to rootless storage + Rootless bool + // User is used in conjunction with Rootless to determine which user to use to obtain the uid + User string } // ImageTreeOptions provides options for ImageEngine.Tree() diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 7aa202334..5c0227986 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -6,9 +6,12 @@ import ( "io/ioutil" "net/url" "os" + "os/exec" + "os/user" "path" "path/filepath" "strconv" + "strings" "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" @@ -18,6 +21,7 @@ import ( "github.com/containers/image/v5/signature" "github.com/containers/image/v5/transports" "github.com/containers/image/v5/transports/alltransports" + "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/entities/reports" domainUtils "github.com/containers/podman/v3/pkg/domain/utils" @@ -330,6 +334,67 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri return pushError } +// Transfer moves images from root to rootless storage so the user specified in the scp call can access and use the image modified by root +func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error { + if scpOpts.User == "" { + return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage") + } + var u *user.User + scpOpts.User = strings.Split(scpOpts.User, ":")[0] // split in case provided with uid:gid + _, err := strconv.Atoi(scpOpts.User) + if err != nil { + u, err = user.Lookup(scpOpts.User) + if err != nil { + return err + } + } else { + u, err = user.LookupId(scpOpts.User) + if err != nil { + return err + } + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + return err + } + gid, err := strconv.Atoi(u.Gid) + if err != nil { + return err + } + err = os.Chown(scpOpts.Save.Output, uid, gid) // chown the output because was created by root so we need to give th euser read access + if err != nil { + return err + } + + podman, err := os.Executable() + if err != nil { + return err + } + machinectl, err := exec.LookPath("machinectl") + if err != nil { + logrus.Warn("defaulting to su since machinectl is not available, su will fail if no user session is available") + cmd := exec.Command("su", "-l", u.Username, "--command", podman+" --log-level="+logrus.GetLevel().String()+" --cgroup-manager=cgroupfs load --input="+scpOpts.Save.Output) // load the new image to the rootless storage + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + logrus.Debug("Executing load command su") + err = cmd.Run() + if err != nil { + return err + } + } else { + cmd := exec.Command(machinectl, "shell", "-q", u.Username+"@.host", podman, "--log-level="+logrus.GetLevel().String(), "--cgroup-manager=cgroupfs", "load", "--input", scpOpts.Save.Output) // load the new image to the rootless storage + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + logrus.Debug("Executing load command machinectl") + err = cmd.Run() + if err != nil { + return err + } + } + + return nil +} + func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error { // Allow tagging manifest list instead of resolving instances from manifest lookupOptions := &libimage.LookupImageOptions{ManifestList: true} diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index e17f746a5..fde57972f 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -12,6 +12,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/types" + "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/bindings/images" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/entities/reports" @@ -122,6 +123,10 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities. return &entities.ImagePullReport{Images: pulledImages}, nil } +func (ir *ImageEngine) Transfer(ctx context.Context, scpOpts entities.ImageScpOptions) error { + return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage") +} + func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, opt entities.ImageTagOptions) error { options := new(images.TagOptions) for _, newTag := range tags { diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index e598f7ab9..200faae2d 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -208,9 +208,7 @@ var _ = SynchronizedAfterSuite(func() {}, // PodmanTestCreate creates a PodmanTestIntegration instance for the tests func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { - var ( - podmanRemoteBinary string - ) + var podmanRemoteBinary string host := GetHostDistributionInfo() cwd, _ := os.Getwd() @@ -220,12 +218,11 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { podmanBinary = os.Getenv("PODMAN_BINARY") } - if remote { - podmanRemoteBinary = filepath.Join(cwd, "../../bin/podman-remote") - if os.Getenv("PODMAN_REMOTE_BINARY") != "" { - podmanRemoteBinary = os.Getenv("PODMAN_REMOTE_BINARY") - } + podmanRemoteBinary = filepath.Join(cwd, "../../bin/podman-remote") + if os.Getenv("PODMAN_REMOTE_BINARY") != "" { + podmanRemoteBinary = os.Getenv("PODMAN_REMOTE_BINARY") } + conmonBinary := filepath.Join("/usr/libexec/podman/conmon") altConmonBinary := "/usr/bin/conmon" if _, err := os.Stat(conmonBinary); os.IsNotExist(err) { @@ -271,12 +268,13 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { p := &PodmanTestIntegration{ PodmanTest: PodmanTest{ - PodmanBinary: podmanBinary, - ArtifactPath: ARTIFACT_DIR, - TempDir: tempDir, - RemoteTest: remote, - ImageCacheFS: storageFs, - ImageCacheDir: ImageCacheDir, + PodmanBinary: podmanBinary, + RemotePodmanBinary: podmanRemoteBinary, + ArtifactPath: ARTIFACT_DIR, + TempDir: tempDir, + RemoteTest: remote, + ImageCacheFS: storageFs, + ImageCacheDir: ImageCacheDir, }, ConmonBinary: conmonBinary, CrioRoot: filepath.Join(tempDir, "crio"), @@ -289,8 +287,8 @@ func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration { CgroupManager: cgroupManager, Host: host, } + if remote { - p.PodmanTest.RemotePodmanBinary = podmanRemoteBinary uuid := stringid.GenerateNonCryptoID() if !rootless.IsRootless() { p.RemoteSocket = fmt.Sprintf("unix:/run/podman/podman-%s.sock", uuid) @@ -632,6 +630,19 @@ func SkipIfNotRootless(reason string) { } } +func SkipIfSystemdNotRunning(reason string) { + checkReason(reason) + + cmd := exec.Command("systemctl", "list-units") + err := cmd.Run() + if err != nil { + if _, ok := err.(*exec.Error); ok { + ginkgo.Skip("[notSystemd]: not running " + reason) + } + Expect(err).ToNot(HaveOccurred()) + } +} + func SkipIfNotSystemd(manager, reason string) { checkReason(reason) if manager != "systemd" { @@ -683,6 +694,41 @@ func SkipIfContainerized(reason string) { } } +func SkipIfRemote(reason string) { + checkReason(reason) + if !IsRemote() { + return + } + ginkgo.Skip("[remote]: " + reason) +} + +// SkipIfInContainer skips a test if the test is run inside a container +func SkipIfInContainer(reason string) { + checkReason(reason) + if os.Getenv("TEST_ENVIRON") == "container" { + Skip("[container]: " + reason) + } +} + +// SkipIfNotActive skips a test if the given systemd unit is not active +func SkipIfNotActive(unit string, reason string) { + checkReason(reason) + + var buffer bytes.Buffer + cmd := exec.Command("systemctl", "is-active", unit) + cmd.Stdout = &buffer + err := cmd.Start() + Expect(err).ToNot(HaveOccurred()) + + err = cmd.Wait() + Expect(err).ToNot(HaveOccurred()) + + Expect(err).ToNot(HaveOccurred()) + if strings.TrimSpace(buffer.String()) != "active" { + Skip(fmt.Sprintf("[systemd]: unit %s is not active: %s", unit, reason)) + } +} + // PodmanAsUser is the exec call to podman on the filesystem with the specified uid/gid and environment func (p *PodmanTestIntegration) PodmanAsUser(args []string, uid, gid uint32, cwd string, env []string) *PodmanSessionIntegration { podmanSession := p.PodmanAsUserBase(args, uid, gid, cwd, env, false, false, nil, nil) diff --git a/test/e2e/image_scp_test.go b/test/e2e/image_scp_test.go index 9fd8d7e27..acea2993d 100644 --- a/test/e2e/image_scp_test.go +++ b/test/e2e/image_scp_test.go @@ -22,12 +22,14 @@ var _ = Describe("podman image scp", func() { ) BeforeEach(func() { + ConfPath.Value, ConfPath.IsSet = os.LookupEnv("CONTAINERS_CONF") conf, err := ioutil.TempFile("", "containersconf") if err != nil { panic(err) } os.Setenv("CONTAINERS_CONF", conf.Name()) + tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) @@ -38,6 +40,7 @@ var _ = Describe("podman image scp", func() { AfterEach(func() { podmanTest.Cleanup() + os.Remove(os.Getenv("CONTAINERS_CONF")) if ConfPath.IsSet { os.Setenv("CONTAINERS_CONF", ConfPath.Value) @@ -58,6 +61,25 @@ var _ = Describe("podman image scp", func() { Expect(scp).To(Exit(0)) }) + It("podman image scp root to rootless transfer", func() { + SkipIfNotRootless("this is a rootless only test, transfering from root to rootless using PodmanAsUser") + if IsRemote() { + Skip("this test is only for non-remote") + } + env := os.Environ() + img := podmanTest.PodmanAsUser([]string{"image", "pull", ALPINE}, 0, 0, "", env) // pull image to root + img.WaitWithDefaultTimeout() + Expect(img).To(Exit(0)) + scp := podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE, "1000:1000@localhost::"}, 0, 0, "", env) //transfer from root to rootless (us) + scp.WaitWithDefaultTimeout() + Expect(scp).To(Exit(0)) + + list := podmanTest.Podman([]string{"image", "list"}) // our image should now contain alpine loaded in from root + list.WaitWithDefaultTimeout() + Expect(list).To(Exit(0)) + Expect(list.LineInOutputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) + }) + It("podman image scp bogus image", func() { if IsRemote() { Skip("this test is only for non-remote") diff --git a/test/e2e/libpod_suite_remote_test.go b/test/e2e/libpod_suite_remote_test.go index ad511cc9e..1fa29daa1 100644 --- a/test/e2e/libpod_suite_remote_test.go +++ b/test/e2e/libpod_suite_remote_test.go @@ -16,20 +16,12 @@ import ( "time" "github.com/containers/podman/v3/pkg/rootless" - "github.com/onsi/ginkgo" ) func IsRemote() bool { return true } -func SkipIfRemote(reason string) { - if len(reason) < 5 { - panic("SkipIfRemote must specify a reason to skip") - } - ginkgo.Skip("[remote]: " + reason) -} - // Podman is the exec call to podman on the filesystem func (p *PodmanTestIntegration) Podman(args []string) *PodmanSessionIntegration { var remoteArgs = []string{"--remote", "--url", p.RemoteSocket} diff --git a/test/e2e/libpod_suite_test.go b/test/e2e/libpod_suite_test.go index 6d2d3fee8..001a869b1 100644 --- a/test/e2e/libpod_suite_test.go +++ b/test/e2e/libpod_suite_test.go @@ -16,9 +16,6 @@ func IsRemote() bool { return false } -func SkipIfRemote(string) { -} - // Podman is the exec call to podman on the filesystem func (p *PodmanTestIntegration) Podman(args []string) *PodmanSessionIntegration { podmanSession := p.PodmanBase(args, false, false) diff --git a/test/e2e/system_connection_test.go b/test/e2e/system_connection_test.go index 842ae8df6..c0e29d525 100644 --- a/test/e2e/system_connection_test.go +++ b/test/e2e/system_connection_test.go @@ -3,7 +3,11 @@ package integration import ( "fmt" "io/ioutil" + "net/url" "os" + "os/exec" + "os/user" + "path/filepath" "github.com/containers/common/pkg/config" . "github.com/containers/podman/v3/test/utils" @@ -19,22 +23,16 @@ var _ = Describe("podman system connection", func() { IsSet bool }{} - var ( - podmanTest *PodmanTestIntegration - ) + var podmanTest *PodmanTestIntegration BeforeEach(func() { ConfPath.Value, ConfPath.IsSet = os.LookupEnv("CONTAINERS_CONF") conf, err := ioutil.TempFile("", "containersconf") - if err != nil { - panic(err) - } + Expect(err).ToNot(HaveOccurred()) os.Setenv("CONTAINERS_CONF", conf.Name()) tempdir, err := CreateTempDirInTempDir() - if err != nil { - panic(err) - } + Expect(err).ToNot(HaveOccurred()) podmanTest = PodmanTestCreate(tempdir) podmanTest.Setup() }) @@ -49,196 +47,241 @@ var _ = Describe("podman system connection", func() { } f := CurrentGinkgoTestDescription() - timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) - GinkgoWriter.Write([]byte(timedResult)) + GinkgoWriter.Write( + []byte( + fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()))) }) - It("add ssh://", func() { - cmd := []string{"system", "connection", "add", - "--default", - "--identity", "~/.ssh/id_rsa", - "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", - } - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QA")) - Expect(cfg.Engine.ServiceDestinations["QA"]).To(Equal( - config.Destination{ - URI: "ssh://root@server.fubar.com:2222/run/podman/podman.sock", - Identity: "~/.ssh/id_rsa", - }, - )) - - cmd = []string{"system", "connection", "rename", - "QA", - "QE", - } - session = podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - - cfg, err = config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QE")) - Expect(cfg.Engine.ServiceDestinations["QE"]).To(Equal( - config.Destination{ - URI: "ssh://root@server.fubar.com:2222/run/podman/podman.sock", - Identity: "~/.ssh/id_rsa", - }, - )) - }) + Context("without running API service", func() { + It("add ssh://", func() { + cmd := []string{"system", "connection", "add", + "--default", + "--identity", "~/.ssh/id_rsa", + "QA", + "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + } + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) - It("add UDS", func() { - cmd := []string{"system", "connection", "add", - "QA-UDS", - "unix:///run/podman/podman.sock", - } - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QA-UDS")) - Expect(cfg.Engine.ServiceDestinations["QA-UDS"]).To(Equal( - config.Destination{ - URI: "unix:///run/podman/podman.sock", - Identity: "", - }, - )) - - cmd = []string{"system", "connection", "add", - "QA-UDS1", - "--socket-path", "/run/user/podman/podman.sock", - "unix:///run/podman/podman.sock", - } - session = podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err = config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QA-UDS")) - Expect(cfg.Engine.ServiceDestinations["QA-UDS1"]).To(Equal( - config.Destination{ - URI: "unix:///run/user/podman/podman.sock", - Identity: "", - }, - )) - }) + cfg, err := config.ReadCustomConfig() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).To(HaveActiveService("QA")) + Expect(cfg).Should(VerifyService( + "QA", + "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + "~/.ssh/id_rsa", + )) - It("add tcp", func() { - cmd := []string{"system", "connection", "add", - "QA-TCP", - "tcp://localhost:8888", - } - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("QA-TCP")) - Expect(cfg.Engine.ServiceDestinations["QA-TCP"]).To(Equal( - config.Destination{ - URI: "tcp://localhost:8888", - Identity: "", - }, - )) - }) + cmd = []string{"system", "connection", "rename", + "QA", + "QE", + } + session = podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) - It("remove", func() { - cmd := []string{"system", "connection", "add", - "--default", - "--identity", "~/.ssh/id_rsa", - "QA", - "ssh://root@server.fubar.com:2222/run/podman/podman.sock", - } - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) + Expect(config.ReadCustomConfig()).To(HaveActiveService("QE")) + }) - for i := 0; i < 2; i++ { - cmd = []string{"system", "connection", "remove", "QA"} + It("add UDS", func() { + cmd := []string{"system", "connection", "add", + "QA-UDS", + "unix:///run/podman/podman.sock", + } + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + + Expect(config.ReadCustomConfig()).Should(VerifyService( + "QA-UDS", + "unix:///run/podman/podman.sock", + "", + )) + + cmd = []string{"system", "connection", "add", + "QA-UDS1", + "--socket-path", "/run/user/podman/podman.sock", + "unix:///run/podman/podman.sock", + } session = podmanTest.Podman(cmd) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) + Expect(session.Out.Contents()).Should(BeEmpty()) - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(BeEmpty()) - Expect(cfg.Engine.ServiceDestinations).To(BeEmpty()) - } - }) + Expect(config.ReadCustomConfig()).Should(HaveActiveService("QA-UDS")) + Expect(config.ReadCustomConfig()).Should(VerifyService( + "QA-UDS1", + "unix:///run/user/podman/podman.sock", + "", + )) + }) - It("default", func() { - for _, name := range []string{"devl", "qe"} { + It("add tcp", func() { cmd := []string{"system", "connection", "add", + "QA-TCP", + "tcp://localhost:8888", + } + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + + Expect(config.ReadCustomConfig()).Should(VerifyService( + "QA-TCP", + "tcp://localhost:8888", + "", + )) + }) + + It("remove", func() { + session := podmanTest.Podman([]string{"system", "connection", "add", + "--default", + "--identity", "~/.ssh/id_rsa", + "QA", + "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + }) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // two passes to test that removing non-existent connection is not an error + for i := 0; i < 2; i++ { + session = podmanTest.Podman([]string{"system", "connection", "remove", "QA"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + + cfg, err := config.ReadCustomConfig() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg.Engine.ActiveService).To(BeEmpty()) + Expect(cfg.Engine.ServiceDestinations).To(BeEmpty()) + } + }) + + It("remove --all", func() { + session := podmanTest.Podman([]string{"system", "connection", "add", "--default", "--identity", "~/.ssh/id_rsa", - name, + "QA", "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + }) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"system", "connection", "remove", "--all"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + Expect(session.Err.Contents()).Should(BeEmpty()) + + session = podmanTest.Podman([]string{"system", "connection", "list"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + }) + + It("default", func() { + for _, name := range []string{"devl", "qe"} { + cmd := []string{"system", "connection", "add", + "--default", + "--identity", "~/.ssh/id_rsa", + name, + "ssh://root@server.fubar.com:2222/run/podman/podman.sock", + } + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) } + + cmd := []string{"system", "connection", "default", "devl"} session := podmanTest.Podman(cmd) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - } + Expect(session.Out.Contents()).Should(BeEmpty()) - cmd := []string{"system", "connection", "default", "devl"} - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - - cfg, err := config.ReadCustomConfig() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg.Engine.ActiveService).To(Equal("devl")) - - cmd = []string{"system", "connection", "list"} - session = podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("Name *URI *Identity *Default")) - - cmd = []string{"system", "connection", "list", "--format", "{{.Name}}"} - session = podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.OutputToString()).Should(Equal("devl qe")) - }) + Expect(config.ReadCustomConfig()).Should(HaveActiveService("devl")) - It("failed default", func() { - cmd := []string{"system", "connection", "default", "devl"} - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).ShouldNot(Exit(0)) - Expect(session.Err).Should(Say("destination is not defined")) - }) + cmd = []string{"system", "connection", "list"} + session = podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out).Should(Say("Name *URI *Identity *Default")) + + cmd = []string{"system", "connection", "list", "--format", "{{.Name}}"} + session = podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).Should(Equal("devl qe")) + }) + + It("failed default", func() { + cmd := []string{"system", "connection", "default", "devl"} + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).ShouldNot(Exit(0)) + Expect(session.Err).Should(Say("destination is not defined")) + }) + + It("failed rename", func() { + cmd := []string{"system", "connection", "rename", "devl", "QE"} + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).ShouldNot(Exit(0)) + Expect(session.Err).Should(Say("destination is not defined")) + }) - It("failed rename", func() { - cmd := []string{"system", "connection", "rename", "devl", "QE"} - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).ShouldNot(Exit(0)) - Expect(session.Err).Should(Say("destination is not defined")) + It("empty list", func() { + cmd := []string{"system", "connection", "list"} + session := podmanTest.Podman(cmd) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + Expect(session.Err.Contents()).Should(BeEmpty()) + }) }) - It("empty list", func() { - cmd := []string{"system", "connection", "list"} - session := podmanTest.Podman(cmd) - session.WaitWithDefaultTimeout() - Expect(session).Should(Exit(0)) - Expect(session.Out).Should(Say("")) - Expect(session.Err).Should(Say("")) + Context("sshd and API services required", func() { + BeforeEach(func() { + // These tests are unique in as much as they require podman, podman-remote, systemd and sshd. + // podman-remote commands will be executed by ginkgo directly. + SkipIfContainerized("sshd is not available when running in a container") + SkipIfRemote("connection heuristic requires both podman and podman-remote binaries") + SkipIfNotRootless("FIXME: setup ssh keys when root") + SkipIfSystemdNotRunning("cannot test connection heuristic if systemd is not running") + SkipIfNotActive("sshd", "cannot test connection heuristic if sshd is not running") + }) + + It("add ssh:// socket path using connection heuristic", func() { + u, err := user.Current() + Expect(err).ShouldNot(HaveOccurred()) + + cmd := exec.Command(podmanTest.RemotePodmanBinary, + "system", "connection", "add", + "--default", + "--identity", filepath.Join(u.HomeDir, ".ssh", "id_ed25519"), + "QA", + fmt.Sprintf("ssh://%s@localhost", u.Username)) + + session, err := Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%q failed to execute", podmanTest.RemotePodmanBinary)) + Eventually(session, DefaultWaitTimeout).Should(Exit(0)) + Expect(session.Out.Contents()).Should(BeEmpty()) + Expect(session.Err.Contents()).Should(BeEmpty()) + + uri := url.URL{ + Scheme: "ssh", + User: url.User(u.Username), + Host: "localhost:22", + Path: fmt.Sprintf("/run/user/%s/podman/podman.sock", u.Uid), + } + + Expect(config.ReadCustomConfig()).Should(HaveActiveService("QA")) + Expect(config.ReadCustomConfig()).Should(VerifyService( + "QA", + uri.String(), + filepath.Join(u.HomeDir, ".ssh", "id_ed25519"), + )) + }) }) }) diff --git a/test/system/001-basic.bats b/test/system/001-basic.bats index 78b8ecdfd..03f07d602 100644 --- a/test/system/001-basic.bats +++ b/test/system/001-basic.bats @@ -120,9 +120,7 @@ function setup() { fi run_podman 125 --remote - is "$output" "Error: missing command 'podman COMMAND' -Try 'podman --help' for more information." \ - "podman --remote show usage message without running endpoint" + is "$output" ".*Usage:" "podman --remote show usage message without running endpoint" } # This is for development only; it's intended to make sure our timeout diff --git a/test/system/015-help.bats b/test/system/015-help.bats index b0795b524..a87081687 100644 --- a/test/system/015-help.bats +++ b/test/system/015-help.bats @@ -149,12 +149,12 @@ function check_help() { count=$(expr $count + 1) done - # Any command that takes subcommands, must throw error if called + # Any command that takes subcommands, prints its help and errors if called # without one. dprint "podman $@" run_podman '?' "$@" is "$status" 125 "'podman $*' without any subcommand - exit status" - is "$output" "Error: missing command .*$@ COMMAND" \ + is "$output" ".*Usage:.*Error: missing command '.*$@ COMMAND'" \ "'podman $*' without any subcommand - expected error message" # Assume that 'NoSuchCommand' is not a command diff --git a/test/system/272-system-connection.bats b/test/system/272-system-connection.bats index 5a90d9398..14c4f6664 100644 --- a/test/system/272-system-connection.bats +++ b/test/system/272-system-connection.bats @@ -34,10 +34,7 @@ function teardown() { | xargs -l1 --no-run-if-empty umount # Remove all system connections - run_podman system connection ls --format json - while read name; do - run_podman system connection rm "$name" - done < <(jq -r '.[].Name' <<<"$output") + run_podman system connection rm --all basic_teardown } diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index b3471b425..c86497f4c 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -167,6 +167,13 @@ load helpers $IMAGE nc -l -n -v -p $myport cid="$output" + # FIXME: debugging for #11871 + run_podman exec $cid cat /etc/resolv.conf + if is_rootless; then + run_podman unshare --rootless-cni cat /etc/resolv.conf + fi + ps uxww + # check that dns is working inside the container run_podman exec $cid nslookup google.com diff --git a/test/utils/matchers.go b/test/utils/matchers.go index 07c1232e7..17ff3ea75 100644 --- a/test/utils/matchers.go +++ b/test/utils/matchers.go @@ -2,57 +2,164 @@ package utils import ( "fmt" + "net/url" + "github.com/containers/common/pkg/config" + . "github.com/onsi/gomega" "github.com/onsi/gomega/format" "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/matchers" + "github.com/onsi/gomega/types" ) +// HaveActiveService verifies the given service is the active service +func HaveActiveService(name interface{}) OmegaMatcher { + return WithTransform( + func(cfg *config.Config) string { + return cfg.Engine.ActiveService + }, + Equal(name)) +} + +type ServiceMatcher struct { + types.GomegaMatcher + Name interface{} + URI interface{} + Identity interface{} + failureMessage string + negatedFailureMessage string +} + +func VerifyService(name, uri, identity interface{}) OmegaMatcher { + return &ServiceMatcher{ + Name: name, + URI: uri, + Identity: identity, + } +} + +func (matcher *ServiceMatcher) Match(actual interface{}) (success bool, err error) { + cfg, ok := actual.(*config.Config) + if !ok { + return false, fmt.Errorf("ServiceMatcher matcher expects a config.Config") + } + + if _, err = url.Parse(matcher.URI.(string)); err != nil { + return false, err + } + + success, err = HaveKey(matcher.Name).Match(cfg.Engine.ServiceDestinations) + if !success || err != nil { + matcher.failureMessage = HaveKey(matcher.Name).FailureMessage(cfg.Engine.ServiceDestinations) + matcher.negatedFailureMessage = HaveKey(matcher.Name).NegatedFailureMessage(cfg.Engine.ServiceDestinations) + return + } + + sd := cfg.Engine.ServiceDestinations[matcher.Name.(string)] + success, err = Equal(matcher.URI).Match(sd.URI) + if !success || err != nil { + matcher.failureMessage = Equal(matcher.URI).FailureMessage(sd.URI) + matcher.negatedFailureMessage = Equal(matcher.URI).NegatedFailureMessage(sd.URI) + return + } + + success, err = Equal(matcher.Identity).Match(sd.Identity) + if !success || err != nil { + matcher.failureMessage = Equal(matcher.Identity).FailureMessage(sd.Identity) + matcher.negatedFailureMessage = Equal(matcher.Identity).NegatedFailureMessage(sd.Identity) + return + } + + return true, nil +} + +func (matcher *ServiceMatcher) FailureMessage(_ interface{}) string { + return matcher.failureMessage +} + +func (matcher *ServiceMatcher) NegatedFailureMessage(_ interface{}) string { + return matcher.negatedFailureMessage +} + +type URLMatcher struct { + matchers.EqualMatcher +} + +// VerifyURL matches when actual is a valid URL and matches expected +func VerifyURL(uri interface{}) OmegaMatcher { + return &URLMatcher{matchers.EqualMatcher{Expected: uri}} +} + +func (matcher *URLMatcher) Match(actual interface{}) (bool, error) { + e, ok := matcher.Expected.(string) + if !ok { + return false, fmt.Errorf("VerifyURL requires string inputs %T is not supported", matcher.Expected) + } + e_uri, err := url.Parse(e) + if err != nil { + return false, err + } + + a, ok := actual.(string) + if !ok { + return false, fmt.Errorf("VerifyURL requires string inputs %T is not supported", actual) + } + a_uri, err := url.Parse(a) + if err != nil { + return false, err + } + + return (&matchers.EqualMatcher{Expected: e_uri}).Match(a_uri) +} + +type ExitMatcher struct { + types.GomegaMatcher + Expected int + Actual int +} + // ExitWithError matches when assertion is > argument. Default 0 -// Modeled after the gomega Exit() matcher -func ExitWithError(optionalExitCode ...int) *exitMatcher { +// Modeled after the gomega Exit() matcher and also operates on sessions. +func ExitWithError(optionalExitCode ...int) *ExitMatcher { exitCode := 0 if len(optionalExitCode) > 0 { exitCode = optionalExitCode[0] } - return &exitMatcher{exitCode: exitCode} + return &ExitMatcher{Expected: exitCode} } -type exitMatcher struct { - exitCode int - actualExitCode int -} - -func (m *exitMatcher) Match(actual interface{}) (success bool, err error) { +// Match follows gexec.Matcher interface +func (matcher *ExitMatcher) Match(actual interface{}) (success bool, err error) { exiter, ok := actual.(gexec.Exiter) if !ok { return false, fmt.Errorf("ExitWithError must be passed a gexec.Exiter (Missing method ExitCode() int) Got:\n#{format.Object(actual, 1)}") } - m.actualExitCode = exiter.ExitCode() - if m.actualExitCode == -1 { + matcher.Actual = exiter.ExitCode() + if matcher.Actual == -1 { return false, nil } - return m.actualExitCode > m.exitCode, nil + return matcher.Actual > matcher.Expected, nil } -func (m *exitMatcher) FailureMessage(actual interface{}) (message string) { - if m.actualExitCode == -1 { +func (matcher *ExitMatcher) FailureMessage(_ interface{}) (message string) { + if matcher.Actual == -1 { return "Expected process to exit. It did not." } - return format.Message(m.actualExitCode, "to be greater than exit code:", m.exitCode) + return format.Message(matcher.Actual, "to be greater than exit code: ", matcher.Expected) } -func (m *exitMatcher) NegatedFailureMessage(actual interface{}) (message string) { - if m.actualExitCode == -1 { +func (matcher *ExitMatcher) NegatedFailureMessage(_ interface{}) (message string) { + switch { + case matcher.Actual == -1: return "you really shouldn't be able to see this!" - } else { - if m.exitCode == -1 { - return "Expected process not to exit. It did." - } - return format.Message(m.actualExitCode, "is less than or equal to exit code:", m.exitCode) + case matcher.Expected == -1: + return "Expected process not to exit. It did." } + return format.Message(matcher.Actual, "is less than or equal to exit code: ", matcher.Expected) } -func (m *exitMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + +func (matcher *ExitMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { session, ok := actual.(*gexec.Session) if ok { return session.ExitCode() == -1 diff --git a/test/utils/utils.go b/test/utils/utils.go index d4d5e6e2f..8d1edb23a 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -397,7 +397,7 @@ func tagOutputToMap(imagesOutput []string) map[string]map[string]bool { return m } -// GetHostDistributionInfo returns a struct with its distribution name and version +// GetHostDistributionInfo returns a struct with its distribution Name and version func GetHostDistributionInfo() HostOS { f, err := os.Open(OSReleasePath) defer f.Close() @@ -491,13 +491,3 @@ func RandomString(n int) string { } return string(b) } - -//SkipIfInContainer skips a test if the test is run inside a container -func SkipIfInContainer(reason string) { - if len(reason) < 5 { - panic("SkipIfInContainer must specify a reason to skip") - } - if os.Getenv("TEST_ENVIRON") == "container" { - Skip("[container]: " + reason) - } -} |