diff options
29 files changed, 1048 insertions, 105 deletions
diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index 3802c37b0..f9e4d7ca5 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -10,7 +10,7 @@ type ContainerCLIOpts struct { BlkIOWeightDevice []string CapAdd []string CapDrop []string - CGroupsNS string + CgroupNS string CGroupsMode string CGroupParent string CIDFile string diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 07c88efea..0b6897d3a 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -186,6 +186,46 @@ func getMemoryLimits(s *specgen.SpecGenerator, c *ContainerCLIOpts) (*specs.Linu return memory, nil } +func setNamespaces(s *specgen.SpecGenerator, c *ContainerCLIOpts) error { + var err error + + if c.PID != "" { + s.PidNS, err = specgen.ParseNamespace(c.PID) + if err != nil { + return err + } + } + if c.IPC != "" { + s.IpcNS, err = specgen.ParseNamespace(c.IPC) + if err != nil { + return err + } + } + if c.UTS != "" { + s.UtsNS, err = specgen.ParseNamespace(c.UTS) + if err != nil { + return err + } + } + if c.CgroupNS != "" { + s.CgroupNS, err = specgen.ParseNamespace(c.CgroupNS) + if err != nil { + return err + } + } + // userns must be treated differently + if c.UserNS != "" { + s.UserNS, err = specgen.ParseUserNamespace(c.UserNS) + if err != nil { + return err + } + } + if c.Net != nil { + s.NetNS = c.Net.Network + } + return nil +} + func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) error { var ( err error @@ -252,28 +292,8 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string } s.Expose = expose - for k, v := range map[string]*specgen.Namespace{ - c.IPC: &s.IpcNS, - c.PID: &s.PidNS, - c.UTS: &s.UtsNS, - c.CGroupsNS: &s.CgroupNS, - } { - if k != "" { - *v, err = specgen.ParseNamespace(k) - if err != nil { - return err - } - } - } - // userns must be treated differently - if c.UserNS != "" { - s.UserNS, err = specgen.ParseUserNamespace(c.UserNS) - if err != nil { - return err - } - } - if c.Net != nil { - s.NetNS = c.Net.Network + if err := setNamespaces(s, c); err != nil { + return err } if sig := c.StopSignal; len(sig) > 0 { diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index dcbc52b56..dd77dc9d7 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -195,7 +195,7 @@ func createInit(c *cobra.Command) error { cliVals.IPC = c.Flag("ipc").Value.String() cliVals.UTS = c.Flag("uts").Value.String() cliVals.PID = c.Flag("pid").Value.String() - cliVals.CGroupsNS = c.Flag("cgroupns").Value.String() + cliVals.CgroupNS = c.Flag("cgroupns").Value.String() if c.Flag("entrypoint").Changed { val := c.Flag("entrypoint").Value.String() cliVals.Entrypoint = &val diff --git a/cmd/podman/images/build.go b/cmd/podman/images/build.go index de750d892..400f960cc 100644 --- a/cmd/podman/images/build.go +++ b/cmd/podman/images/build.go @@ -138,36 +138,9 @@ func build(cmd *cobra.Command, args []string) error { return errors.New("cannot specify --squash, --squash-all and --layers options together") } - contextDir, containerFiles, err := extractContextAndFiles(args, buildOpts.File) - if err != nil { - return err - } - - ie, err := registry.NewImageEngine(cmd, args) - if err != nil { - return err - } - - apiBuildOpts, err := buildFlagsWrapperToOptions(cmd, contextDir, &buildOpts) - if err != nil { - return err - } - - _, err = ie.Build(registry.GetContext(), containerFiles, *apiBuildOpts) - return err -} - -// extractContextAndFiles parses args and files to extract a context directory -// and {Container,Docker}files. -// -// TODO: this was copied and altered from the v1 client which in turn was -// copied and altered from the Buildah code. Ideally, all of this code should -// be cleanly consolidated into a package that is shared between Buildah and -// Podman. -func extractContextAndFiles(args, files []string) (string, []string, error) { // Extract container files from the CLI (i.e., --file/-f) first. var containerFiles []string - for _, f := range files { + for _, f := range buildOpts.File { if f == "-" { containerFiles = append(containerFiles, "/dev/stdin") } else { @@ -181,7 +154,7 @@ func extractContextAndFiles(args, files []string) (string, []string, error) { // The context directory could be a URL. Try to handle that. tempDir, subDir, err := imagebuildah.TempDirForURL("", "buildah", args[0]) if err != nil { - return "", nil, errors.Wrapf(err, "error prepping temporary context directory") + return errors.Wrapf(err, "error prepping temporary context directory") } if tempDir != "" { // We had to download it to a temporary directory. @@ -196,7 +169,7 @@ func extractContextAndFiles(args, files []string) (string, []string, error) { // Nope, it was local. Use it as is. absDir, err := filepath.Abs(args[0]) if err != nil { - return "", nil, errors.Wrapf(err, "error determining path to directory %q", args[0]) + return errors.Wrapf(err, "error determining path to directory %q", args[0]) } contextDir = absDir } @@ -212,7 +185,7 @@ func extractContextAndFiles(args, files []string) (string, []string, error) { } absFile, err := filepath.Abs(containerFiles[i]) if err != nil { - return "", nil, errors.Wrapf(err, "error determining path to file %q", containerFiles[i]) + return errors.Wrapf(err, "error determining path to file %q", containerFiles[i]) } contextDir = filepath.Dir(absFile) break @@ -220,10 +193,10 @@ func extractContextAndFiles(args, files []string) (string, []string, error) { } if contextDir == "" { - return "", nil, errors.Errorf("no context directory and no Containerfile specified") + return errors.Errorf("no context directory and no Containerfile specified") } if !utils.IsDir(contextDir) { - return "", nil, errors.Errorf("context must be a directory: %q", contextDir) + return errors.Errorf("context must be a directory: %q", contextDir) } if len(containerFiles) == 0 { if utils.FileExists(filepath.Join(contextDir, "Containerfile")) { @@ -233,7 +206,18 @@ func extractContextAndFiles(args, files []string) (string, []string, error) { } } - return contextDir, containerFiles, nil + ie, err := registry.NewImageEngine(cmd, args) + if err != nil { + return err + } + + apiBuildOpts, err := buildFlagsWrapperToOptions(cmd, contextDir, &buildOpts) + if err != nil { + return err + } + + _, err = ie.Build(registry.GetContext(), containerFiles, *apiBuildOpts) + return err } // buildFlagsWrapperToOptions converts the local build flags to the build options used diff --git a/cmd/podman/images/mount.go b/cmd/podman/images/mount.go new file mode 100644 index 000000000..fac06e324 --- /dev/null +++ b/cmd/podman/images/mount.go @@ -0,0 +1,139 @@ +package images + +import ( + "fmt" + "os" + "text/tabwriter" + "text/template" + + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/cmd/podman/utils" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + mountDescription = `podman image mount + Lists all mounted images mount points if no images is specified + + podman image mount IMAGE-NAME-OR-ID + Mounts the specified image and prints the mountpoint +` + + mountCommand = &cobra.Command{ + Use: "mount [flags] [IMAGE...]", + Short: "Mount an images's root filesystem", + Long: mountDescription, + RunE: mount, + Example: `podman image mount imgID + podman image mount imgID1 imgID2 imgID3 + podman image mount + podman image mount --all`, + Annotations: map[string]string{ + registry.UnshareNSRequired: "", + registry.ParentNSRequired: "", + }, + } +) + +var ( + mountOpts entities.ImageMountOptions +) + +func mountFlags(flags *pflag.FlagSet) { + flags.BoolVarP(&mountOpts.All, "all", "a", false, "Mount all images") + flags.StringVar(&mountOpts.Format, "format", "", "Print the mounted images in specified format (json)") +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: mountCommand, + Parent: imageCmd, + }) + mountFlags(mountCommand.Flags()) +} + +func mount(_ *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if len(args) > 0 && mountOpts.All { + return errors.New("when using the --all switch, you may not pass any image names or IDs") + } + reports, err := registry.ImageEngine().Mount(registry.GetContext(), args, mountOpts) + if err != nil { + return err + } + if len(args) > 0 || mountOpts.All { + for _, r := range reports { + if r.Err == nil { + fmt.Println(r.Path) + continue + } + errs = append(errs, r.Err) + } + return errs.PrintErrors() + } + + switch mountOpts.Format { + case "json": + return printJSON(reports) + case "": + // do nothing + default: + return errors.Errorf("unknown --format argument: %s", mountOpts.Format) + } + + mrs := make([]mountReporter, 0, len(reports)) + for _, r := range reports { + mrs = append(mrs, mountReporter{r}) + } + row := "{{.ID}} {{.Path}}\n" + format := "{{range . }}" + row + "{{end}}" + tmpl, err := template.New("mounts").Parse(format) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + defer w.Flush() + return tmpl.Execute(w, mrs) +} + +func printJSON(reports []*entities.ImageMountReport) error { + type jreport struct { + ID string `json:"id"` + Names []string + Repositories []string + Mountpoint string `json:"mountpoint"` + } + jreports := make([]jreport, 0, len(reports)) + + for _, r := range reports { + jreports = append(jreports, jreport{ + ID: r.Id, + Names: []string{r.Name}, + Repositories: r.Repositories, + Mountpoint: r.Path, + }) + } + b, err := json.MarshalIndent(jreports, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil +} + +type mountReporter struct { + *entities.ImageMountReport +} + +func (m mountReporter) ID() string { + if len(m.Repositories) > 0 { + return m.Repositories[0] + } + return m.Id +} diff --git a/cmd/podman/images/unmount.go b/cmd/podman/images/unmount.go new file mode 100644 index 000000000..f7f6cf8e5 --- /dev/null +++ b/cmd/podman/images/unmount.go @@ -0,0 +1,71 @@ +package images + +import ( + "fmt" + + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/cmd/podman/utils" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + description = `Image storage increments a mount counter each time an image is mounted. + + When an image is unmounted, the mount counter is decremented. The image's root filesystem is physically unmounted only when the mount counter reaches zero indicating no other processes are using the mount. + + An unmount can be forced with the --force flag. +` + unmountCommand = &cobra.Command{ + Use: "unmount [flags] IMAGE [IMAGE...]", + Aliases: []string{"umount"}, + Short: "Unmount an image's root filesystem", + Long: description, + RunE: unmount, + Example: `podman unmount imgID + podman unmount imgID1 imgID2 imgID3 + podman unmount --all`, + } +) + +var ( + unmountOpts entities.ImageUnmountOptions +) + +func unmountFlags(flags *pflag.FlagSet) { + flags.BoolVarP(&unmountOpts.All, "all", "a", false, "Unmount all of the currently mounted images") + flags.BoolVarP(&unmountOpts.Force, "force", "f", false, "Force the complete unmount of the specified mounted images") +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Parent: imageCmd, + Command: unmountCommand, + }) + unmountFlags(unmountCommand.Flags()) +} + +func unmount(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + if len(args) < 1 && !unmountOpts.All { + return errors.New("image name or ID must be specified") + } + if len(args) > 0 && unmountOpts.All { + return errors.New("when using the --all switch, you may not pass any image names or IDs") + } + reports, err := registry.ImageEngine().Unmount(registry.GetContext(), args, unmountOpts) + if err != nil { + return err + } + for _, r := range reports { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/completions/bash/podman b/completions/bash/podman index eb727ef63..f0c8e7394 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1524,6 +1524,56 @@ _podman_info() { esac } +_podman_image_umount() { + _podman_image_unmount +} + +_podman_image_unmount() { + local boolean_options=" + --all + -a + --help + -h + --force + -f + " + local options_with_args=" + " + + local all_options="$options_with_args $boolean_options" + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __podman_complete_images --force-tag --id + ;; + esac +} + +_podman_image_mount() { + local boolean_options=" + --all + -a + --help + -h + " + + local options_with_args=" + --format + " + + local all_options="$options_with_args $boolean_options" + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __podman_complete_images --force-tag --id + ;; + esac +} + _podman_image_build() { _podman_build } @@ -1590,6 +1640,7 @@ _podman_image() { inspect load ls + mount prune pull push @@ -1598,6 +1649,8 @@ _podman_image() { sign tag trust + umount + unmount untag " local aliases=" diff --git a/contrib/cirrus/logformatter b/contrib/cirrus/logformatter index b56a829c5..f97638b6f 100755 --- a/contrib/cirrus/logformatter +++ b/contrib/cirrus/logformatter @@ -208,13 +208,13 @@ END_HTML } # Try to identify the git commit we're working with... - if ($line =~ m!libpod/define.gitCommit=([0-9a-f]+)!) { + if ($line =~ m!/define.gitCommit=([0-9a-f]+)!) { $git_commit = $1; } # ...so we can link to specific lines in source files if ($git_commit) { - # 1 12 3 34 4 5 526 6 - $line =~ s{^(.*)(\/(containers\/libpod)(\/\S+):(\d+))(.*)$} + # 1 12 3 34 4 5 526 6 + $line =~ s{^(.*)(\/(containers\/[^/]+)(\/\S+):(\d+))(.*)$} {$1<a class="codelink" href='https://github.com/$3/blob/$git_commit$4#L$5'>$2</a>$6}; } diff --git a/docs/source/markdown/links/podman-image-umount.1 b/docs/source/markdown/links/podman-image-umount.1 new file mode 100644 index 000000000..129212aab --- /dev/null +++ b/docs/source/markdown/links/podman-image-umount.1 @@ -0,0 +1 @@ +.so man1/podman-image-unmount.1 diff --git a/docs/source/markdown/podman-image-mount.1.md b/docs/source/markdown/podman-image-mount.1.md new file mode 100644 index 000000000..f98b46571 --- /dev/null +++ b/docs/source/markdown/podman-image-mount.1.md @@ -0,0 +1,76 @@ +% podman-image-mount(1) + +## NAME +podman\-image\-mount - Mount an image's root filesystem + +## SYNOPSIS +**podman image mount** [*options*] [*image* ...] + +## DESCRIPTION +Mounts the specified images' root file system in a location which can be +accessed from the host, and returns its location. + +If you execute the command without any arguments, Podman will list all of the +currently mounted images. + +Rootless mode only supports mounting VFS driver, unless you enter the user namespace +via the `podman unshare` command. All other storage drivers will fail to mount. + +## RETURN VALUE +The location of the mounted file system. On error an empty string and errno is +returned. + +## OPTIONS + +**--all**, **-a** + +Mount all images. + +**--format**=*format* + +Print the mounted images in specified format (json). + +## EXAMPLE + +``` +podman image mount fedora ubi8-init + +/var/lib/containers/storage/overlay/f3ac502d97b5681989dff84dfedc8354239bcecbdc2692f9a639f4e080a02364/merged +/var/lib/containers/storage/overlay/0ff7d7ca68bed1ace424f9df154d2dd7b5a125c19d887f17653cbcd5b6e30ba1/merged +``` + +``` +podman mount + +registry.fedoraproject.org/fedora:latest /var/lib/containers/storage/overlay/f3ac502d97b5681989dff84dfedc8354239bcecbdc2692f9a639f4e080a02364/merged +registry.access.redhat.com/ubi8-init:latest /var/lib/containers/storage/overlay/0ff7d7ca68bed1ace424f9df154d2dd7b5a125c19d887f17653cbcd5b6e30ba1/merged +``` + +``` +podman image mount --format json +[ + { + "id": "00ff39a8bf19f810a7e641f7eb3ddc47635913a19c4996debd91fafb6b379069", + "Names": [ + "sha256:58de585a231aca14a511347bc85b912a6f000159b49bc2b0582032911e5d3a6c" + ], + "Repositories": [ + "registry.fedoraproject.org/fedora:latest" + ], + "mountpoint": "/var/lib/containers/storage/overlay/0ccfac04663bbe8813b5f24502ee0b7371ce5bf3c5adeb12e4258d191c2cf7bc/merged" + }, + { + "id": "bcc2dc9a261774ad25a15e07bb515f9b77424266abf2a1252ec7bcfed1dd0ac2", + "Names": [ + "sha256:d5f260b2e51b3ee9d05de1c31d261efc9af28e7d2d47cedf054c496d71424d63" + ], + "Repositories": [ + "registry.access.redhat.com/ubi8-init:latest" + ], + "mountpoint": "/var/lib/containers/storage/overlay/d66b58e3391ea8ce4c81316c72e22b332618f2a28b461a32ed673e8998cdaeb8/merged" + } +] +``` + +## SEE ALSO +podman(1), podman-image-umount(1), mount(8), podman-unshare(1) diff --git a/docs/source/markdown/podman-image-unmount.1.md b/docs/source/markdown/podman-image-unmount.1.md new file mode 100644 index 000000000..c026c49ac --- /dev/null +++ b/docs/source/markdown/podman-image-unmount.1.md @@ -0,0 +1,43 @@ +% podman-image-unmount(1) + +## NAME +podman\-image\-unmount - Unmount an image's root filesystem + +## SYNOPSIS +**podman image unmount** [*options*] *image* [...] + +**podman image umount** [*options*] *image* [...] + +## DESCRIPTION +Unmounts the specified images' root file system, if no other processes +are using it. + +Image storage increments a mount counter each time a image is mounted. +When a image is unmounted, the mount counter is decremented, and the +image's root filesystem is physically unmounted only when the mount +counter reaches zero indicating no other processes are using the mount. +An unmount can be forced with the --force flag. + +## OPTIONS +**--all**, **-a** + +All of the currently mounted images will be unmounted. + +**--force**, **-f** + +Force the unmounting of specified images' root file system, even if other +processes have mounted it. + +Note: This could cause other processes that are using the file system to fail, +as the mount point could be removed without their knowledge. + +## EXAMPLE + +podman image unmount imageID + +podman image unmount imageID1 imageID2 imageID3 + +podman image unmount --all + +## SEE ALSO +podman(1), podman-image-mount(1), podman-container-mount(1) diff --git a/docs/source/markdown/podman-image.1.md b/docs/source/markdown/podman-image.1.md index dfff57b31..55e95d032 100644 --- a/docs/source/markdown/podman-image.1.md +++ b/docs/source/markdown/podman-image.1.md @@ -17,21 +17,23 @@ The image command allows you to manage images | diff | [podman-image-diff(1)](podman-image-diff.1.md) | Inspect changes on an image's filesystem. | | exists | [podman-image-exists(1)](podman-image-exists.1.md) | Check if an image exists in local storage. | | history | [podman-history(1)](podman-history.1.md) | Show the history of an image. | -| import | [podman-import(1)](podman-import.1.md) | Import a tarball and save it as a filesystem image. | -| inspect | [podman-inspect(1)](podman-inspect.1.md) | Display a image or image's configuration. | -| list | [podman-images(1)](podman-images.1.md) | List the container images on the system.(alias ls) | -| load | [podman-load(1)](podman-load.1.md) | Load an image from the docker archive. | -| prune | [podman-image-prune(1)](podman-image-prune.1.md)| Remove all unused images from the local store. | -| pull | [podman-pull(1)](podman-pull.1.md) | Pull an image from a registry. | -| push | [podman-push(1)](podman-push.1.md) | Push an image from local storage to elsewhere. | -| rm | [podman-rmi(1)](podman-rmi.1.md) | Removes one or more locally stored images. | -| save | [podman-save(1)](podman-save.1.md) | Save an image to docker-archive or oci. | -| search | [podman-search(1)](podman-search.1.md) | Search a registry for an image. | -| sign | [podman-image-sign(1)](podman-image-sign.1.md) | Create a signature for an image. | -| tag | [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. | -| untag | [podman-untag(1)](podman-untag.1.md) | Removes one or more names from a locally-stored image. | -| tree | [podman-image-tree(1)](podman-image-tree.1.md) | Prints layer hierarchy of an image in a tree format. | -| trust | [podman-image-trust(1)](podman-image-trust.1.md)| Manage container registry image trust policy. | +| import | [podman-import(1)](podman-import.1.md) | Import a tarball and save it as a filesystem image. | +| inspect | [podman-inspect(1)](podman-inspect.1.md) | Display a image or image's configuration. | +| list | [podman-images(1)](podman-images.1.md) | List the container images on the system.(alias ls) | +| mount | [podman-image-mount(1)](podman-image-mount.1.md) | Mount an image's root filesystem. | +| load | [podman-load(1)](podman-load.1.md) | Load an image from the docker archive. | +| prune | [podman-image-prune(1)](podman-image-prune.1.md) | Remove all unused images from the local store. | +| pull | [podman-pull(1)](podman-pull.1.md) | Pull an image from a registry. | +| push | [podman-push(1)](podman-push.1.md) | Push an image from local storage to elsewhere. | +| rm | [podman-rmi(1)](podman-rmi.1.md) | Removes one or more locally stored images. | +| save | [podman-save(1)](podman-save.1.md) | Save an image to docker-archive or oci. | +| search | [podman-search(1)](podman-search.1.md) | Search a registry for an image. | +| sign | [podman-image-sign(1)](podman-image-sign.1.md) | Create a signature for an image. | +| tag | [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. | +| untag | [podman-untag(1)](podman-untag.1.md) | Removes one or more names from a locally-stored image. | +| unmount | [podman-image-unmount(1)](podman-image-unmount.1.md) | Unmount an image's root filesystem. | +| tree | [podman-image-tree(1)](podman-image-tree.1.md) | Prints layer hierarchy of an image in a tree format. | +| trust | [podman-image-trust(1)](podman-image-trust.1.md) | Manage container registry image trust policy. | ## SEE ALSO podman diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index b959b947f..4fdb7f81b 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -1411,9 +1411,9 @@ required for VPN, without it containers need to be run with the **--network=host Environment variables within containers can be set using multiple different options, in the following order of precedence (later entries override earlier entries): -- **--env-host**: Host environment of the process executing Podman is added. -- **--http-proxy**: By default, several environment variables will be passed in from the host, such as **http_proxy** and **no_proxy**. See **--http-proxy** for details. - Container image: Any environment variables specified in the container image. +- **--http-proxy**: By default, several environment variables will be passed in from the host, such as **http_proxy** and **no_proxy**. See **--http-proxy** for details. +- **--env-host**: Host environment of the process executing Podman is added. - **--env-file**: Any environment variables specified via env-files. If multiple files specified, then they override each other in order of entry. - **--env**: Any environment variables specified will override previous settings. diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 795611596..4cfe992ea 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -1480,11 +1480,26 @@ func (c *Container) generateCurrentUserPasswdEntry() (string, error) { if uid == 0 { return "", nil } + u, err := user.LookupId(strconv.Itoa(rootless.GetRootlessUID())) if err != nil { return "", errors.Wrapf(err, "failed to get current user") } - return fmt.Sprintf("%s:x:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Username, c.WorkingDir()), nil + + // Lookup the user to see if it exists in the container image. + _, err = lookup.GetUser(c.state.Mountpoint, u.Username) + if err != User.ErrNoPasswdEntries { + return "", err + } + + // If the user's actual home directory exists, or was mounted in - use + // that. + homeDir := c.WorkingDir() + if MountExists(c.config.Spec.Mounts, u.HomeDir) { + homeDir = u.HomeDir + } + + return fmt.Sprintf("%s:x:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Username, homeDir), nil } // generateUserPasswdEntry generates an /etc/passwd entry for the container user @@ -1510,12 +1525,9 @@ func (c *Container) generateUserPasswdEntry() (string, error) { // Lookup the user to see if it exists in the container image _, err = lookup.GetUser(c.state.Mountpoint, userspec) - if err != nil && err != User.ErrNoPasswdEntries { + if err != User.ErrNoPasswdEntries { return "", err } - if err == nil { - return "", nil - } if groupspec != "" { ugid, err := strconv.ParseUint(groupspec, 10, 32) @@ -1564,6 +1576,32 @@ func (c *Container) generatePasswd() (string, error) { if pwd == "" { return "", nil } + + // If we are *not* read-only - edit /etc/passwd in the container. + // This is *gross* (shows up in changes to the container, will be + // committed to images based on the container) but it actually allows us + // to add users to the container (a bind mount breaks useradd). + // We should never get here twice, because generateUserPasswdEntry will + // not return anything if the user already exists in /etc/passwd. + if !c.IsReadOnly() { + containerPasswd, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") + if err != nil { + return "", errors.Wrapf(err, "error looking up location of container %s /etc/passwd", c.ID()) + } + + f, err := os.OpenFile(containerPasswd, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return "", errors.Wrapf(err, "error opening container %s /etc/passwd", c.ID()) + } + defer f.Close() + + if _, err := f.WriteString(pwd); err != nil { + return "", errors.Wrapf(err, "unable to append to container %s /etc/passwd", c.ID()) + } + + return "", nil + } + originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd") orig, err := ioutil.ReadFile(originPasswdFile) if err != nil && !os.IsNotExist(err) { diff --git a/libpod/image/image.go b/libpod/image/image.go index 14ffad4bf..8b2aa318f 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -1593,6 +1593,63 @@ func (i *Image) newImageEvent(status events.Status) { } } +// Mount mounts a image's filesystem on the host +// The path where the image has been mounted is returned +func (i *Image) Mount(options []string, mountLabel string) (string, error) { + defer i.newImageEvent(events.Mount) + return i.mount(options, mountLabel) +} + +// Unmount unmounts a image's filesystem on the host +func (i *Image) Unmount(force bool) error { + defer i.newImageEvent(events.Unmount) + return i.unmount(force) +} + +// Mounted returns whether the image is mounted and the path it is mounted +// at (if it is mounted). +// If the image is not mounted, no error is returned, and the mountpoint +// will be set to "". +func (i *Image) Mounted() (bool, string, error) { + mountedTimes, err := i.imageruntime.store.Mounted(i.TopLayer()) + if err != nil { + return false, "", err + } + + if mountedTimes > 0 { + layer, err := i.imageruntime.store.Layer(i.TopLayer()) + if err != nil { + return false, "", err + } + return true, layer.MountPoint, nil + } + + return false, "", nil +} + +// mount mounts the container's root filesystem +func (i *Image) mount(options []string, mountLabel string) (string, error) { + mountPoint, err := i.imageruntime.store.MountImage(i.ID(), options, mountLabel) + if err != nil { + return "", errors.Wrapf(err, "error mounting storage for image %s", i.ID()) + } + mountPoint, err = filepath.EvalSymlinks(mountPoint) + if err != nil { + return "", errors.Wrapf(err, "error resolving storage path for image %s", i.ID()) + } + return mountPoint, nil +} + +// unmount unmounts the image's root filesystem +func (i *Image) unmount(force bool) error { + // Also unmount storage + if _, err := i.imageruntime.store.UnmountImage(i.ID(), force); err != nil { + return errors.Wrapf(err, "error unmounting image %s root filesystem", i.ID()) + } + + return nil +} + // LayerInfo keeps information of single layer type LayerInfo struct { // Layer ID diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index c1a01c280..9a188e5da 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -280,6 +280,7 @@ var _ = Describe("Podman containers ", func() { }) It("podman wait to pause|unpause condition", func() { + Skip("FIXME: https://github.com/containers/podman/issues/6518") var ( name = "top" exitCode int32 = -1 diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 7ece24c60..594f9617f 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -16,6 +16,7 @@ type ImageEngine interface { Inspect(ctx context.Context, namesOrIDs []string, opts InspectOptions) ([]*ImageInspectReport, []error, error) List(ctx context.Context, opts ImageListOptions) ([]*ImageSummary, error) Load(ctx context.Context, opts ImageLoadOptions) (*ImageLoadReport, error) + Mount(ctx context.Context, images []string, options ImageMountOptions) ([]*ImageMountReport, error) Prune(ctx context.Context, opts ImagePruneOptions) (*ImagePruneReport, error) Pull(ctx context.Context, rawImage string, opts ImagePullOptions) (*ImagePullReport, error) Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error @@ -27,6 +28,7 @@ type ImageEngine interface { Shutdown(ctx context.Context) Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) 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 ManifestCreate(ctx context.Context, names, images []string, opts ManifestCreateOptions) (string, error) ManifestInspect(ctx context.Context, name string) ([]byte, error) diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index b38facfb5..cb970b09a 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -91,7 +91,7 @@ type ImageRemoveOptions struct { } // ImageRemoveResponse is the response for removing one or more image(s) from storage -// and containers what was untagged vs actually removed. +// and images what was untagged vs actually removed. type ImageRemoveReport struct { // Deleted images. Deleted []string `json:",omitempty"` @@ -318,3 +318,31 @@ type SignOptions struct { // SignReport describes the result of signing type SignReport struct{} + +// ImageMountOptions describes the input values for mounting images +// in the CLI +type ImageMountOptions struct { + All bool + Format string +} + +// ImageUnmountOptions are the options from the cli for unmounting +type ImageUnmountOptions struct { + All bool + Force bool +} + +// ImageMountReport describes the response from image mount +type ImageMountReport struct { + Err error + Id string //nolint + Name string + Repositories []string + Path string +} + +// ImageUnmountReport describes the response from umounting an image +type ImageUnmountReport struct { + Err error + Id string //nolint +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index e2fe8a5e6..05adc40fe 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -11,8 +11,6 @@ import ( "strconv" "strings" - "github.com/containers/podman/v2/pkg/rootless" - "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker" dockerarchive "github.com/containers/image/v5/docker/archive" @@ -27,6 +25,7 @@ import ( libpodImage "github.com/containers/podman/v2/libpod/image" "github.com/containers/podman/v2/pkg/domain/entities" domainUtils "github.com/containers/podman/v2/pkg/domain/utils" + "github.com/containers/podman/v2/pkg/rootless" "github.com/containers/podman/v2/pkg/trust" "github.com/containers/podman/v2/pkg/util" "github.com/containers/storage" @@ -85,6 +84,125 @@ func (ir *ImageEngine) History(ctx context.Context, nameOrID string, opts entiti return &history, nil } +func (ir *ImageEngine) Mount(ctx context.Context, nameOrIDs []string, opts entities.ImageMountOptions) ([]*entities.ImageMountReport, error) { + var ( + images []*image.Image + err error + ) + if os.Geteuid() != 0 { + if driver := ir.Libpod.StorageConfig().GraphDriverName; driver != "vfs" { + // Do not allow to mount a graphdriver that is not vfs if we are creating the userns as part + // of the mount command. + return nil, errors.Errorf("cannot mount using driver %s in rootless mode", driver) + } + + became, ret, err := rootless.BecomeRootInUserNS("") + if err != nil { + return nil, err + } + if became { + os.Exit(ret) + } + } + if opts.All { + allImages, err := ir.Libpod.ImageRuntime().GetImages() + if err != nil { + return nil, err + } + for _, img := range allImages { + if !img.IsReadOnly() { + images = append(images, img) + } + } + } else { + for _, i := range nameOrIDs { + img, err := ir.Libpod.ImageRuntime().NewFromLocal(i) + if err != nil { + return nil, err + } + images = append(images, img) + } + } + reports := make([]*entities.ImageMountReport, 0, len(images)) + for _, img := range images { + report := entities.ImageMountReport{Id: img.ID()} + if img.IsReadOnly() { + report.Err = errors.Errorf("mounting readonly %s image not supported", img.ID()) + } else { + report.Path, report.Err = img.Mount([]string{}, "") + } + reports = append(reports, &report) + } + if len(reports) > 0 { + return reports, nil + } + + images, err = ir.Libpod.ImageRuntime().GetImages() + if err != nil { + return nil, err + } + for _, i := range images { + mounted, path, err := i.Mounted() + if err != nil { + if errors.Cause(err) == storage.ErrLayerUnknown { + continue + } + return nil, err + } + if mounted { + tags, err := i.RepoTags() + if err != nil { + return nil, err + } + reports = append(reports, &entities.ImageMountReport{ + Id: i.ID(), + Name: string(i.Digest()), + Repositories: tags, + Path: path, + }) + } + } + return reports, nil +} + +func (ir *ImageEngine) Unmount(ctx context.Context, nameOrIDs []string, options entities.ImageUnmountOptions) ([]*entities.ImageUnmountReport, error) { + var images []*image.Image + + if options.All { + allImages, err := ir.Libpod.ImageRuntime().GetImages() + if err != nil { + return nil, err + } + for _, img := range allImages { + if !img.IsReadOnly() { + images = append(images, img) + } + } + } else { + for _, i := range nameOrIDs { + img, err := ir.Libpod.ImageRuntime().NewFromLocal(i) + if err != nil { + return nil, err + } + images = append(images, img) + } + } + + reports := []*entities.ImageUnmountReport{} + for _, img := range images { + report := entities.ImageUnmountReport{Id: img.ID()} + if err := img.Unmount(options.Force); err != nil { + if options.All && errors.Cause(err) == storage.ErrLayerNotMounted { + logrus.Debugf("Error umounting image %s, storage.ErrLayerNotMounted", img.ID()) + continue + } + report.Err = errors.Wrapf(err, "error unmounting image %s", img.ID()) + } + reports = append(reports, &report) + } + return reports, nil +} + func ToDomainHistoryLayer(layer *libpodImage.History) entities.ImageHistoryLayer { l := entities.ImageHistoryLayer{} l.ID = layer.ID @@ -225,7 +343,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri case "v2s2", "docker": manifestType = manifest.DockerV2Schema2MediaType default: - return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", options.Format) + return errors.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", options.Format) } var registryCreds *types.DockerAuthConfig @@ -292,7 +410,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri // // // TODO: Determine Size // report := entities.ImagePruneReport{} -// copy(report.Report.Id, id) +// copy(report.Report.ID, id) // return &report, nil // } @@ -402,7 +520,7 @@ func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.Im for i := range searchResults { reports[i].Index = searchResults[i].Index reports[i].Name = searchResults[i].Name - reports[i].Description = searchResults[i].Index + reports[i].Description = searchResults[i].Description reports[i].Stars = searchResults[i].Stars reports[i].Official = searchResults[i].Official reports[i].Automated = searchResults[i].Automated diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 6407724b5..6845d01c0 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -54,6 +54,14 @@ func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) return is, nil } +func (ir *ImageEngine) Mount(ctx context.Context, images []string, options entities.ImageMountOptions) ([]*entities.ImageMountReport, error) { + return nil, errors.New("mounting images is not supported for remote clients") +} + +func (ir *ImageEngine) Unmount(ctx context.Context, images []string, options entities.ImageUnmountOptions) ([]*entities.ImageUnmountReport, error) { + return nil, errors.New("unmounting images is not supported for remote clients") +} + func (ir *ImageEngine) History(ctx context.Context, nameOrID string, opts entities.ImageHistoryOptions) (*entities.ImageHistoryReport, error) { results, err := images.History(ir.ClientCxt, nameOrID) if err != nil { diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index 8e0151496..d3e43e44d 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -211,7 +211,8 @@ can_use_shortcut () break; } - if (argv[argc+1] != NULL && strcmp (argv[argc], "container") == 0 && + if (argv[argc+1] != NULL && (strcmp (argv[argc], "container") == 0 || + strcmp (argv[argc], "image") == 0) && strcmp (argv[argc+1], "mount") == 0) { ret = false; diff --git a/test/e2e/mount_rootless_test.go b/test/e2e/mount_rootless_test.go index ec7a573cb..312258532 100644 --- a/test/e2e/mount_rootless_test.go +++ b/test/e2e/mount_rootless_test.go @@ -59,4 +59,25 @@ var _ = Describe("Podman mount", func() { session.WaitWithDefaultTimeout() Expect(setup.ExitCode()).To(Equal(0)) }) + + It("podman image mount", func() { + setup := podmanTest.PodmanNoCache([]string{"pull", ALPINE}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + mount := podmanTest.PodmanNoCache([]string{"image", "mount", ALPINE}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).ToNot(Equal(0)) + Expect(mount.ErrorToString()).To(ContainSubstring("podman unshare")) + }) + + It("podman unshare image podman mount", func() { + setup := podmanTest.PodmanNoCache([]string{"pull", ALPINE}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + session := podmanTest.Podman([]string{"unshare", PODMAN_BINARY, "image", "mount", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + }) }) diff --git a/test/e2e/mount_test.go b/test/e2e/mount_test.go index 0749a34f2..a2b448337 100644 --- a/test/e2e/mount_test.go +++ b/test/e2e/mount_test.go @@ -282,4 +282,143 @@ var _ = Describe("Podman mount", func() { umount.WaitWithDefaultTimeout() Expect(umount.ExitCode()).To(Equal(0)) }) + + It("podman image mount", func() { + setup := podmanTest.PodmanNoCache([]string{"pull", ALPINE}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + images := podmanTest.PodmanNoCache([]string{"images"}) + images.WaitWithDefaultTimeout() + Expect(images.ExitCode()).To(Equal(0)) + + mount := podmanTest.PodmanNoCache([]string{"image", "mount", ALPINE}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + + umount := podmanTest.PodmanNoCache([]string{"image", "umount", ALPINE}) + umount.WaitWithDefaultTimeout() + Expect(umount.ExitCode()).To(Equal(0)) + + mount = podmanTest.PodmanNoCache([]string{"image", "mount"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + Expect(mount.OutputToString()).To(Equal("")) + + // Mount multiple times + mount = podmanTest.PodmanNoCache([]string{"image", "mount", ALPINE}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + + mount = podmanTest.PodmanNoCache([]string{"image", "mount", ALPINE}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + + // Unmount once + mount = podmanTest.PodmanNoCache([]string{"image", "mount", ALPINE}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + + mount = podmanTest.PodmanNoCache([]string{"image", "mount"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + Expect(mount.OutputToString()).To(ContainSubstring(ALPINE)) + + mount = podmanTest.PodmanNoCache([]string{"image", "umount", "--all"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + }) + + It("podman mount with json format", func() { + setup := podmanTest.PodmanNoCache([]string{"pull", fedoraMinimal}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + mount := podmanTest.PodmanNoCache([]string{"image", "mount", fedoraMinimal}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + + j := podmanTest.PodmanNoCache([]string{"image", "mount", "--format=json"}) + j.WaitWithDefaultTimeout() + Expect(j.ExitCode()).To(Equal(0)) + Expect(j.IsJSONOutputValid()).To(BeTrue()) + + umount := podmanTest.PodmanNoCache([]string{"image", "umount", fedoraMinimal}) + umount.WaitWithDefaultTimeout() + Expect(umount.ExitCode()).To(Equal(0)) + }) + + It("podman mount many", func() { + setup := podmanTest.PodmanNoCache([]string{"pull", fedoraMinimal}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + setup = podmanTest.PodmanNoCache([]string{"pull", ALPINE}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + setup = podmanTest.PodmanNoCache([]string{"pull", "busybox"}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + mount1 := podmanTest.PodmanNoCache([]string{"image", "mount", fedoraMinimal, ALPINE, "busybox"}) + mount1.WaitWithDefaultTimeout() + Expect(mount1.ExitCode()).To(Equal(0)) + + umount := podmanTest.PodmanNoCache([]string{"image", "umount", fedoraMinimal, ALPINE}) + umount.WaitWithDefaultTimeout() + Expect(umount.ExitCode()).To(Equal(0)) + + mount := podmanTest.PodmanNoCache([]string{"image", "mount"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + Expect(mount.OutputToString()).To(ContainSubstring("busybox")) + + mount1 = podmanTest.PodmanNoCache([]string{"image", "unmount", "busybox"}) + mount1.WaitWithDefaultTimeout() + Expect(mount1.ExitCode()).To(Equal(0)) + + mount = podmanTest.PodmanNoCache([]string{"image", "mount"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + Expect(mount.OutputToString()).To(Equal("")) + + mount1 = podmanTest.PodmanNoCache([]string{"image", "mount", fedoraMinimal, ALPINE, "busybox"}) + mount1.WaitWithDefaultTimeout() + Expect(mount1.ExitCode()).To(Equal(0)) + + mount = podmanTest.PodmanNoCache([]string{"image", "mount"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + Expect(mount.OutputToString()).To(ContainSubstring(fedoraMinimal)) + Expect(mount.OutputToString()).To(ContainSubstring(ALPINE)) + + umount = podmanTest.PodmanNoCache([]string{"image", "umount", "--all"}) + umount.WaitWithDefaultTimeout() + Expect(umount.ExitCode()).To(Equal(0)) + + mount = podmanTest.PodmanNoCache([]string{"image", "mount"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + Expect(mount.OutputToString()).To(Equal("")) + + mount1 = podmanTest.PodmanNoCache([]string{"image", "mount", "--all"}) + mount1.WaitWithDefaultTimeout() + Expect(mount1.ExitCode()).To(Equal(0)) + + mount = podmanTest.PodmanNoCache([]string{"image", "mount"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + Expect(mount.OutputToString()).To(ContainSubstring(fedoraMinimal)) + Expect(mount.OutputToString()).To(ContainSubstring(ALPINE)) + + umount = podmanTest.PodmanNoCache([]string{"image", "umount", "--all"}) + umount.WaitWithDefaultTimeout() + Expect(umount.ExitCode()).To(Equal(0)) + + mount = podmanTest.PodmanNoCache([]string{"image", "mount"}) + mount.WaitWithDefaultTimeout() + Expect(mount.ExitCode()).To(Equal(0)) + Expect(mount.OutputToString()).To(Equal("")) + }) }) diff --git a/test/e2e/run_ns_test.go b/test/e2e/run_ns_test.go index 2b6da2888..5242e04d2 100644 --- a/test/e2e/run_ns_test.go +++ b/test/e2e/run_ns_test.go @@ -2,6 +2,7 @@ package integration import ( "os" + "os/exec" "strings" . "github.com/containers/podman/v2/test/utils" @@ -102,4 +103,34 @@ var _ = Describe("Podman run ns", func() { session.WaitWithDefaultTimeout() Expect(session).To(ExitWithError()) }) + + It("podman run --ipc=host --pid=host", func() { + cmd := exec.Command("ls", "-l", "/proc/self/ns/pid") + res, err := cmd.Output() + Expect(err).To(BeNil()) + fields := strings.Split(string(res), " ") + hostPidNS := strings.TrimSuffix(fields[len(fields)-1], "\n") + + cmd = exec.Command("ls", "-l", "/proc/self/ns/ipc") + res, err = cmd.Output() + Expect(err).To(BeNil()) + fields = strings.Split(string(res), " ") + hostIpcNS := strings.TrimSuffix(fields[len(fields)-1], "\n") + + session := podmanTest.Podman([]string{"run", "--ipc=host", "--pid=host", ALPINE, "ls", "-l", "/proc/self/ns/pid"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + fields = strings.Split(session.OutputToString(), " ") + ctrPidNS := strings.TrimSuffix(fields[len(fields)-1], "\n") + + session = podmanTest.Podman([]string{"run", "--ipc=host", "--pid=host", ALPINE, "ls", "-l", "/proc/self/ns/ipc"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + fields = strings.Split(session.OutputToString(), " ") + ctrIpcNS := strings.TrimSuffix(fields[len(fields)-1], "\n") + + Expect(hostPidNS).To(Equal(ctrPidNS)) + Expect(hostIpcNS).To(Equal(ctrIpcNS)) + }) + }) diff --git a/test/e2e/run_passwd_test.go b/test/e2e/run_passwd_test.go index a1414e313..8dea7d39b 100644 --- a/test/e2e/run_passwd_test.go +++ b/test/e2e/run_passwd_test.go @@ -33,27 +33,27 @@ var _ = Describe("Podman run passwd", func() { }) It("podman run no user specified ", func() { - session := podmanTest.Podman([]string{"run", BB, "mount"}) + session := podmanTest.Podman([]string{"run", "--read-only", BB, "mount"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.LineInOutputContains("passwd")).To(BeFalse()) }) It("podman run user specified in container", func() { - session := podmanTest.Podman([]string{"run", "-u", "bin", BB, "mount"}) + session := podmanTest.Podman([]string{"run", "--read-only", "-u", "bin", BB, "mount"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.LineInOutputContains("passwd")).To(BeFalse()) }) It("podman run UID specified in container", func() { - session := podmanTest.Podman([]string{"run", "-u", "2:1", BB, "mount"}) + session := podmanTest.Podman([]string{"run", "--read-only", "-u", "2:1", BB, "mount"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.LineInOutputContains("passwd")).To(BeFalse()) }) It("podman run UID not specified in container", func() { - session := podmanTest.Podman([]string{"run", "-u", "20001:1", BB, "mount"}) + session := podmanTest.Podman([]string{"run", "--read-only", "-u", "20001:1", BB, "mount"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.LineInOutputContains("passwd")).To(BeTrue()) diff --git a/test/e2e/run_userns_test.go b/test/e2e/run_userns_test.go index 198217433..25f8d0d15 100644 --- a/test/e2e/run_userns_test.go +++ b/test/e2e/run_userns_test.go @@ -111,6 +111,31 @@ var _ = Describe("Podman UserNS support", func() { Expect(session.OutputToString()).To(Equal("0")) }) + It("podman run --userns=keep-id can add users", func() { + if os.Geteuid() == 0 { + Skip("Test only runs without root") + } + + userName := os.Getenv("USER") + if userName == "" { + Skip("Can't complete test if no username available") + } + + ctrName := "ctr-name" + session := podmanTest.Podman([]string{"run", "--userns=keep-id", "--user", "root:root", "-d", "--stop-signal", "9", "--name", ctrName, fedoraMinimal, "sleep", "600"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + exec1 := podmanTest.Podman([]string{"exec", "-t", "-i", ctrName, "cat", "/etc/passwd"}) + exec1.WaitWithDefaultTimeout() + Expect(exec1.ExitCode()).To(Equal(0)) + Expect(exec1.OutputToString()).To(ContainSubstring(userName)) + + exec2 := podmanTest.Podman([]string{"exec", "-t", "-i", ctrName, "useradd", "testuser"}) + exec2.WaitWithDefaultTimeout() + Expect(exec2.ExitCode()).To(Equal(0)) + }) + It("podman --userns=auto", func() { u, err := user.Current() Expect(err).To(BeNil()) diff --git a/test/e2e/untag_test.go b/test/e2e/untag_test.go index 50da76f7c..4e6dd6462 100644 --- a/test/e2e/untag_test.go +++ b/test/e2e/untag_test.go @@ -33,7 +33,11 @@ var _ = Describe("Podman untag", func() { }) It("podman untag all", func() { - Skip(v2remotefail) + SkipIfRemote() + setup := podmanTest.PodmanNoCache([]string{"pull", ALPINE}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + tags := []string{ALPINE, "registry.com/foo:bar", "localhost/foo:bar"} cmd := []string{"tag"} @@ -63,6 +67,10 @@ var _ = Describe("Podman untag", func() { }) It("podman tag/untag - tag normalization", func() { + setup := podmanTest.PodmanNoCache([]string{"pull", ALPINE}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + tests := []struct { tag, normalized string }{ diff --git a/test/system/070-build.bats b/test/system/070-build.bats index 84d3adec1..627b9caa6 100644 --- a/test/system/070-build.bats +++ b/test/system/070-build.bats @@ -109,6 +109,7 @@ EOF s_env1=$(random_string 20) s_env2=$(random_string 25) s_env3=$(random_string 30) + s_env4=$(random_string 40) # Label name: make sure it begins with a letter! jq barfs if you # try to ask it for '.foo.<N>xyz', i.e. any string beginning with digit @@ -118,11 +119,17 @@ EOF # Command to run on container startup with no args cat >$tmpdir/mycmd <<EOF #!/bin/sh +PATH=/usr/bin:/bin pwd echo "\$1" -echo "\$MYENV1" -echo "\$MYENV2" -echo "\$MYENV3" +printenv | grep MYENV | sort | sed -e 's/^MYENV.=//' +EOF + + # For overridding with --env-file + cat >$PODMAN_TMPDIR/env-file <<EOF +MYENV3=$s_env3 +http_proxy=http-proxy-in-env-file +https_proxy=https-proxy-in-env-file EOF cat >$tmpdir/Containerfile <<EOF @@ -130,11 +137,25 @@ FROM $IMAGE LABEL $label_name=$label_value RUN mkdir $workdir WORKDIR $workdir + +# Test for #7094 - chowning of invalid symlinks +RUN mkdir -p /a/b/c +RUN ln -s /no/such/nonesuch /a/b/c/badsymlink +RUN ln -s /bin/mydefaultcmd /a/b/c/goodsymlink +RUN touch /a/b/c/myfile +RUN chown -h 1:2 /a/b/c/badsymlink /a/b/c/goodsymlink /a/b/c/myfile +VOLUME /a/b/c + +# Test for environment passing and override ENV MYENV1=$s_env1 -ENV MYENV2 $s_env2 -ENV MYENV3 this-should-be-overridden +ENV MYENV2 this-should-be-overridden-by-env-host +ENV MYENV3 this-should-be-overridden-by-env-file +ENV MYENV4 this-should-be-overridden-by-cmdline +ENV http_proxy http-proxy-in-image +ENV ftp_proxy ftp-proxy-in-image ADD mycmd /bin/mydefaultcmd RUN chmod 755 /bin/mydefaultcmd +RUN chown 2:3 /bin/mydefaultcmd CMD ["/bin/mydefaultcmd","$s_echo"] EOF @@ -143,12 +164,28 @@ EOF run_podman build -t build_test -f build-test/Containerfile build-test # Run without args - should run the above script. Verify its output. - run_podman run --rm -e MYENV3="$s_env3" build_test + export MYENV2="$s_env2" + export MYENV3="env-file-should-override-env-host!" + run_podman run --rm \ + --env-file=$PODMAN_TMPDIR/env-file \ + --env-host \ + -e MYENV4="$s_env4" \ + build_test is "${lines[0]}" "$workdir" "container default command: pwd" is "${lines[1]}" "$s_echo" "container default command: output from echo" is "${lines[2]}" "$s_env1" "container default command: env1" is "${lines[3]}" "$s_env2" "container default command: env2" - is "${lines[4]}" "$s_env3" "container default command: env3 (from cmdline)" + is "${lines[4]}" "$s_env3" "container default command: env3 (from envfile)" + is "${lines[5]}" "$s_env4" "container default command: env4 (from cmdline)" + + # Proxies - environment should override container, but not env-file + http_proxy=http-proxy-from-env ftp_proxy=ftp-proxy-from-env \ + run_podman run --rm --env-file=$PODMAN_TMPDIR/env-file \ + build_test \ + printenv http_proxy https_proxy ftp_proxy + is "${lines[0]}" "http-proxy-in-env-file" "env-file overrides env" + is "${lines[1]}" "https-proxy-in-env-file" "env-file sets proxy var" + is "${lines[2]}" "ftp-proxy-from-env" "ftp-proxy is passed through" # test that workdir is set for command-line commands also run_podman run --rm build_test pwd @@ -159,8 +196,9 @@ EOF run_podman image inspect build_test tests=" Env[1] | MYENV1=$s_env1 -Env[2] | MYENV2=$s_env2 -Env[3] | MYENV3=this-should-be-overridden +Env[2] | MYENV2=this-should-be-overridden-by-env-host +Env[3] | MYENV3=this-should-be-overridden-by-env-file +Env[4] | MYENV4=this-should-be-overridden-by-cmdline Cmd[0] | /bin/mydefaultcmd Cmd[1] | $s_echo WorkingDir | $workdir @@ -173,10 +211,49 @@ Labels.$label_name | $label_value is "$actual" "$expect" "jq .Config.$field" done + # Bad symlink in volume. Prior to #7094, well, we wouldn't actually + # get here because any 'podman run' on a volume that had symlinks, + # be they dangling or valid, would barf with + # Error: chown <mountpath>/_data/symlink: ENOENT + run_podman run --rm build_test stat -c'%u:%g:%N' /a/b/c/badsymlink + is "$output" "0:0:'/a/b/c/badsymlink' -> '/no/such/nonesuch'" \ + "bad symlink to nonexistent file is chowned and preserved" + + run_podman run --rm build_test stat -c'%u:%g:%N' /a/b/c/goodsymlink + is "$output" "0:0:'/a/b/c/goodsymlink' -> '/bin/mydefaultcmd'" \ + "good symlink to existing file is chowned and preserved" + + run_podman run --rm build_test stat -c'%u:%g' /bin/mydefaultcmd + is "$output" "2:3" "target of symlink is not chowned" + + run_podman run --rm build_test stat -c'%u:%g:%N' /a/b/c/myfile + is "$output" "0:0:/a/b/c/myfile" "file in volume is chowned to root" + # Clean up run_podman rmi -f build_test } +@test "podman build - stdin test" { + if is_remote && is_rootless; then + skip "unreliable with podman-remote and rootless; #2972" + fi + + # Random workdir, and multiple random strings to verify command & env + workdir=/$(random_string 10) + PODMAN_TIMEOUT=240 run_podman build -t build_test - << EOF +FROM $IMAGE +RUN mkdir $workdir +WORKDIR $workdir +RUN /bin/echo 'Test' +EOF + is "$output" ".*STEP 5: COMMIT" "COMMIT seen in log" + + run_podman run --rm build_test pwd + is "$output" "$workdir" "pwd command in container" + + run_podman rmi -f build_test +} + function teardown() { # A timeout or other error in 'build' can leave behind stale images # that podman can't even see and which will cascade into subsequent diff --git a/test/system/helpers.t b/test/system/helpers.t index a022f11c4..bee09505c 100755 --- a/test/system/helpers.t +++ b/test/system/helpers.t @@ -6,7 +6,7 @@ # anything if we have to mess with them. # -source $(dirname $0)/helpers.bash +source "$(dirname $0)"/helpers.bash die() { echo "$(basename $0): $*" >&2 |