diff options
109 files changed, 2704 insertions, 1814 deletions
@@ -3,20 +3,26 @@ approvers: - edsantiago - giuseppe - jwhonce + - Luap99 - mheon - rhatdan + - saschagrunert - TomSweeneyRedHat - - vrothberg - umohnani8 + - vrothberg + - zhangguanzhang reviewers: + - ashley-cui - baude - edsantiago - giuseppe - jwhonce + - Luap99 - mheon + - QiWang19 - rhatdan + - saschagrunert - TomSweeneyRedHat - - vrothberg - - ashley-cui - - QiWang19 - umohnani8 + - vrothberg + - zhangguanzhang diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 7fed15e5e..25f4d0f79 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -22,7 +22,7 @@ var ( // ChangeCmds is the list of valid Change commands to passed to the Commit call ChangeCmds = []string{"CMD", "ENTRYPOINT", "ENV", "EXPOSE", "LABEL", "ONBUILD", "STOPSIGNAL", "USER", "VOLUME", "WORKDIR"} // LogLevels supported by podman - LogLevels = []string{"debug", "info", "warn", "error", "fatal", "panic"} + LogLevels = []string{"debug", "info", "warn", "warning", "error", "fatal", "panic"} ) type completeType int diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 0bb6e79e5..e0da142ad 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -517,18 +517,22 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string } switch con[0] { - case "proc-opts": - s.ProcOpts = strings.Split(con[1], ",") + case "apparmor": + s.ContainerSecurityConfig.ApparmorProfile = con[1] + s.Annotations[define.InspectAnnotationApparmor] = con[1] case "label": // TODO selinux opts and label opts are the same thing s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1]) s.Annotations[define.InspectAnnotationLabel] = strings.Join(s.ContainerSecurityConfig.SelinuxOpts, ",label=") - case "apparmor": - s.ContainerSecurityConfig.ApparmorProfile = con[1] - s.Annotations[define.InspectAnnotationApparmor] = con[1] + case "mask": + s.ContainerSecurityConfig.Mask = append(s.ContainerSecurityConfig.Mask, strings.Split(con[1], ":")...) + case "proc-opts": + s.ProcOpts = strings.Split(con[1], ",") case "seccomp": s.SeccompProfilePath = con[1] s.Annotations[define.InspectAnnotationSeccomp] = con[1] + case "unmask": + s.ContainerSecurityConfig.Unmask = append(s.ContainerSecurityConfig.Unmask, strings.Split(con[1], ":")...) default: return fmt.Errorf("invalid --security-opt 2: %q", opt) } diff --git a/cmd/podman/containers/cp.go b/cmd/podman/containers/cp.go index a74ea89d2..fd3aa7680 100644 --- a/cmd/podman/containers/cp.go +++ b/cmd/podman/containers/cp.go @@ -3,10 +3,7 @@ package containers import ( "github.com/containers/podman/v2/cmd/podman/common" "github.com/containers/podman/v2/cmd/podman/registry" - "github.com/containers/podman/v2/pkg/cgroups" "github.com/containers/podman/v2/pkg/domain/entities" - "github.com/containers/podman/v2/pkg/rootless" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -43,7 +40,7 @@ var ( func cpFlags(cmd *cobra.Command) { flags := cmd.Flags() flags.BoolVar(&cpOpts.Extract, "extract", false, "Extract the tar file into the destination directory.") - flags.BoolVar(&cpOpts.Pause, "pause", copyPause(), "Pause the container while copying") + flags.BoolVar(&cpOpts.Pause, "pause", true, "Pause the container while copying") } func init() { @@ -62,17 +59,5 @@ func init() { } func cp(cmd *cobra.Command, args []string) error { - _, err := registry.ContainerEngine().ContainerCp(registry.GetContext(), args[0], args[1], cpOpts) - return err -} - -func copyPause() bool { - if rootless.IsRootless() { - cgroupv2, _ := cgroups.IsCgroup2UnifiedMode() - if !cgroupv2 { - logrus.Debugf("defaulting to pause==false on rootless cp in cgroupv1 systems") - return false - } - } - return true + return registry.ContainerEngine().ContainerCp(registry.GetContext(), args[0], args[1], cpOpts) } diff --git a/cmd/podman/generate/kube.go b/cmd/podman/generate/kube.go index e47bd35b5..0517db19a 100644 --- a/cmd/podman/generate/kube.go +++ b/cmd/podman/generate/kube.go @@ -17,16 +17,16 @@ import ( var ( kubeOptions = entities.GenerateKubeOptions{} kubeFile = "" - kubeDescription = `Command generates Kubernetes pod and service YAML (v1 specification) from a Podman container or pod. + kubeDescription = `Command generates Kubernetes pod and service YAML (v1 specification) from Podman containers or a pod. Whether the input is for a container or pod, Podman will always generate the specification as a pod.` kubeCmd = &cobra.Command{ - Use: "kube [options] CONTAINER | POD", + Use: "kube [options] CONTAINER... | POD", Short: "Generate Kubernetes YAML from a container or pod.", Long: kubeDescription, RunE: kube, - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: common.AutocompleteContainersAndPods, Example: `podman generate kube ctrID podman generate kube podID @@ -51,7 +51,7 @@ func init() { } func kube(cmd *cobra.Command, args []string) error { - report, err := registry.ContainerEngine().GenerateKube(registry.GetContext(), args[0], kubeOptions) + report, err := registry.ContainerEngine().GenerateKube(registry.GetContext(), args, kubeOptions) if err != nil { return err } diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 7840e6100..0830a62a5 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -71,7 +71,7 @@ var ( DisableFlagsInUseLine: true, } - logLevel = "error" + logLevel = "warn" useSyslog bool requireCleanup = true ) diff --git a/docs/source/markdown/podman-cp.1.md b/docs/source/markdown/podman-cp.1.md index 8f63c00ee..241a74c42 100644 --- a/docs/source/markdown/podman-cp.1.md +++ b/docs/source/markdown/podman-cp.1.md @@ -9,12 +9,12 @@ podman\-cp - Copy files/folders between a container and the local filesystem **podman container cp** [*options*] [*container*:]*src_path* [*container*:]*dest_path* ## DESCRIPTION -Copies the contents of **src_path** to the **dest_path**. You can copy from the container's filesystem to the local machine or the reverse, from the local filesystem to the container. -If - is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. +Copy the contents of **src_path** to the **dest_path**. You can copy from the container's filesystem to the local machine or the reverse, from the local filesystem to the container. +If `-` is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. The CONTAINER can be a running or stopped container. The **src_path** or **dest_path** can be a file or directory. -The **podman cp** command assumes container paths are relative to the container's / (root) directory. +The **podman cp** command assumes container paths are relative to the container's root directory (i.e., `/`). This means supplying the initial forward slash is optional; @@ -27,24 +27,22 @@ Assuming a path separator of /, a first argument of **src_path** and second argu **src_path** specifies a file - **dest_path** does not exist - - the file is saved to a file created at **dest_path** - - **dest_path** does not exist and ends with / - - Error condition: the destination directory must exist. + - the file is saved to a file created at **dest_path** (note that parent directory must exist) - **dest_path** exists and is a file - - the destination is overwritten with the source file's contents + - the destination is overwritten with the source file's contents - **dest_path** exists and is a directory - - the file is copied into this directory using the basename from **src_path** + - the file is copied into this directory using the basename from **src_path** **src_path** specifies a directory - **dest_path** does not exist - - **dest_path** is created as a directory and the contents of the source directory are copied into this directory + - **dest_path** is created as a directory and the contents of the source directory are copied into this directory - **dest_path** exists and is a file - - Error condition: cannot copy a directory to a file + - Error condition: cannot copy a directory to a file - **dest_path** exists and is a directory - - **src_path** ends with / - - the source directory is copied into this directory - - **src_path** ends with /. (that is: slash followed by dot) - - the content of the source directory is copied into this directory + - **src_path** ends with `/` + - the source directory is copied into this directory + - **src_path** ends with `/.` (i.e., slash followed by dot) + - the content of the source directory is copied into this directory The command requires **src_path** and **dest_path** to exist according to the above rules. @@ -57,11 +55,13 @@ You can also use : when specifying paths to a **src_path** or **dest_path** on a If you use a : in a local machine path, you must be explicit with a relative or absolute path, for example: `/path/to/file:name.txt` or `./file:name.txt` +Using `-` as the *src_path* streams the contents of STDIN as a tar archive. The command extracts the content of the tar to the *DEST_PATH* in the container. In this case, *dest_path* must specify a directory. Using `-` as the *dest_path* streams the contents of the resource (can be a directory) as a tar archive to STDOUT. + ## OPTIONS #### **--extract** -Extract the tar file into the destination directory. If the destination directory is not provided, extract the tar file into the root directory. +If the source is a tar archive, extract it to the provided destination (must be a directory). If the source is not a tar archive, follow the above rules. #### **--pause** diff --git a/docs/source/markdown/podman-generate-kube.1.md b/docs/source/markdown/podman-generate-kube.1.md index 6fad89b1f..ed2143388 100644 --- a/docs/source/markdown/podman-generate-kube.1.md +++ b/docs/source/markdown/podman-generate-kube.1.md @@ -3,12 +3,12 @@ podman-generate-kube - Generate Kubernetes YAML based on a pod or container ## SYNOPSIS -**podman generate kube** [*options*] *container* | *pod* +**podman generate kube** [*options*] *container...* | *pod* ## DESCRIPTION -**podman generate kube** will generate Kubernetes Pod YAML (v1 specification) from a Podman container or pod. Whether -the input is for a container or pod, Podman will always generate the specification as a Pod. The input may be in the form -of a pod or container name or ID. +**podman generate kube** will generate Kubernetes Pod YAML (v1 specification) from Podman one or more containers or a single pod. Whether +the input is for containers or a pod, Podman will always generate the specification as a Pod. The input may be in the form +of a pod or one or more container names or IDs. Note that the generated Kubernetes YAML file can be used to re-run the deployment via podman-play-kube(1). diff --git a/docs/source/markdown/podman-image-sign.1.md b/docs/source/markdown/podman-image-sign.1.md index 1bd6e5b9d..7a924b80b 100644 --- a/docs/source/markdown/podman-image-sign.1.md +++ b/docs/source/markdown/podman-image-sign.1.md @@ -9,7 +9,9 @@ podman-image-sign - Create a signature for an image ## DESCRIPTION **podman image sign** will create a local signature for one or more local images that have been pulled from a registry. The signature will be written to a directory -derived from the registry configuration files in /etc/containers/registries.d. By default, the signature will be written into /var/lib/containers/sigstore directory. +derived from the registry configuration files in `$HOME/.config/containers/registries.d` if it exists, +otherwise `/etc/containers/registries.d` (unless overridden at compile-time), see **containers-registries.d(5)** for more information. +By default, the signature will be written into `/var/lib/containers/sigstore` for root and `$HOME/.local/share/containers/sigstore` for non-root users ## OPTIONS @@ -38,7 +40,8 @@ Sign the busybox image with the identify of foo@bar.com with a user's keyring an ## RELATED CONFIGURATION The write (and read) location for signatures is defined in YAML-based -configuration files in /etc/containers/registries.d/. When you sign +configuration files in /etc/containers/registries.d/ for root, +or $HOME/.config/containers/registries.d for non-root users. When you sign an image, Podman will use those configuration files to determine where to write the signature based on the the name of the originating registry or a default storage value unless overridden with the --directory @@ -53,5 +56,8 @@ the signature will be written into sub-directories of /var/lib/containers/sigstore/privateregistry.example.com. The use of 'sigstore' also means the signature will be 'read' from that same location on a pull-related function. +## SEE ALSO +containers-registries.d(5) + ## HISTORY November 2018, Originally compiled by Qi Wang (qiwan at redhat dot com) diff --git a/docs/source/markdown/podman-network-connect.1.md b/docs/source/markdown/podman-network-connect.1.md index 58b6e5c44..a31a415dc 100644 --- a/docs/source/markdown/podman-network-connect.1.md +++ b/docs/source/markdown/podman-network-connect.1.md @@ -10,6 +10,8 @@ podman\-network\-connect - Connect a container to a network Connects a container to a network. A container can be connected to a network by name or by ID. Once connected, the container can communicate with other containers in the same network. +This command is not available for rootless users. + ## OPTIONS #### **--alias** Add network-scoped alias for the container. If the network is using the `dnsname` CNI plugin, these aliases diff --git a/docs/source/markdown/podman-network-disconnect.1.md b/docs/source/markdown/podman-network-disconnect.1.md index 95c7018a8..8b7125282 100644 --- a/docs/source/markdown/podman-network-disconnect.1.md +++ b/docs/source/markdown/podman-network-disconnect.1.md @@ -9,6 +9,8 @@ podman\-network\-disconnect - Disconnect a container from a network ## DESCRIPTION Disconnects a container from a network. +This command is not available for rootless users. + ## OPTIONS #### **--force**, **-f** diff --git a/docs/source/markdown/podman-network-inspect.1.md b/docs/source/markdown/podman-network-inspect.1.md index 47d647b3f..56515d0c1 100644 --- a/docs/source/markdown/podman-network-inspect.1.md +++ b/docs/source/markdown/podman-network-inspect.1.md @@ -7,7 +7,7 @@ podman\-network\-inspect - Displays the raw CNI network configuration for one or **podman network inspect** [*options*] [*network* ...] ## DESCRIPTION -Display the raw (JSON format) network configuration. This command is not available for rootless users. +Display the raw (JSON format) network configuration. ## OPTIONS #### **--format**, **-f** diff --git a/docs/source/markdown/podman-network-ls.1.md b/docs/source/markdown/podman-network-ls.1.md index a964c97e8..9d2dd52a8 100644 --- a/docs/source/markdown/podman-network-ls.1.md +++ b/docs/source/markdown/podman-network-ls.1.md @@ -7,7 +7,7 @@ podman\-network\-ls - Display a summary of CNI networks **podman network ls** [*options*] ## DESCRIPTION -Displays a list of existing podman networks. This command is not available for rootless users. +Displays a list of existing podman networks. ## OPTIONS #### **--filter**, **-f** diff --git a/docs/source/markdown/podman-network.1.md b/docs/source/markdown/podman-network.1.md index d11f0d7b6..41e2ae885 100644 --- a/docs/source/markdown/podman-network.1.md +++ b/docs/source/markdown/podman-network.1.md @@ -7,7 +7,7 @@ podman\-network - Manage Podman CNI networks **podman network** *subcommand* ## DESCRIPTION -The network command manages CNI networks for Podman. It is not supported for rootless users. +The network command manages CNI networks for Podman. ## COMMANDS diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 83aaa33e8..53c5b2d4b 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -26,9 +26,12 @@ Several files will be automatically created within the container. These include _/etc/hosts_, _/etc/hostname_, and _/etc/resolv.conf_ to manage networking. These will be based on the host's version of the files, though they can be customized with options (for example, **--dns** will override the host's DNS -servers in the created _resolv.conf_). Additionally, an empty file is created in -each container to indicate to programs they are running in a container. This file -is located at _/run/.containerenv_. +servers in the created _resolv.conf_). Additionally, a container environment +file is created in each container to indicate to programs they are running in a +container. This file is located at _/run/.containerenv_. When using the +--privileged flag the .containerenv contains name/value pairs indicating the +container engine version, whether the engine is running in rootless mode, the +container name and id, as well as the image name and id that the container is based on. When running from a user defined network namespace, the _/etc/netns/NSNAME/resolv.conf_ will be used if it exists, otherwise _/etc/resolv.conf_ will be used. @@ -885,11 +888,16 @@ Security Options - **label=level:**_LEVEL_: Set the label level for the container processes - **label=filetype:**TYPE_: Set the label file type for the container files - **label=disable**: Turn off label separation for the container +- **mask**=_/path/1:/path/2_: The paths to mask separated by a colon. A masked path + cannot be accessed inside the container. - **no-new-privileges**: Disable container processes from gaining additional privileges - **seccomp=unconfined**: Turn off seccomp confinement for the container - **seccomp**=_profile.json_: Allowed syscall list seccomp JSON file to be used as a seccomp filter - **proc-opts**=_OPTIONS_ : Comma separated list of options to use for the /proc mount. More details for the possible mount options are specified at **proc(5)** man page. +- **unmask**=_ALL_ or _/path/1:/path/2_: Paths to unmask separated by a colon. If set to **ALL**, it will + unmask all the paths that are masked by default. + The default masked paths are **/proc/acpi, /proc/kcore, /proc/keys, /proc/latency_stats, /proc/sched_debug, /proc/scsi, /proc/timer_list, /proc/timer_stats, /sys/firmware, and /sys/fs/selinux.** Note: Labeling can be disabled for all containers by setting **label=false** in the **containers.conf**(5) file. @@ -1479,6 +1487,26 @@ $ podman run --security-opt label=type:svirt_apache_t -i -t centos bash Note you would have to write policy defining a **svirt_apache_t** type. +To mask additional specific paths in the container, specify the paths +separated by a colon using the **mask** option with the **--security-opt** +flag. + +``` +$ podman run --security-opt mask=/foo/bar:/second/path fedora bash +``` + +To unmask all the paths that are masked by default, set the **unmask** option to +**ALL**. Or to only unmask specific paths, specify the paths as shown above with +the **mask** option. + +``` +$ podman run --security-opt unmask=ALL fedora bash +``` + +``` +$ podman run --security-opt unmask=/foo/bar:/sys/firmware fedora bash +``` + ### Setting device weight If you want to set _/dev/sda_ device weight to **200**, you can specify the device @@ -11,13 +11,13 @@ require ( github.com/containernetworking/cni v0.8.0 github.com/containernetworking/plugins v0.8.7 github.com/containers/buildah v1.18.1-0.20201125084616-dd26b137459c - github.com/containers/common v0.29.0 + github.com/containers/common v0.31.0 github.com/containers/conmon v2.0.20+incompatible github.com/containers/image/v5 v5.8.1 github.com/containers/psgo v1.5.1 github.com/containers/storage v1.24.1 github.com/coreos/go-systemd/v22 v22.1.0 - github.com/cri-o/ocicni v0.2.1-0.20201102180012-75c612fda1a2 + github.com/cri-o/ocicni v0.2.1-0.20201125151022-df072ea5421c github.com/cyphar/filepath-securejoin v0.2.2 github.com/davecgh/go-spew v1.1.1 github.com/docker/distribution v2.7.1+incompatible @@ -73,5 +73,3 @@ require ( k8s.io/apimachinery v0.19.4 k8s.io/client-go v0.0.0-20190620085101-78d2af792bab ) - -replace github.com/cri-o/ocicni => github.com/cri-o/ocicni v0.2.1-0.20201109200316-afdc16ba66df @@ -95,6 +95,8 @@ github.com/containers/buildah v1.18.1-0.20201125084616-dd26b137459c h1:vyc2iYz9b github.com/containers/buildah v1.18.1-0.20201125084616-dd26b137459c/go.mod h1:B+0OkXUogxdwsEy4ax3a5/vDtJjL6vCisiV6frQZJ4A= github.com/containers/common v0.29.0 h1:hTMC+urdkk5bKfhL/OgCixIX5xjJgQ2l2jPG745ECFQ= github.com/containers/common v0.29.0/go.mod h1:yT4GTUHsKRmpaDb+mecXRnIMre7W3ZgwXqaYMywXlaA= +github.com/containers/common v0.31.0 h1:SRnjfoqbjfaojpY9YJq9JBPEslwB5hoXJbaE+5zMFwM= +github.com/containers/common v0.31.0/go.mod h1:yT4GTUHsKRmpaDb+mecXRnIMre7W3ZgwXqaYMywXlaA= github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg= github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I= github.com/containers/image/v5 v5.8.1 h1:aHW8a/Kd0dTJ7PTL/fc6y12sJqHxWgqilu+XyHfjD8Q= @@ -124,8 +126,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cri-o/ocicni v0.2.1-0.20201109200316-afdc16ba66df h1:c35uRFkER07nAkB1X21e+PI5xO21SOyI6G7tdfvz1z4= -github.com/cri-o/ocicni v0.2.1-0.20201109200316-afdc16ba66df/go.mod h1:vingr1ztOAzP2WyTgGbpMov9dFhbjNxdLtDv0+PhAvY= +github.com/cri-o/ocicni v0.2.1-0.20201125151022-df072ea5421c h1:iGaCU6d3oVT0pl8tmvyDhoA/vTDL3IX08akfsKZIy9o= +github.com/cri-o/ocicni v0.2.1-0.20201125151022-df072ea5421c/go.mod h1:vingr1ztOAzP2WyTgGbpMov9dFhbjNxdLtDv0+PhAvY= github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= diff --git a/hack/podman-socat b/hack/podman-socat new file mode 100755 index 000000000..7bc571816 --- /dev/null +++ b/hack/podman-socat @@ -0,0 +1,122 @@ +#!/bin/bash -e +# Execute podman while capturing the API stream +# +# Script will run an instance of podman sand-boxed, the API stream will be captured and then formatted for readability. + +if [[ $(id -u) != 0 ]]; then + echo >&2 "$0 must be run as root." + exit 2 +fi + +if ! command -v socat >/dev/null 2>&1; then + echo 1>&2 "socat not found on PATH" +fi + +PODMAN=${PODMAN:-podman} +if ! command -v "$PODMAN" >/dev/null 2>&1; then + echo 1>&2 "$PODMAN not found on PATH" +fi + +function usage() { + echo 1>&2 $0 '[-v] [-h]' +} + +while getopts "vh" arg; do + case $arg in + v) + VERBOSE='-v' + export PODMAN_LOG_LEVEL=debug + ;; + h) + usage + exit 0 + ;; + \?) + usage + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +function cleanup() { + set +xeuo pipefail + rm -r "$1" + kill -9 $REAP_PIDS + + sed -e 's/^> /\nClient Request> /' -e 's/^< /\nServer Response< /' -i /tmp/podman-socat.log +} + +# Create temporary directory for storage +export TMPDIR=$(mktemp -d /tmp/podman.XXXXXXXXXX) +trap "cleanup $TMPDIR" EXIT + +# Need locations to store stuff +mkdir -p "${TMPDIR}"/{podman,crio,crio-run,cni/net.d,ctnr,tunnel} + +export REGISTRIES_CONFIG_PATH=${TMPDIR}/registry.conf +cat >"$REGISTRIES_CONFIG_PATH" <<-EOT + [registries.search] + registries = ['docker.io'] + [registries.insecure] + registries = [] + [registries.block] + registries = [] +EOT + +export CNI_CONFIG_PATH=${TMPDIR}/cni/net.d +cat >"$CNI_CONFIG_PATH"/87-podman-bridge.conflist <<-EOT +{ + "cniVersion": "0.3.0", + "name": "podman", + "plugins": [{ + "type": "bridge", + "bridge": "cni0", + "isGateway": true, + "ipMasq": true, + "ipam": { + "type": "host-local", + "subnet": "10.88.0.0/16", + "routes": [{ + "dst": "0.0.0.0/0" + }] + } + }, + { + "type": "portmap", + "capabilities": { + "portMappings": true + } + } + ] +} +EOT + +PODMAN_ARGS="--storage-driver=vfs \ + --root=${TMPDIR}/crio \ + --runroot=${TMPDIR}/crio-run \ + --cni-config-dir=$CNI_CONFIG_PATH \ + --cgroup-manager=systemd \ + " +if [[ -n $VERBOSE ]]; then + PODMAN_ARGS="$PODMAN_ARGS --log-level=$PODMAN_LOG_LEVEL --syslog=true" +fi +PODMAN="$PODMAN $PODMAN_ARGS" + +PODMAN_HOST="${TMPDIR}/podman/podman-socat.sock" +SOCAT_HOST="${TMPDIR}/podman/podman.sock" + +cat <<-EOT +Podman service running at unix:$SOCAT_HOST +See /tmp/podman-socat.log for API stream capture +See /tmp/podman-service.log for service logging + +usage: sudo bin/podman-remote --url unix:$SOCAT_HOST images + +^C to exit +EOT + +$PODMAN system service --timeout=0 "unix:$PODMAN_HOST" >/tmp/podman-service.log 2>&1 & +REAP_PIDS=$! + +socat -v "UNIX-LISTEN:$SOCAT_HOST",fork,reuseaddr,unlink-early "UNIX-CONNECT:$PODMAN_HOST" >/tmp/podman-socat.log 2>&1 diff --git a/install.md b/install.md index 2ef6eae2c..d09abec3a 100644 --- a/install.md +++ b/install.md @@ -1,5 +1,5 @@ # libpod Installation Instructions -The installation instructions for Podman and libpod now reside **[here](https://podman.io/getting-started/installation)** in the **[podman.io](https://podman.io)** site. From the homepage, the installation instructions can be found under "Get Started->Installing Podman". +The installation instructions for Podman and libpod now reside **[here](https://podman.io/getting-started/installation)** on the **[podman.io](https://podman.io)** site. -The podman.io site resides in a GitHub under the Containers repository at [https://github.com/containers/podman.io](https://github.com/containers/podman.io). If you see a change that needs to happen to the installation instructions, please feel free to open a pull request there, we're always happy to have new contributors! +The podman.io site resides in a GitHub repository under the containers organization at [https://github.com/containers/podman.io](https://github.com/containers/podman.io). If you see a change that needs to happen to the installation instructions, please feel free to open a pull request there. We're always happy to have new contributors! diff --git a/libpod/container.go b/libpod/container.go index e954d84eb..96a21736c 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -267,6 +267,11 @@ func (c *Container) Config() *ContainerConfig { return returnConfig } +// Runtime returns the container's Runtime. +func (c *Container) Runtime() *Runtime { + return c.runtime +} + // Spec returns the container's OCI runtime spec // The spec returned is the one used to create the container. The running // spec may differ slightly as mounts are added based on the image @@ -916,13 +921,33 @@ func (c *Container) CgroupManager() string { return cgroupManager } -// CGroupPath returns a cgroups "path" for a given container. +// CGroupPath returns a cgroups "path" for the given container. +// Note that the container must be running. Otherwise, an error +// is returned. func (c *Container) CGroupPath() (string, error) { + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + if err := c.syncContainer(); err != nil { + return "", errors.Wrapf(err, "error updating container %s state", c.ID()) + } + } + return c.cGroupPath() +} + +// cGroupPath returns a cgroups "path" for the given container. +// Note that the container must be running. Otherwise, an error +// is returned. +// NOTE: only call this when owning the container's lock. +func (c *Container) cGroupPath() (string, error) { if c.config.NoCgroups || c.config.CgroupsMode == "disabled" { return "", errors.Wrapf(define.ErrNoCgroups, "this container is not creating cgroups") } + if c.state.State != define.ContainerStateRunning && c.state.State != define.ContainerStatePaused { + return "", errors.Wrapf(define.ErrCtrStopped, "cannot get cgroup path unless container %s is running", c.ID()) + } - // Read /proc/[PID]/cgroup and find the *longest* cgroup entry. That's + // Read /proc/{PID}/cgroup and find the *longest* cgroup entry. That's // needed to account for hacks in cgroups v1, where each line in the // file could potentially point to a cgroup. The longest one, however, // is the libpod-specific one we're looking for. @@ -947,7 +972,6 @@ func (c *Container) CGroupPath() (string, error) { if len(path) > len(cgroupPath) { cgroupPath = path } - } if len(cgroupPath) == 0 { diff --git a/libpod/container_config.go b/libpod/container_config.go index cc3ad25ea..c95be9b55 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -135,7 +135,13 @@ type ContainerRootFSConfig struct { // OverlayVolumes lists the overlay volumes to mount into the container. OverlayVolumes []*ContainerOverlayVolume `json:"overlayVolumes,omitempty"` // ImageVolumes lists the image volumes to mount into the container. - ImageVolumes []*ContainerImageVolume `json:"imageVolumes,omitempty"` + // Please note that this is named ctrImageVolumes in JSON to + // distinguish between these and the old `imageVolumes` field in Podman + // pre-1.8, which was used in very old Podman versions to determine how + // image volumes were handled in Libpod (support for these eventually + // moved out of Libpod into pkg/specgen). + // Please DO NOT re-use the `imageVolumes` name in container JSON again. + ImageVolumes []*ContainerImageVolume `json:"ctrImageVolumes,omitempty"` // CreateWorkingDir indicates that Libpod should create the container's // working directory if it does not exist. Some OCI runtimes do this by // default, but others do not. diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index f78d74ef7..2ce3e8e68 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -488,7 +488,7 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named // Weight? For now, ignore anything // without Weight set. if dev.Weight == nil { - logrus.Warnf("Ignoring weight device %s as it lacks a weight", key) + logrus.Infof("Ignoring weight device %s as it lacks a weight", key) continue } if deviceNodes == nil { @@ -500,7 +500,7 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named } path, ok := deviceNodes[key] if !ok { - logrus.Warnf("Could not locate weight device %s in system devices", key) + logrus.Infof("Could not locate weight device %s in system devices", key) continue } weightDev := define.InspectBlkioWeightDevice{} @@ -522,7 +522,7 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named } path, ok := deviceNodes[key] if !ok { - logrus.Warnf("Could not locate throttle device %s in system devices", key) + logrus.Infof("Could not locate throttle device %s in system devices", key) continue } throttleDev := define.InspectBlkioThrottleDevice{} diff --git a/libpod/container_internal.go b/libpod/container_internal.go index b6a3244ea..c751d775d 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -525,7 +525,7 @@ func (c *Container) teardownStorage() error { // Potentially another tool using containers/storage already // removed it? if errors.Cause(err) == storage.ErrNotAContainer || errors.Cause(err) == storage.ErrContainerUnknown { - logrus.Warnf("Storage for container %s already removed", c.ID()) + logrus.Infof("Storage for container %s already removed", c.ID()) return nil } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index a9a789ad8..1bf044f9d 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -35,6 +35,7 @@ import ( "github.com/containers/podman/v2/pkg/rootless" "github.com/containers/podman/v2/pkg/util" "github.com/containers/podman/v2/utils" + "github.com/containers/podman/v2/version" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/idtools" securejoin "github.com/cyphar/filepath-securejoin" @@ -365,7 +366,7 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { if !MountExists(g.Mounts(), dstPath) { g.AddMount(newMount) } else { - logrus.Warnf("User mount overriding libpod mount at %q", dstPath) + logrus.Infof("User mount overriding libpod mount at %q", dstPath) } } @@ -1436,11 +1437,26 @@ func (c *Container) makeBindMounts() error { } } - // Make .containerenv - // Empty file, so no need to recreate if it exists + // Make .containerenv if it does not exist if _, ok := c.state.BindMounts["/run/.containerenv"]; !ok { - // Empty string for now, but we may consider populating this later - containerenvPath, err := c.writeStringToRundir(".containerenv", "") + var containerenv string + isRootless := 0 + if rootless.IsRootless() { + isRootless = 1 + } + imageID, imageName := c.Image() + + if c.Privileged() { + // Populate the .containerenv with container information + containerenv = fmt.Sprintf(`engine="podman-%s" +name=%q +id=%q +image=%q +imageid=%q +rootless=%d +`, version.Version.String(), c.Name(), c.ID(), imageName, imageID, isRootless) + } + containerenvPath, err := c.writeStringToRundir(".containerenv", containerenv) if err != nil { return errors.Wrapf(err, "error creating containerenv file for container %s", c.ID()) } diff --git a/libpod/events/config.go b/libpod/events/config.go index fc1457289..085fa9d52 100644 --- a/libpod/events/config.go +++ b/libpod/events/config.go @@ -121,6 +121,8 @@ const ( Cleanup Status = "cleanup" // Commit ... Commit Status = "commit" + // Copy ... + Copy Status = "copy" // Create ... Create Status = "create" // Exec ... diff --git a/libpod/image/image.go b/libpod/image/image.go index cecd64eb7..5c3f3b9e4 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -1222,6 +1222,11 @@ func (i *Image) inspect(ctx context.Context, calculateSize bool) (*inspect.Image } } + parent, err := i.ParentID(ctx) + if err != nil { + return nil, err + } + repoTags, err := i.RepoTags() if err != nil { return nil, err @@ -1248,6 +1253,7 @@ func (i *Image) inspect(ctx context.Context, calculateSize bool) (*inspect.Image data := &inspect.ImageData{ ID: i.ID(), + Parent: parent, RepoTags: repoTags, RepoDigests: repoDigests, Comment: comment, @@ -1258,10 +1264,12 @@ func (i *Image) inspect(ctx context.Context, calculateSize bool) (*inspect.Image Config: &ociv1Img.Config, Version: info.DockerVersion, Size: size, - VirtualSize: size, - Annotations: annotations, - Digest: i.Digest(), - Labels: info.Labels, + // This is good enough for now, but has to be + // replaced later with correct calculation logic + VirtualSize: size, + Annotations: annotations, + Digest: i.Digest(), + Labels: info.Labels, RootFS: &inspect.RootFS{ Type: ociv1Img.RootFS.Type, Layers: ociv1Img.RootFS.DiffIDs, @@ -1505,6 +1513,15 @@ func (i *Image) GetParent(ctx context.Context) (*Image, error) { return tree.parent(ctx, i) } +// ParentID returns the image ID of the parent. Return empty string if a parent is not found. +func (i *Image) ParentID(ctx context.Context) (string, error) { + parent, err := i.GetParent(ctx) + if err == nil && parent != nil { + return parent.ID(), nil + } + return "", err +} + // GetChildren returns a list of the imageIDs that depend on the image func (i *Image) GetChildren(ctx context.Context) ([]string, error) { children, err := i.getChildren(ctx, true) diff --git a/libpod/info.go b/libpod/info.go index dd7a521c1..2f64a107e 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -117,7 +117,6 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { if rootless.IsRootless() { if path, err := exec.LookPath("slirp4netns"); err == nil { - logrus.Warnf("Failed to retrieve program version for %s: %v", path, err) version, err := programVersion(path) if err != nil { logrus.Warnf("Failed to retrieve program version for %s: %v", path, err) diff --git a/libpod/kube.go b/libpod/kube.go index 067e7827d..bf041112a 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -21,9 +21,9 @@ import ( // GenerateForKube takes a slice of libpod containers and generates // one v1.Pod description that includes just a single container. -func (c *Container) GenerateForKube() (*v1.Pod, error) { +func GenerateForKube(ctrs []*Container) (*v1.Pod, error) { // Generate the v1.Pod yaml description - return simplePodWithV1Container(c) + return simplePodWithV1Containers(ctrs) } // GenerateForKube takes a slice of libpod containers and generates @@ -236,14 +236,20 @@ func addContainersAndVolumesToPodObject(containers []v1.Container, volumes []v1. return &p } -// simplePodWithV1Container is a function used by inspect when kube yaml needs to be generated +// simplePodWithV1Containers is a function used by inspect when kube yaml needs to be generated // for a single container. we "insert" that container description in a pod. -func simplePodWithV1Container(ctr *Container) (*v1.Pod, error) { - kubeCtr, kubeVols, err := containerToV1Container(ctr) - if err != nil { - return nil, err +func simplePodWithV1Containers(ctrs []*Container) (*v1.Pod, error) { + kubeCtrs := make([]v1.Container, 0, len(ctrs)) + kubeVolumes := make([]v1.Volume, 0) + for _, ctr := range ctrs { + kubeCtr, kubeVols, err := containerToV1Container(ctr) + if err != nil { + return nil, err + } + kubeCtrs = append(kubeCtrs, kubeCtr) + kubeVolumes = append(kubeVolumes, kubeVols...) } - return addContainersAndVolumesToPodObject([]v1.Container{kubeCtr}, kubeVols, ctr.Name()), nil + return addContainersAndVolumesToPodObject(kubeCtrs, kubeVolumes, strings.ReplaceAll(ctrs[0].Name(), "_", "")), nil } @@ -294,6 +300,12 @@ func containerToV1Container(c *Container) (v1.Container, []v1.Volume, error) { _, image := c.Image() kubeContainer.Image = image kubeContainer.Stdin = c.Stdin() + + // prepend the entrypoint of the container to command + if ep := c.Entrypoint(); len(c.Entrypoint()) > 0 { + ep = append(ep, containerCommands...) + containerCommands = ep + } kubeContainer.Command = containerCommands // TODO need to figure out how we handle command vs entry point. Kube appears to prefer entrypoint. // right now we just take the container's command diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index c7db95eb3..2171a9208 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -246,7 +246,7 @@ func (r *Runtime) setupRootlessNetNS(ctr *Container) error { // setupSlirp4netns can be called in rootful as well as in rootless func (r *Runtime) setupSlirp4netns(ctr *Container) error { path := r.config.Engine.NetworkCmdPath - + slirpOptions := r.config.Engine.NetworkCmdOptions if path == "" { var err error path, err = exec.LookPath("slirp4netns") @@ -274,68 +274,69 @@ func (r *Runtime) setupSlirp4netns(ctr *Container) error { outboundAddr6 := "" if ctr.config.NetworkOptions != nil { - slirpOptions := ctr.config.NetworkOptions["slirp4netns"] - for _, o := range slirpOptions { - parts := strings.SplitN(o, "=", 2) - if len(parts) < 2 { - return errors.Errorf("unknown option for slirp4netns: %q", o) + slirpOptions = append(slirpOptions, ctr.config.NetworkOptions["slirp4netns"]...) + } + + for _, o := range slirpOptions { + parts := strings.SplitN(o, "=", 2) + if len(parts) < 2 { + return errors.Errorf("unknown option for slirp4netns: %q", o) + } + option, value := parts[0], parts[1] + switch option { + case "cidr": + ipv4, _, err := net.ParseCIDR(value) + if err != nil || ipv4.To4() == nil { + return errors.Errorf("invalid cidr %q", value) } - option, value := parts[0], parts[1] - switch option { - case "cidr": - ipv4, _, err := net.ParseCIDR(value) - if err != nil || ipv4.To4() == nil { - return errors.Errorf("invalid cidr %q", value) - } - cidr = value - case "port_handler": - switch value { - case "slirp4netns": - isSlirpHostForward = true - case "rootlesskit": - isSlirpHostForward = false - default: - return errors.Errorf("unknown port_handler for slirp4netns: %q", value) - } - case "allow_host_loopback": - switch value { - case "true": - disableHostLoopback = false - case "false": - disableHostLoopback = true - default: - return errors.Errorf("invalid value of allow_host_loopback for slirp4netns: %q", value) - } - case "enable_ipv6": - switch value { - case "true": - enableIPv6 = true - case "false": - enableIPv6 = false - default: - return errors.Errorf("invalid value of enable_ipv6 for slirp4netns: %q", value) - } - case "outbound_addr": - ipv4 := net.ParseIP(value) - if ipv4 == nil || ipv4.To4() == nil { - _, err := net.InterfaceByName(value) - if err != nil { - return errors.Errorf("invalid outbound_addr %q", value) - } + cidr = value + case "port_handler": + switch value { + case "slirp4netns": + isSlirpHostForward = true + case "rootlesskit": + isSlirpHostForward = false + default: + return errors.Errorf("unknown port_handler for slirp4netns: %q", value) + } + case "allow_host_loopback": + switch value { + case "true": + disableHostLoopback = false + case "false": + disableHostLoopback = true + default: + return errors.Errorf("invalid value of allow_host_loopback for slirp4netns: %q", value) + } + case "enable_ipv6": + switch value { + case "true": + enableIPv6 = true + case "false": + enableIPv6 = false + default: + return errors.Errorf("invalid value of enable_ipv6 for slirp4netns: %q", value) + } + case "outbound_addr": + ipv4 := net.ParseIP(value) + if ipv4 == nil || ipv4.To4() == nil { + _, err := net.InterfaceByName(value) + if err != nil { + return errors.Errorf("invalid outbound_addr %q", value) } - outboundAddr = value - case "outbound_addr6": - ipv6 := net.ParseIP(value) - if ipv6 == nil || ipv6.To4() != nil { - _, err := net.InterfaceByName(value) - if err != nil { - return errors.Errorf("invalid outbound_addr6: %q", value) - } + } + outboundAddr = value + case "outbound_addr6": + ipv6 := net.ParseIP(value) + if ipv6 == nil || ipv6.To4() != nil { + _, err := net.InterfaceByName(value) + if err != nil { + return errors.Errorf("invalid outbound_addr6: %q", value) } - outboundAddr6 = value - default: - return errors.Errorf("unknown option for slirp4netns: %q", o) } + outboundAddr6 = value + default: + return errors.Errorf("unknown option for slirp4netns: %q", o) } } diff --git a/libpod/oci_conmon_exec_linux.go b/libpod/oci_conmon_exec_linux.go index 7068bf87a..f8e7020f7 100644 --- a/libpod/oci_conmon_exec_linux.go +++ b/libpod/oci_conmon_exec_linux.go @@ -231,7 +231,7 @@ func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, t // Wait for the PID to stop if err := waitPidStop(pid, time.Duration(timeout)*time.Second); err != nil { - logrus.Warnf("Timed out waiting for container %s exec session %s to stop, resorting to SIGKILL", ctr.ID(), sessionID) + logrus.Infof("Timed out waiting for container %s exec session %s to stop, resorting to SIGKILL: %v", ctr.ID(), sessionID, err) } else { // No error, container is dead return nil diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index bd58610a2..307b9bc54 100644 --- a/libpod/oci_conmon_linux.go +++ b/libpod/oci_conmon_linux.go @@ -442,7 +442,7 @@ func (r *ConmonOCIRuntime) StopContainer(ctr *Container, timeout uint, all bool) } if err := waitContainerStop(ctr, time.Duration(timeout)*time.Second); err != nil { - logrus.Warnf("Timed out stopping container %s, resorting to SIGKILL", ctr.ID()) + logrus.Infof("Timed out stopping container %s, resorting to SIGKILL: %v", ctr.ID(), err) } else { // No error, the container is dead return nil @@ -1009,7 +1009,7 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co if ctr.config.SdNotifyMode == define.SdNotifyModeIgnore { if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil { - logrus.Warnf("Error unsetting NOTIFY_SOCKET %s", err.Error()) + logrus.Warnf("Error unsetting NOTIFY_SOCKET %v", err) } } @@ -1155,14 +1155,14 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co conmonPID, err := readConmonPidFile(ctr.config.ConmonPidFile) if err != nil { - logrus.Warnf("error reading conmon pid file for container %s: %s", ctr.ID(), err.Error()) + logrus.Warnf("error reading conmon pid file for container %s: %v", ctr.ID(), err) } else if conmonPID > 0 { // conmon not having a pid file is a valid state, so don't set it if we don't have it logrus.Infof("Got Conmon PID as %d", conmonPID) ctr.state.ConmonPID = conmonPID if ctr.config.SdNotifyMode != define.SdNotifyModeIgnore { if sent, err := daemon.SdNotify(false, fmt.Sprintf("MAINPID=%d", conmonPID)); err != nil { - logrus.Errorf("Error notifying systemd of Conmon PID: %s", err.Error()) + logrus.Errorf("Error notifying systemd of Conmon PID: %v", err) } else if sent { logrus.Debugf("Notify MAINPID sent successfully") } diff --git a/libpod/reset.go b/libpod/reset.go index f8828fed4..6d2842723 100644 --- a/libpod/reset.go +++ b/libpod/reset.go @@ -46,7 +46,7 @@ func (r *Runtime) Reset(ctx context.Context) error { } } - if err := stopPauseProcess(); err != nil { + if err := r.stopPauseProcess(); err != nil { logrus.Errorf("Error stopping pause process: %v", err) } diff --git a/libpod/runtime.go b/libpod/runtime.go index df3dfae2b..1004e4fa7 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -190,7 +190,7 @@ func newRuntimeFromConfig(ctx context.Context, conf *config.Config, options ...R if err := shutdown.Register("libpod", func(sig os.Signal) error { os.Exit(1) return nil - }); err != nil { + }); err != nil && errors.Cause(err) != shutdown.ErrHandlerExists { logrus.Errorf("Error registering shutdown handler for libpod: %v", err) } @@ -387,8 +387,8 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { // Don't fatally error. // This will allow us to ship configs including optional // runtimes that might not be installed (crun, kata). - // Only a warnf so default configs don't spec errors. - logrus.Warnf("Error initializing configured OCI runtime %s: %v", name, err) + // Only a infof so default configs don't spec errors. + logrus.Infof("Error initializing configured OCI runtime %s: %v", name, err) continue } @@ -472,7 +472,7 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { // we will need to access the storage. if os.Geteuid() != 0 { aliveLock.Unlock() // Unlock to avoid deadlock as BecomeRootInUserNS will reexec. - pausePid, err := util.GetRootlessPauseProcessPidPath() + pausePid, err := util.GetRootlessPauseProcessPidPathGivenDir(runtime.config.Engine.TmpDir) if err != nil { return errors.Wrapf(err, "could not get pause process pid file path") } @@ -538,6 +538,15 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { return nil } +// TmpDir gets the current Libpod temporary files directory. +func (r *Runtime) TmpDir() (string, error) { + if !r.valid { + return "", define.ErrRuntimeStopped + } + + return r.config.Engine.TmpDir, nil +} + // GetConfig returns a copy of the configuration used by the runtime func (r *Runtime) GetConfig() (*config.Config, error) { r.lock.RLock() diff --git a/libpod/runtime_cstorage.go b/libpod/runtime_cstorage.go index 61fdd42d3..6ee8a9354 100644 --- a/libpod/runtime_cstorage.go +++ b/libpod/runtime_cstorage.go @@ -103,7 +103,7 @@ func (r *Runtime) removeStorageContainer(idOrName string, force bool) error { if errors.Cause(err) == storage.ErrContainerUnknown { // Container was removed from under us. // It's gone, so don't bother erroring. - logrus.Warnf("Storage for container %s already removed", ctr.ID) + logrus.Infof("Storage for container %s already removed", ctr.ID) return nil } return errors.Wrapf(err, "error looking up container %q mounts", idOrName) @@ -114,7 +114,7 @@ func (r *Runtime) removeStorageContainer(idOrName string, force bool) error { } else if _, err := r.store.Unmount(ctr.ID, true); err != nil { if errors.Cause(err) == storage.ErrContainerUnknown { // Container again gone, no error - logrus.Warnf("Storage for container %s already removed", ctr.ID) + logrus.Infof("Storage for container %s already removed", ctr.ID) return nil } return errors.Wrapf(err, "error unmounting container %q", idOrName) @@ -123,7 +123,7 @@ func (r *Runtime) removeStorageContainer(idOrName string, force bool) error { if err := r.store.DeleteContainer(ctr.ID); err != nil { if errors.Cause(err) == storage.ErrContainerUnknown { // Container again gone, no error - logrus.Warnf("Storage for container %s already removed", ctr.ID) + logrus.Infof("Storage for container %s already removed", ctr.ID) return nil } return errors.Wrapf(err, "error removing storage for container %q", idOrName) diff --git a/libpod/runtime_migrate.go b/libpod/runtime_migrate.go index 1ad32fe9c..f0f800ef0 100644 --- a/libpod/runtime_migrate.go +++ b/libpod/runtime_migrate.go @@ -18,9 +18,9 @@ import ( "github.com/sirupsen/logrus" ) -func stopPauseProcess() error { +func (r *Runtime) stopPauseProcess() error { if rootless.IsRootless() { - pausePidPath, err := util.GetRootlessPauseProcessPidPath() + pausePidPath, err := util.GetRootlessPauseProcessPidPathGivenDir(r.config.Engine.TmpDir) if err != nil { return errors.Wrapf(err, "could not get pause process pid file path") } @@ -98,5 +98,5 @@ func (r *Runtime) migrate(ctx context.Context) error { } } - return stopPauseProcess() + return r.stopPauseProcess() } diff --git a/libpod/runtime_migrate_unsupported.go b/libpod/runtime_migrate_unsupported.go index e362cca63..a9d351318 100644 --- a/libpod/runtime_migrate_unsupported.go +++ b/libpod/runtime_migrate_unsupported.go @@ -10,6 +10,6 @@ func (r *Runtime) migrate(ctx context.Context) error { return nil } -func stopPauseProcess() error { +func (r *Runtime) stopPauseProcess() error { return nil } diff --git a/libpod/runtime_pod_linux.go b/libpod/runtime_pod_linux.go index 25598ce4d..1eb42660c 100644 --- a/libpod/runtime_pod_linux.go +++ b/libpod/runtime_pod_linux.go @@ -117,7 +117,7 @@ func (r *Runtime) NewPod(ctx context.Context, options ...PodCreateOption) (_ *Po return nil, errors.Errorf("Pods must have an infra container to share namespaces") } if pod.HasInfraContainer() && !pod.SharesNamespaces() { - logrus.Warnf("Pod has an infra container, but shares no namespaces") + logrus.Infof("Pod has an infra container, but shares no namespaces") } if err := r.state.AddPod(pod); err != nil { @@ -212,7 +212,7 @@ func (r *Runtime) removePod(ctx context.Context, p *Pod, removeCtrs, force bool) // Don't try if we failed to retrieve the cgroup if err == nil { if err := conmonCgroup.Update(resLimits); err != nil { - logrus.Warnf("Error updating pod %s conmon cgroup %s PID limit: %v", p.ID(), conmonCgroupPath, err) + logrus.Warnf("Error updating pod %s conmon cgroup PID limit: %v", p.ID(), err) } } } diff --git a/libpod/shutdown/handler.go b/libpod/shutdown/handler.go index 87538dec9..f0f228b19 100644 --- a/libpod/shutdown/handler.go +++ b/libpod/shutdown/handler.go @@ -11,6 +11,10 @@ import ( ) var ( + ErrHandlerExists error = errors.New("handler with given name already exists") +) + +var ( stopped bool sigChan chan os.Signal cancelChan chan bool @@ -98,7 +102,7 @@ func Register(name string, handler func(os.Signal) error) error { } if _, ok := handlers[name]; ok { - return errors.Errorf("handler with name %s already exists", name) + return ErrHandlerExists } handlers[name] = handler diff --git a/libpod/stats.go b/libpod/stats.go index e34739626..09d990017 100644 --- a/libpod/stats.go +++ b/libpod/stats.go @@ -34,7 +34,7 @@ func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*de return stats, define.ErrCtrStateInvalid } - cgroupPath, err := c.CGroupPath() + cgroupPath, err := c.cGroupPath() if err != nil { return nil, err } diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 5886455e7..7a3e5dd84 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" + "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -73,7 +74,7 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } func ListContainers(w http.ResponseWriter, r *http.Request) { @@ -207,7 +208,7 @@ func KillContainer(w http.ResponseWriter, r *http.Request) { } } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } func WaitContainer(w http.ResponseWriter, r *http.Request) { @@ -215,8 +216,10 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) { // /{version}/containers/(name)/wait exitCode, err := utils.WaitContainer(w, r) if err != nil { + logrus.Warnf("failed to wait on container %q: %v", mux.Vars(r)["name"], err) return } + utils.WriteResponse(w, http.StatusOK, handlers.ContainerWaitOKBody{ StatusCode: int(exitCode), Error: struct { diff --git a/pkg/api/handlers/compat/containers_archive.go b/pkg/api/handlers/compat/containers_archive.go index 1dd563393..223eb2cd5 100644 --- a/pkg/api/handlers/compat/containers_archive.go +++ b/pkg/api/handlers/compat/containers_archive.go @@ -5,24 +5,16 @@ import ( "encoding/base64" "encoding/json" "fmt" - "path/filepath" - "strings" - - "github.com/containers/buildah/copier" - "github.com/containers/buildah/pkg/chrootuser" - "github.com/containers/podman/v2/libpod" - "github.com/containers/podman/v2/libpod/define" - "github.com/containers/podman/v2/pkg/api/handlers/utils" - "github.com/containers/storage/pkg/idtools" - "github.com/opencontainers/runtime-spec/specs-go" - "net/http" "os" "time" + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/pkg/api/handlers/utils" + "github.com/containers/podman/v2/pkg/copy" "github.com/gorilla/schema" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) func Archive(w http.ResponseWriter, r *http.Request) { @@ -32,14 +24,14 @@ func Archive(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPut: handlePut(w, r, decoder, runtime) - case http.MethodGet, http.MethodHead: - handleHeadOrGet(w, r, decoder, runtime) + case http.MethodHead, http.MethodGet: + handleHeadAndGet(w, r, decoder, runtime) default: - utils.Error(w, fmt.Sprintf("not implemented, method: %v", r.Method), http.StatusNotImplemented, errors.New(fmt.Sprintf("not implemented, method: %v", r.Method))) + utils.Error(w, fmt.Sprintf("unsupported method: %v", r.Method), http.StatusNotImplemented, errors.New(fmt.Sprintf("unsupported method: %v", r.Method))) } } -func handleHeadOrGet(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { +func handleHeadAndGet(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { query := struct { Path string `schema:"path"` }{} @@ -66,170 +58,62 @@ func handleHeadOrGet(w http.ResponseWriter, r *http.Request, decoder *schema.Dec return } - mountPoint, err := ctr.Mount() + source, err := copy.CopyItemForContainer(ctr, query.Path, true, true) + defer source.CleanUp() if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to mount the container")) + utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrapf(err, "error stating container path %q", query.Path)) return } - defer func() { - if err := ctr.Unmount(false); err != nil { - logrus.Warnf("failed to unmount container %s: %q", containerName, err) - } - }() - - opts := copier.StatOptions{} - - mountPoint, path, err := fixUpMountPointAndPath(runtime, ctr, mountPoint, query.Path) + // NOTE: Docker always sets the header. + info, err := source.Stat() if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrapf(err, "error stating container path %q", query.Path)) return } - - stats, err := copier.Stat(mountPoint, "", opts, []string{filepath.Join(mountPoint, path)}) + statHeader, err := fileInfoToDockerStats(info) if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to get stats about file")) - return - } - - if len(stats) <= 0 || len(stats[0].Globbed) <= 0 { - errs := make([]string, 0, len(stats)) - - for _, stat := range stats { - if stat.Error != "" { - errs = append(errs, stat.Error) - } - } - - utils.Error(w, "Not found.", http.StatusNotFound, fmt.Errorf("file doesn't exist (errs: %q)", strings.Join(errs, ";"))) - - return - } - - statHeader, err := statsToHeader(stats[0].Results[stats[0].Globbed[0]]) - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } - w.Header().Add("X-Docker-Container-Path-Stat", statHeader) - if r.Method == http.MethodGet { - idMappingOpts, err := ctr.IDMappings() - if err != nil { - utils.Error(w, "Not found.", http.StatusInternalServerError, - errors.Wrapf(err, "error getting IDMappingOptions")) - return - } - - destOwner := idtools.IDPair{UID: os.Getuid(), GID: os.Getgid()} - - opts := copier.GetOptions{ - UIDMap: idMappingOpts.UIDMap, - GIDMap: idMappingOpts.GIDMap, - ChownDirs: &destOwner, - ChownFiles: &destOwner, - KeepDirectoryNames: true, - } - - w.WriteHeader(http.StatusOK) - - err = copier.Get(mountPoint, "", opts, []string{filepath.Join(mountPoint, path)}, w) - if err != nil { - logrus.Error(errors.Wrapf(err, "failed to copy from the %s container path %s", containerName, query.Path)) - return - } - } else { + // Our work is done when the user is interested in the header only. + if r.Method == http.MethodHead { w.WriteHeader(http.StatusOK) - } -} - -func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { - query := struct { - Path string `schema:"path"` - // TODO handle params below - NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"` - CopyUIDGID bool `schema:"copyUIDGID"` - }{} - - err := decoder.Decode(&query, r.URL.Query()) - if err != nil { - utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.Wrap(err, "couldn't decode the query")) return } - ctrName := utils.GetName(r) - - ctr, err := runtime.LookupContainer(ctrName) - if err != nil { - utils.Error(w, "Not found", http.StatusNotFound, errors.Wrapf(err, "the %s container doesn't exists", ctrName)) - return - } - - mountPoint, err := ctr.Mount() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, errors.Wrapf(err, "failed to mount the %s container", ctrName)) - return - } - - defer func() { - if err := ctr.Unmount(false); err != nil { - logrus.Warnf("failed to unmount container %s", ctrName) - } - }() - - user, err := getUser(mountPoint, ctr.User()) - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) - return - } - - idMappingOpts, err := ctr.IDMappings() - if err != nil { - utils.Error(w, "Something went wrong", http.StatusInternalServerError, errors.Wrapf(err, "error getting IDMappingOptions")) - return - } - - destOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)} - - opts := copier.PutOptions{ - UIDMap: idMappingOpts.UIDMap, - GIDMap: idMappingOpts.GIDMap, - ChownDirs: &destOwner, - ChownFiles: &destOwner, - } - - mountPoint, path, err := fixUpMountPointAndPath(runtime, ctr, mountPoint, query.Path) + // Alright, the users wants data from the container. + destination, err := copy.CopyItemForWriter(w) if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } w.WriteHeader(http.StatusOK) - - err = copier.Put(mountPoint, filepath.Join(mountPoint, path), opts, r.Body) - if err != nil { - logrus.Error(errors.Wrapf(err, "failed to copy to the %s container path %s", ctrName, query.Path)) + if err := copy.Copy(&source, &destination, false); err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return } } -func statsToHeader(stats *copier.StatForItem) (string, error) { - statsDTO := struct { +func fileInfoToDockerStats(info *copy.FileInfo) (string, error) { + dockerStats := struct { Name string `json:"name"` Size int64 `json:"size"` Mode os.FileMode `json:"mode"` ModTime time.Time `json:"mtime"` LinkTarget string `json:"linkTarget"` }{ - Name: filepath.Base(stats.Name), - Size: stats.Size, - Mode: stats.Mode, - ModTime: stats.ModTime, - LinkTarget: stats.ImmediateTarget, + Name: info.Name, + Size: info.Size, + Mode: info.Mode, + ModTime: info.ModTime, + LinkTarget: info.LinkTarget, } - jsonBytes, err := json.Marshal(&statsDTO) + jsonBytes, err := json.Marshal(&dockerStats) if err != nil { return "", errors.Wrap(err, "failed to serialize file stats") } @@ -250,130 +134,45 @@ func statsToHeader(stats *copier.StatForItem) (string, error) { return buff.String(), nil } -// the utility functions below are copied from abi/cp.go - -func getUser(mountPoint string, userspec string) (specs.User, error) { - uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec) - u := specs.User{ - UID: uid, - GID: gid, - Username: userspec, - } - - if !strings.Contains(userspec, ":") { - groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) - if err2 != nil { - if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil { - err = err2 - } - } else { - u.AdditionalGids = groups - } - } - - return u, err -} - -func fixUpMountPointAndPath(runtime *libpod.Runtime, ctr *libpod.Container, mountPoint, ctrPath string) (string, string, error) { - if !filepath.IsAbs(ctrPath) { - endsWithSep := strings.HasSuffix(ctrPath, string(filepath.Separator)) - ctrPath = filepath.Join(ctr.WorkingDir(), ctrPath) - - if endsWithSep { - ctrPath = ctrPath + string(filepath.Separator) - } - } - if isVol, volDestName, volName := isVolumeDestName(ctrPath, ctr); isVol { //nolint(gocritic) - newMountPoint, path, err := pathWithVolumeMount(runtime, volDestName, volName, ctrPath) - if err != nil { - return "", "", errors.Wrapf(err, "error getting source path from volume %s", volDestName) - } - - mountPoint = newMountPoint - ctrPath = path - } else if isBindMount, mount := isBindMountDestName(ctrPath, ctr); isBindMount { //nolint(gocritic) - newMountPoint, path := pathWithBindMountSource(mount, ctrPath) - mountPoint = newMountPoint - ctrPath = path - } - - return mountPoint, ctrPath, nil -} - -func isVolumeDestName(path string, ctr *libpod.Container) (bool, string, string) { - separator := string(os.PathSeparator) - - if filepath.IsAbs(path) { - path = strings.TrimPrefix(path, separator) - } - - if path == "" { - return false, "", "" - } - - for _, vol := range ctr.Config().NamedVolumes { - volNamePath := strings.TrimPrefix(vol.Dest, separator) - if matchVolumePath(path, volNamePath) { - return true, vol.Dest, vol.Name - } - } - - return false, "", "" -} +func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { + query := struct { + Path string `schema:"path"` + // TODO handle params below + NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"` + CopyUIDGID bool `schema:"copyUIDGID"` + }{} -func pathWithVolumeMount(runtime *libpod.Runtime, volDestName, volName, path string) (string, string, error) { - destVolume, err := runtime.GetVolume(volName) + err := decoder.Decode(&query, r.URL.Query()) if err != nil { - return "", "", errors.Wrapf(err, "error getting volume destination %s", volName) - } - - if !filepath.IsAbs(path) { - path = filepath.Join(string(os.PathSeparator), path) + utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.Wrap(err, "couldn't decode the query")) + return } - return destVolume.MountPoint(), strings.TrimPrefix(path, volDestName), err -} - -func isBindMountDestName(path string, ctr *libpod.Container) (bool, specs.Mount) { - separator := string(os.PathSeparator) - - if filepath.IsAbs(path) { - path = strings.TrimPrefix(path, string(os.PathSeparator)) - } + ctrName := utils.GetName(r) - if path == "" { - return false, specs.Mount{} + ctr, err := runtime.LookupContainer(ctrName) + if err != nil { + utils.Error(w, "Not found", http.StatusNotFound, errors.Wrapf(err, "the %s container doesn't exists", ctrName)) + return } - for _, m := range ctr.Config().Spec.Mounts { - if m.Type != "bind" { - continue - } - - mDest := strings.TrimPrefix(m.Destination, separator) - if matchVolumePath(path, mDest) { - return true, m - } + destination, err := copy.CopyItemForContainer(ctr, query.Path, true, false) + defer destination.CleanUp() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return } - return false, specs.Mount{} -} - -func matchVolumePath(path, target string) bool { - pathStr := filepath.Clean(path) - target = filepath.Clean(target) - - for len(pathStr) > len(target) && strings.Contains(pathStr, string(os.PathSeparator)) { - pathStr = pathStr[:strings.LastIndex(pathStr, string(os.PathSeparator))] + source, err := copy.CopyItemForReader(r.Body) + defer source.CleanUp() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return } - return pathStr == target -} - -func pathWithBindMountSource(m specs.Mount, path string) (string, string) { - if !filepath.IsAbs(path) { - path = filepath.Join(string(os.PathSeparator), path) + w.WriteHeader(http.StatusOK) + if err := copy.Copy(&source, &destination, false); err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return } - - return m.Source, strings.TrimPrefix(path, m.Destination) } diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 729639928..409a74de2 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -37,6 +37,9 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { return } + // Override the container name in the body struct + body.Name = query.Name + if len(body.HostConfig.Links) > 0 { utils.Error(w, utils.ErrLinkNotSupport.Error(), http.StatusBadRequest, errors.Wrapf(utils.ErrLinkNotSupport, "bad parameter")) return @@ -69,9 +72,6 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { return } - // Override the container name in the body struct - body.Name = query.Name - ic := abi.ContainerEngine{Libpod: runtime} report, err := ic.ContainerCreate(r.Context(), sg) if err != nil { diff --git a/pkg/api/handlers/compat/containers_pause.go b/pkg/api/handlers/compat/containers_pause.go index 8712969c0..a7e0a66f1 100644 --- a/pkg/api/handlers/compat/containers_pause.go +++ b/pkg/api/handlers/compat/containers_pause.go @@ -24,5 +24,5 @@ func PauseContainer(w http.ResponseWriter, r *http.Request) { return } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/containers_restart.go b/pkg/api/handlers/compat/containers_restart.go index f4d8f06a1..e8928596a 100644 --- a/pkg/api/handlers/compat/containers_restart.go +++ b/pkg/api/handlers/compat/containers_restart.go @@ -41,5 +41,5 @@ func RestartContainer(w http.ResponseWriter, r *http.Request) { } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/containers_start.go b/pkg/api/handlers/compat/containers_start.go index 6236b1357..726da6f99 100644 --- a/pkg/api/handlers/compat/containers_start.go +++ b/pkg/api/handlers/compat/containers_start.go @@ -39,12 +39,12 @@ func StartContainer(w http.ResponseWriter, r *http.Request) { return } if state == define.ContainerStateRunning { - utils.WriteResponse(w, http.StatusNotModified, "") + utils.WriteResponse(w, http.StatusNotModified, nil) return } if err := con.Start(r.Context(), len(con.PodID()) > 0); err != nil { utils.InternalServerError(w, err) return } - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/containers_stop.go b/pkg/api/handlers/compat/containers_stop.go index 13fe25338..8bc58cf59 100644 --- a/pkg/api/handlers/compat/containers_stop.go +++ b/pkg/api/handlers/compat/containers_stop.go @@ -40,7 +40,7 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { } // If the Container is stopped already, send a 304 if state == define.ContainerStateStopped || state == define.ContainerStateExited { - utils.WriteResponse(w, http.StatusNotModified, "") + utils.WriteResponse(w, http.StatusNotModified, nil) return } @@ -56,5 +56,5 @@ func StopContainer(w http.ResponseWriter, r *http.Request) { } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/containers_unpause.go b/pkg/api/handlers/compat/containers_unpause.go index f87b95b64..760e85814 100644 --- a/pkg/api/handlers/compat/containers_unpause.go +++ b/pkg/api/handlers/compat/containers_unpause.go @@ -24,5 +24,5 @@ func UnpauseContainer(w http.ResponseWriter, r *http.Request) { } // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index 149050209..43478c1d3 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -263,7 +263,7 @@ loop: failed = true m.Error = string(e) if err := enc.Encode(m); err != nil { - logrus.Warnf("Failed to json encode error %q", err.Error()) + logrus.Warnf("Failed to json encode error %v", err) } flush() case <-runCtx.Done(): @@ -271,7 +271,7 @@ loop: if !utils.IsLibpodRequest(r) { m.Stream = fmt.Sprintf("Successfully built %12.12s\n", imageID) if err := enc.Encode(m); err != nil { - logrus.Warnf("Failed to json encode error %q", err.Error()) + logrus.Warnf("Failed to json encode error %v", err) } flush() } diff --git a/pkg/api/handlers/compat/images_push.go b/pkg/api/handlers/compat/images_push.go index 12593a68c..0f3da53e8 100644 --- a/pkg/api/handlers/compat/images_push.go +++ b/pkg/api/handlers/compat/images_push.go @@ -81,5 +81,4 @@ func PushImage(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, "") - } diff --git a/pkg/api/handlers/compat/networks.go b/pkg/api/handlers/compat/networks.go index b4f3aa2f1..fe13971b0 100644 --- a/pkg/api/handlers/compat/networks.go +++ b/pkg/api/handlers/compat/networks.go @@ -271,11 +271,16 @@ func CreateNetwork(w http.ResponseWriter, r *http.Request) { return } + net, err := getNetworkResourceByNameOrID(name, runtime, nil) + if err != nil { + utils.InternalServerError(w, err) + return + } body := struct { Id string Warning []string }{ - Id: name, + Id: net.ID, } utils.WriteResponse(w, http.StatusCreated, body) } @@ -320,7 +325,7 @@ func RemoveNetwork(w http.ResponseWriter, r *http.Request) { return } - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } // Connect adds a container to a network diff --git a/pkg/api/handlers/compat/ping.go b/pkg/api/handlers/compat/ping.go index 9f6611b30..5513e902e 100644 --- a/pkg/api/handlers/compat/ping.go +++ b/pkg/api/handlers/compat/ping.go @@ -15,6 +15,7 @@ import ( func Ping(w http.ResponseWriter, r *http.Request) { // Note API-Version and Libpod-API-Version are set in handler_api.go w.Header().Set("BuildKit-Version", "") + w.Header().Set("Builder-Version", "") w.Header().Set("Docker-Experimental", "true") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Pragma", "no-cache") diff --git a/pkg/api/handlers/compat/system.go b/pkg/api/handlers/compat/system.go index 322bfa7ed..e21ae160a 100644 --- a/pkg/api/handlers/compat/system.go +++ b/pkg/api/handlers/compat/system.go @@ -2,17 +2,91 @@ package compat import ( "net/http" + "strings" + "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/pkg/api/handlers" "github.com/containers/podman/v2/pkg/api/handlers/utils" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/podman/v2/pkg/domain/infra/abi" docker "github.com/docker/docker/api/types" ) func GetDiskUsage(w http.ResponseWriter, r *http.Request) { + options := entities.SystemDfOptions{} + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ic := abi.ContainerEngine{Libpod: runtime} + df, err := ic.SystemDf(r.Context(), options) + if err != nil { + utils.InternalServerError(w, err) + } + + imgs := make([]*docker.ImageSummary, len(df.Images)) + for i, o := range df.Images { + t := docker.ImageSummary{ + Containers: int64(o.Containers), + Created: o.Created.Unix(), + ID: o.ImageID, + Labels: map[string]string{}, + ParentID: "", + RepoDigests: nil, + RepoTags: []string{o.Tag}, + SharedSize: o.SharedSize, + Size: o.Size, + VirtualSize: o.Size - o.UniqueSize, + } + imgs[i] = &t + } + + ctnrs := make([]*docker.Container, len(df.Containers)) + for i, o := range df.Containers { + t := docker.Container{ + ID: o.ContainerID, + Names: []string{o.Names}, + Image: o.Image, + ImageID: o.Image, + Command: strings.Join(o.Command, " "), + Created: o.Created.Unix(), + Ports: nil, + SizeRw: o.RWSize, + SizeRootFs: o.Size, + Labels: map[string]string{}, + State: o.Status, + Status: o.Status, + HostConfig: struct { + NetworkMode string `json:",omitempty"` + }{}, + NetworkSettings: nil, + Mounts: nil, + } + ctnrs[i] = &t + } + + vols := make([]*docker.Volume, len(df.Volumes)) + for i, o := range df.Volumes { + t := docker.Volume{ + CreatedAt: "", + Driver: "", + Labels: map[string]string{}, + Mountpoint: "", + Name: o.VolumeName, + Options: nil, + Scope: "local", + Status: nil, + UsageData: &docker.VolumeUsageData{ + RefCount: 1, + Size: o.Size, + }, + } + vols[i] = &t + } + utils.WriteResponse(w, http.StatusOK, handlers.DiskUsage{DiskUsage: docker.DiskUsage{ - LayersSize: 0, - Images: nil, - Containers: nil, - Volumes: nil, + LayersSize: 0, + Images: imgs, + Containers: ctnrs, + Volumes: vols, + BuildCache: []*docker.BuildCache{}, + BuilderSize: 0, }}) } diff --git a/pkg/api/handlers/compat/volumes.go b/pkg/api/handlers/compat/volumes.go index a3c9fbd2f..71b848932 100644 --- a/pkg/api/handlers/compat/volumes.go +++ b/pkg/api/handlers/compat/volumes.go @@ -223,7 +223,7 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { } } else { // Success - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } } else { if !query.Force { @@ -232,7 +232,7 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { // Volume does not exist and `force` is truthy - this emulates what // Docker would do when told to `force` removal of a nonextant // volume - utils.WriteResponse(w, http.StatusNoContent, "") + utils.WriteResponse(w, http.StatusNoContent, nil) } } } diff --git a/pkg/api/handlers/libpod/generate.go b/pkg/api/handlers/libpod/generate.go index 33bb75391..b3b8c1f16 100644 --- a/pkg/api/handlers/libpod/generate.go +++ b/pkg/api/handlers/libpod/generate.go @@ -60,7 +60,8 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - Service bool `schema:"service"` + Names []string `schema:"names"` + Service bool `schema:"service"` }{ // Defaults would go here. } @@ -73,7 +74,7 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) { containerEngine := abi.ContainerEngine{Libpod: runtime} options := entities.GenerateKubeOptions{Service: query.Service} - report, err := containerEngine.GenerateKube(r.Context(), utils.GetName(r), options) + report, err := containerEngine.GenerateKube(r.Context(), query.Names, options) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error generating YAML")) return diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 40cf16807..c9adde09d 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -145,13 +145,14 @@ type PodCreateConfig struct { Share string `json:"share"` } +// HistoryResponse provides details on image layers type HistoryResponse struct { - ID string `json:"Id"` - Created int64 `json:"Created"` - CreatedBy string `json:"CreatedBy"` - Tags []string `json:"Tags"` - Size int64 `json:"Size"` - Comment string `json:"Comment"` + ID string `json:"Id"` + Created int64 + CreatedBy string + Tags []string + Size int64 + Comment string } type ImageLayer struct{} @@ -177,55 +178,34 @@ type ExecStartConfig struct { } func ImageToImageSummary(l *libpodImage.Image) (*entities.ImageSummary, error) { - containers, err := l.Containers() - if err != nil { - return nil, errors.Wrapf(err, "failed to obtain Containers for image %s", l.ID()) - } - containerCount := len(containers) - - // FIXME: GetParent() panics - // parent, err := l.GetParent(context.TODO()) - // if err != nil { - // return nil, errors.Wrapf(err, "failed to obtain ParentID for image %s", l.ID()) - // } - - labels, err := l.Labels(context.TODO()) - if err != nil { - return nil, errors.Wrapf(err, "failed to obtain Labels for image %s", l.ID()) - } - - size, err := l.Size(context.TODO()) + imageData, err := l.Inspect(context.TODO()) if err != nil { - return nil, errors.Wrapf(err, "failed to obtain Size for image %s", l.ID()) + return nil, errors.Wrapf(err, "failed to obtain summary for image %s", l.ID()) } - repoTags, err := l.RepoTags() + containers, err := l.Containers() if err != nil { - return nil, errors.Wrapf(err, "failed to obtain RepoTags for image %s", l.ID()) - } - - digests := make([]string, len(l.Digests())) - for i, d := range l.Digests() { - digests[i] = string(d) + return nil, errors.Wrapf(err, "failed to obtain Containers for image %s", l.ID()) } + containerCount := len(containers) is := entities.ImageSummary{ ID: l.ID(), - ParentId: l.Parent, - RepoTags: repoTags, + ParentId: imageData.Parent, + RepoTags: imageData.RepoTags, + RepoDigests: imageData.RepoDigests, Created: l.Created().Unix(), - Size: int64(*size), + Size: imageData.Size, SharedSize: 0, - VirtualSize: l.VirtualSize, - Labels: labels, + VirtualSize: imageData.VirtualSize, + Labels: imageData.Labels, Containers: containerCount, ReadOnly: l.IsReadOnly(), Dangling: l.Dangling(), Names: l.Names(), - Digest: string(l.Digest()), - Digests: digests, + Digest: string(imageData.Digest), ConfigDigest: string(l.ConfigDigest), - History: l.NamesHistory(), + History: imageData.NamesHistory, } return &is, nil } @@ -282,8 +262,8 @@ func ImageDataToImageInspect(ctx context.Context, l *libpodImage.Image) (*ImageI } } dockerImageInspect := docker.ImageInspect{ - Architecture: l.Architecture, - Author: l.Author, + Architecture: info.Architecture, + Author: info.Author, Comment: info.Comment, Config: &config, Created: l.Created().Format(time.RFC3339Nano), @@ -291,9 +271,9 @@ func ImageDataToImageInspect(ctx context.Context, l *libpodImage.Image) (*ImageI GraphDriver: docker.GraphDriverData{}, ID: fmt.Sprintf("sha256:%s", l.ID()), Metadata: docker.ImageMetadata{}, - Os: l.Os, - OsVersion: l.Version, - Parent: l.Parent, + Os: info.Os, + OsVersion: info.Version, + Parent: info.Parent, RepoDigests: info.RepoDigests, RepoTags: info.RepoTags, RootFS: rootfs, @@ -329,7 +309,6 @@ func ImageDataToImageInspect(ctx context.Context, l *libpodImage.Image) (*ImageI dockerImageInspect.Parent = d.Parent.String() } return &ImageInspect{dockerImageInspect}, nil - } // portsToPortSet converts libpods exposed ports to dockers structs diff --git a/pkg/api/server/register_generate.go b/pkg/api/server/register_generate.go index 60e5b03f7..bce5484ab 100644 --- a/pkg/api/server/register_generate.go +++ b/pkg/api/server/register_generate.go @@ -70,7 +70,7 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/generate/{name:.*}/systemd"), s.APIHandler(libpod.GenerateSystemd)).Methods(http.MethodGet) - // swagger:operation GET /libpod/generate/{name:.*}/kube libpod libpodGenerateKube + // swagger:operation GET /libpod/generate/kube libpod libpodGenerateKube // --- // tags: // - containers @@ -78,9 +78,11 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { // summary: Generate a Kubernetes YAML file. // description: Generate Kubernetes YAML based on a pod or container. // parameters: - // - in: path - // name: name:.* - // type: string + // - in: query + // name: names + // type: array + // items: + // type: string // required: true // description: Name or ID of the container or pod. // - in: query @@ -98,6 +100,6 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { // format: binary // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/generate/{name:.*}/kube"), s.APIHandler(libpod.GenerateKube)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/libpod/generate/kube"), s.APIHandler(libpod.GenerateKube)).Methods(http.MethodGet) return nil } diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 31435ae91..f2cb3147c 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -152,7 +152,7 @@ func pingNewConnection(ctx context.Context) error { return err } // the ping endpoint sits at / in this case - response, err := client.DoRequest(nil, http.MethodGet, "../../../_ping", nil, nil) + response, err := client.DoRequest(nil, http.MethodGet, "/_ping", nil, nil) if err != nil { return err } @@ -207,11 +207,11 @@ func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) ( authMethods = append(authMethods, ssh.Password(pw)) } if len(authMethods) == 0 { - pass, err := terminal.ReadPassword("Login password:") - if err != nil { - return Connection{}, err + callback := func() (string, error) { + pass, err := terminal.ReadPassword("Login password:") + return string(pass), err } - authMethods = append(authMethods, ssh.Password(string(pass))) + authMethods = append(authMethods, ssh.PasswordCallback(callback)) } port := _url.Port() diff --git a/pkg/bindings/containers/attach.go b/pkg/bindings/containers/attach.go index 7b321af93..91b155fc4 100644 --- a/pkg/bindings/containers/attach.go +++ b/pkg/bindings/containers/attach.go @@ -332,7 +332,7 @@ func attachHandleResize(ctx, winCtx context.Context, winChange chan os.Signal, i case <-winChange: h, w, err := terminal.GetSize(int(file.Fd())) if err != nil { - logrus.Warnf("failed to obtain TTY size: " + err.Error()) + logrus.Warnf("failed to obtain TTY size: %v", err) } var resizeErr error @@ -342,7 +342,7 @@ func attachHandleResize(ctx, winCtx context.Context, winChange chan os.Signal, i resizeErr = ResizeContainerTTY(ctx, id, &h, &w) } if resizeErr != nil { - logrus.Warnf("failed to resize TTY: " + resizeErr.Error()) + logrus.Warnf("failed to resize TTY: %v", err) } } } diff --git a/pkg/bindings/generate/generate.go b/pkg/bindings/generate/generate.go index dde1cc29c..8d0146ec1 100644 --- a/pkg/bindings/generate/generate.go +++ b/pkg/bindings/generate/generate.go @@ -2,6 +2,7 @@ package generate import ( "context" + "errors" "net/http" "net/url" "strconv" @@ -37,15 +38,21 @@ func Systemd(ctx context.Context, nameOrID string, options entities.GenerateSyst return report, response.Process(&report.Units) } -func Kube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { +func Kube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } + if len(nameOrIDs) < 1 { + return nil, errors.New("must provide the name or ID of one container or pod") + } params := url.Values{} + for _, name := range nameOrIDs { + params.Add("names", name) + } params.Set("service", strconv.FormatBool(options.Service)) - response, err := conn.DoRequest(nil, http.MethodGet, "/generate/%s/kube", params, nil, nameOrID) + response, err := conn.DoRequest(nil, http.MethodGet, "/generate/kube", params, nil) if err != nil { return nil, err } diff --git a/pkg/copy/copy.go b/pkg/copy/copy.go new file mode 100644 index 000000000..0e68eb450 --- /dev/null +++ b/pkg/copy/copy.go @@ -0,0 +1,188 @@ +package copy + +import ( + "io" + "os" + "path/filepath" + "strings" + + buildahCopiah "github.com/containers/buildah/copier" + "github.com/containers/storage/pkg/archive" + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/pkg/errors" +) + +// ********************************* NOTE ************************************* +// +// Most security bugs are caused by attackers playing around with symlinks +// trying to escape from the container onto the host and/or trick into data +// corruption on the host. Hence, file operations on containers (including +// *stat) should always be handled by `github.com/containers/buildah/copier` +// which makes sure to evaluate files in a chroot'ed environment. +// +// Please make sure to add verbose comments when changing code to make the +// lives of future readers easier. +// +// **************************************************************************** + +// Copy the source item to destination. Use extract to untar the source if +// it's a tar archive. +func Copy(source *CopyItem, destination *CopyItem, extract bool) error { + // First, do the man-page dance. See podman-cp(1) for details. + if err := enforceCopyRules(source, destination); err != nil { + return err + } + + // Destination is a stream (e.g., stdout or an http body). + if destination.info.IsStream { + // Source is a stream (e.g., stdin or an http body). + if source.info.IsStream { + _, err := io.Copy(destination.writer, source.reader) + return err + } + root, glob, err := source.buildahGlobs() + if err != nil { + return err + } + return buildahCopiah.Get(root, "", source.getOptions(), []string{glob}, destination.writer) + } + + // Destination is either a file or a directory. + if source.info.IsStream { + return buildahCopiah.Put(destination.root, destination.resolved, source.putOptions(), source.reader) + } + + tarOptions := &archive.TarOptions{ + Compression: archive.Uncompressed, + CopyPass: true, + } + + root := destination.root + dir := destination.resolved + if !source.info.IsDir { + // When copying a file, make sure to rename the + // destination base path. + nameMap := make(map[string]string) + nameMap[filepath.Base(source.resolved)] = filepath.Base(destination.resolved) + tarOptions.RebaseNames = nameMap + dir = filepath.Dir(dir) + } + + var tarReader io.ReadCloser + if extract && archive.IsArchivePath(source.resolved) { + if !destination.info.IsDir { + return errors.Errorf("cannot extract archive %q to file %q", source.original, destination.original) + } + + reader, err := os.Open(source.resolved) + if err != nil { + return err + } + defer reader.Close() + + // The stream from stdin may be compressed (e.g., via gzip). + decompressedStream, err := archive.DecompressStream(reader) + if err != nil { + return err + } + + defer decompressedStream.Close() + tarReader = decompressedStream + } else { + reader, err := archive.TarWithOptions(source.resolved, tarOptions) + if err != nil { + return err + } + defer reader.Close() + tarReader = reader + } + + return buildahCopiah.Put(root, dir, source.putOptions(), tarReader) +} + +// enforceCopyRules enforces the rules for copying from a source to a +// destination as mentioned in the podman-cp(1) man page. Please refer to the +// man page and/or the inline comments for further details. Note that source +// and destination are passed by reference and the their data may be changed. +func enforceCopyRules(source, destination *CopyItem) error { + if source.statError != nil { + return source.statError + } + + // We can copy everything to a stream. + if destination.info.IsStream { + return nil + } + + // Source is a *stream*. + if source.info.IsStream { + if !(destination.info.IsDir || destination.info.IsStream) { + return errors.New("destination must be a directory or stream when copying from a stream") + } + return nil + } + + // Source is a *directory*. + if source.info.IsDir { + if destination.statError != nil { + // It's okay if the destination does not exist. We + // made sure before that it's parent exists, so it + // would be created while copying. + if os.IsNotExist(destination.statError) { + return nil + } + // Could be a permission error. + return destination.statError + } + + // If the destination exists and is not a directory, we have a + // problem. + if !destination.info.IsDir { + return errors.Errorf("cannot copy directory %q to file %q", source.original, destination.original) + } + + // If the destination exists and is a directory, we need to + // append the source base directory to it. This makes sure + // that copying "/foo/bar" "/tmp" will copy to "/tmp/bar" (and + // not "/tmp"). + newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved)) + if err != nil { + return err + } + destination.resolved = newDestination + return nil + } + + // Source is a *file*. + if destination.statError != nil { + // It's okay if the destination does not exist, unless it ends + // with "/". + if !os.IsNotExist(destination.statError) { + return destination.statError + } else if strings.HasSuffix(destination.resolved, "/") { + // Note: this is practically unreachable code as the + // existence of parent directories is enforced early + // on. It's left here as an extra security net. + return errors.Errorf("destination directory %q must exist (trailing %q)", destination.original, "/") + } + // Does not exist and does not end with "/". + return nil + } + + // If the destination is a file, we're good. We will overwrite the + // contents while copying. + if !destination.info.IsDir { + return nil + } + + // If the destination exists and is a directory, we need to append the + // source base directory to it. This makes sure that copying + // "/foo/bar" "/tmp" will copy to "/tmp/bar" (and not "/tmp"). + newDestination, err := securejoin.SecureJoin(destination.resolved, filepath.Base(source.resolved)) + if err != nil { + return err + } + + destination.resolved = newDestination + return nil +} diff --git a/pkg/copy/item.go b/pkg/copy/item.go new file mode 100644 index 000000000..db6bca610 --- /dev/null +++ b/pkg/copy/item.go @@ -0,0 +1,601 @@ +package copy + +import ( + "io" + "os" + "path/filepath" + "strings" + "time" + + buildahCopiah "github.com/containers/buildah/copier" + "github.com/containers/buildah/pkg/chrootuser" + "github.com/containers/buildah/util" + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/pkg/cgroups" + "github.com/containers/podman/v2/pkg/rootless" + "github.com/containers/storage" + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ********************************* NOTE ************************************* +// +// Most security bugs are caused by attackers playing around with symlinks +// trying to escape from the container onto the host and/or trick into data +// corruption on the host. Hence, file operations on containers (including +// *stat) should always be handled by `github.com/containers/buildah/copier` +// which makes sure to evaluate files in a chroot'ed environment. +// +// Please make sure to add verbose comments when changing code to make the +// lives of future readers easier. +// +// **************************************************************************** + +var ( + _stdin = os.Stdin.Name() + _stdout = os.Stdout.Name() +) + +// CopyItem is the source or destination of a copy operation. Use the +// CopyItemFrom* functions to create one for the specific source/destination +// item. +type CopyItem struct { + // The original path provided by the caller. Useful in error messages. + original string + // The resolved path on the host or container. Maybe altered at + // multiple stages when copying. + resolved string + // The root for copying data in a chroot'ed environment. + root string + + // IDPair of the resolved path. + idPair *idtools.IDPair + // Storage ID mappings. + idMappings *storage.IDMappingOptions + + // Internal FileInfo. We really don't want users to mess with a + // CopyItem but only plug and play with it. + info FileInfo + // Error when creating the upper FileInfo. Some errors are non-fatal, + // for instance, when a destination *base* path does not exist. + statError error + + writer io.Writer + reader io.Reader + + // Needed to clean up resources (e.g., unmount a container). + cleanUpFuncs []deferFunc +} + +// deferFunc allows for returning functions that must be deferred at call sites. +type deferFunc func() + +// FileInfo describes a file or directory and is returned by +// (*CopyItem).Stat(). +type FileInfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mode os.FileMode `json:"mode"` + ModTime time.Time `json:"mtime"` + IsDir bool `json:"isDir"` + IsStream bool `json:"isStream"` + LinkTarget string `json:"linkTarget"` +} + +// Stat returns the FileInfo. +func (item *CopyItem) Stat() (*FileInfo, error) { + return &item.info, item.statError +} + +// CleanUp releases resources such as the container mounts. It *must* be +// called even in case of errors. +func (item *CopyItem) CleanUp() { + for _, f := range item.cleanUpFuncs { + f() + } +} + +// CopyItemForWriter returns a CopyItem for the specified io.WriteCloser. Note +// that the returned item can only act as a copy destination. +func CopyItemForWriter(writer io.Writer) (item CopyItem, _ error) { + item.writer = writer + item.info.IsStream = true + return item, nil +} + +// CopyItemForReader returns a CopyItem for the specified io.ReaderCloser. Note +// that the returned item can only act as a copy source. +// +// Note that the specified reader will be auto-decompressed if needed. +func CopyItemForReader(reader io.Reader) (item CopyItem, _ error) { + item.info.IsStream = true + decompressed, err := archive.DecompressStream(reader) + if err != nil { + return item, err + } + item.reader = decompressed + item.cleanUpFuncs = append(item.cleanUpFuncs, func() { + if err := decompressed.Close(); err != nil { + logrus.Errorf("Error closing decompressed reader of copy item: %v", err) + } + }) + return item, nil +} + +// CopyItemForHost creates a CopyItem for the specified host path. It's a +// destination by default. Use isSource to set it as a destination. +// +// Note that callers *must* call (CopyItem).CleanUp(), even in case of errors. +func CopyItemForHost(hostPath string, isSource bool) (item CopyItem, _ error) { + if hostPath == "-" { + if isSource { + hostPath = _stdin + } else { + hostPath = _stdout + } + } + + if hostPath == _stdin { + return CopyItemForReader(os.Stdin) + } + + if hostPath == _stdout { + return CopyItemForWriter(os.Stdout) + } + + // Now do the dance for the host data. + resolvedHostPath, err := filepath.Abs(hostPath) + if err != nil { + return item, err + } + + resolvedHostPath = preserveBasePath(hostPath, resolvedHostPath) + item.original = hostPath + item.resolved = resolvedHostPath + item.root = "/" + + statInfo, statError := os.Stat(resolvedHostPath) + item.statError = statError + + // It exists, we're done. + if statError == nil { + item.info.Name = statInfo.Name() + item.info.Size = statInfo.Size() + item.info.Mode = statInfo.Mode() + item.info.ModTime = statInfo.ModTime() + item.info.IsDir = statInfo.IsDir() + item.info.LinkTarget = resolvedHostPath + return item, nil + } + + // The source must exist, but let's try to give some human-friendly + // errors. + if isSource { + if os.IsNotExist(item.statError) { + return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on the host", hostPath) + } + return item, item.statError // could be a permission error + } + + // If we're a destination, we need to make sure that the parent + // directory exists. + parent := filepath.Dir(resolvedHostPath) + if _, err := os.Stat(parent); err != nil { + if os.IsNotExist(err) { + return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on the host", parent) + } + return item, err + } + + return item, nil +} + +// CopyItemForContainer creates a CopyItem for the specified path on the +// container. It's a destination by default. Use isSource to set it as a +// destination. Note that the container path may resolve to a path outside of +// the container's mount point if the path hits a volume or mount on the +// container. +// +// Note that callers *must* call (CopyItem).CleanUp(), even in case of errors. +func CopyItemForContainer(container *libpod.Container, containerPath string, pause bool, isSource bool) (item CopyItem, _ error) { + // Mount and pause the container. + containerMountPoint, err := item.mountAndPauseContainer(container, pause) + if err != nil { + return item, err + } + + // Make sure that "/" copies the *contents* of the mount point and not + // the directory. + if containerPath == "/" { + containerPath += "/." + } + + // Now resolve the container's path. It may hit a volume, it may hit a + // bind mount, it may be relative. + resolvedRoot, resolvedContainerPath, err := resolveContainerPaths(container, containerMountPoint, containerPath) + if err != nil { + return item, err + } + resolvedContainerPath = preserveBasePath(containerPath, resolvedContainerPath) + + idMappings, idPair, err := getIDMappingsAndPair(container, containerMountPoint) + if err != nil { + return item, err + } + + item.original = containerPath + item.resolved = resolvedContainerPath + item.root = resolvedRoot + item.idMappings = idMappings + item.idPair = idPair + + statInfo, statError := secureStat(resolvedRoot, resolvedContainerPath) + item.statError = statError + + // It exists, we're done. + if statError == nil { + item.info.IsDir = statInfo.IsDir + item.info.Name = filepath.Base(statInfo.Name) + item.info.Size = statInfo.Size + item.info.Mode = statInfo.Mode + item.info.ModTime = statInfo.ModTime + item.info.IsDir = statInfo.IsDir + item.info.LinkTarget = resolvedContainerPath + return item, nil + } + + // The source must exist, but let's try to give some human-friendly + // errors. + if isSource { + if os.IsNotExist(statError) { + return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on container %s (resolved to %q)", containerPath, container.ID(), resolvedContainerPath) + } + return item, item.statError // could be a permission error + } + + // If we're a destination, we need to make sure that the parent + // directory exists. + parent := filepath.Dir(resolvedContainerPath) + if _, err := secureStat(resolvedRoot, parent); err != nil { + if os.IsNotExist(err) { + return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on container %s (resolved to %q)", containerPath, container.ID(), resolvedContainerPath) + } + return item, err + } + + return item, nil +} + +// putOptions returns PUT options for buildah's copier package. +func (item *CopyItem) putOptions() buildahCopiah.PutOptions { + options := buildahCopiah.PutOptions{} + if item.idMappings != nil { + options.UIDMap = item.idMappings.UIDMap + options.GIDMap = item.idMappings.GIDMap + } + if item.idPair != nil { + options.ChownDirs = item.idPair + options.ChownFiles = item.idPair + } + return options +} + +// getOptions returns GET options for buildah's copier package. +func (item *CopyItem) getOptions() buildahCopiah.GetOptions { + options := buildahCopiah.GetOptions{} + if item.idMappings != nil { + options.UIDMap = item.idMappings.UIDMap + options.GIDMap = item.idMappings.GIDMap + } + if item.idPair != nil { + options.ChownDirs = item.idPair + options.ChownFiles = item.idPair + } + return options + +} + +// mount and pause the container. Also set the item's cleanUpFuncs. Those +// *must* be invoked by callers, even in case of errors. +func (item *CopyItem) mountAndPauseContainer(container *libpod.Container, pause bool) (string, error) { + // Make sure to pause and unpause the container. We cannot pause on + // cgroupsv1 as rootless user, in which case we turn off pausing. + if pause && rootless.IsRootless() { + cgroupv2, _ := cgroups.IsCgroup2UnifiedMode() + if !cgroupv2 { + logrus.Debugf("Cannot pause container for copying as a rootless user on cgroupsv1: default to not pause") + pause = false + } + } + + // Mount and unmount the container. + mountPoint, err := container.Mount() + if err != nil { + return "", err + } + + item.cleanUpFuncs = append(item.cleanUpFuncs, func() { + if err := container.Unmount(false); err != nil { + logrus.Errorf("Error unmounting container after copy operation: %v", err) + } + }) + + // Pause and unpause the container. + if pause { + if err := container.Pause(); err != nil { + // Ignore errors when the container isn't running. No + // need to pause. + if errors.Cause(err) != define.ErrCtrStateInvalid { + return "", err + } + } else { + item.cleanUpFuncs = append(item.cleanUpFuncs, func() { + if err := container.Unpause(); err != nil { + logrus.Errorf("Error unpausing container after copy operation: %v", err) + } + }) + } + } + + return mountPoint, nil +} + +// buildahGlobs returns the root, dir and glob used in buildah's copier +// package. +// +// Note that dir is always empty. +func (item *CopyItem) buildahGlobs() (root string, glob string, err error) { + root = item.root + + // If the root and the resolved path are equal, then dir must be empty + // and the glob must be ".". + if filepath.Clean(root) == filepath.Clean(item.resolved) { + glob = "." + return + } + + glob, err = filepath.Rel(root, item.resolved) + return +} + +// preserveBasePath makes sure that the original base path (e.g., "/" or "./") +// is preserved. The filepath API among tends to clean up a bit too much but +// we *must* preserve this data by all means. +func preserveBasePath(original, resolved string) string { + // Handle "/" + if strings.HasSuffix(original, "/") { + if !strings.HasSuffix(resolved, "/") { + resolved += "/" + } + return resolved + } + + // Handle "/." + if strings.HasSuffix(original, "/.") { + if strings.HasSuffix(resolved, "/") { // could be root! + resolved += "." + } else if !strings.HasSuffix(resolved, "/.") { + resolved += "/." + } + return resolved + } + + return resolved +} + +// secureStat extracts file info for path in a chroot'ed environment in root. +func secureStat(root string, path string) (*buildahCopiah.StatForItem, error) { + var glob string + var err error + + // If root and path are equal, then dir must be empty and the glob must + // be ".". + if filepath.Clean(root) == filepath.Clean(path) { + glob = "." + } else { + glob, err = filepath.Rel(root, path) + if err != nil { + return nil, err + } + } + + globStats, err := buildahCopiah.Stat(root, "", buildahCopiah.StatOptions{}, []string{glob}) + if err != nil { + return nil, err + } + + if len(globStats) != 1 { + return nil, errors.Errorf("internal libpod error: secureStat: expected 1 item but got %d", len(globStats)) + } + + stat, exists := globStats[0].Results[glob] // only one glob passed, so that's okay + if !exists { + return stat, os.ErrNotExist + } + + var statErr error + if stat.Error != "" { + statErr = errors.New(stat.Error) + } + return stat, statErr +} + +// resolveContainerPaths resolves the container's mount point and the container +// path as specified by the user. Both may resolve to paths outside of the +// container's mount point when the container path hits a volume or bind mount. +// +// NOTE: We must take volumes and bind mounts into account as, regrettably, we +// can copy to/from stopped containers. In that case, the volumes and bind +// mounts are not present. For running containers, the runtime (e.g., runc or +// crun) takes care of these mounts. For stopped ones, we need to do quite +// some dance, as done below. +func resolveContainerPaths(container *libpod.Container, mountPoint string, containerPath string) (string, string, error) { + // Let's first make sure we have a path relative to the mount point. + pathRelativeToContainerMountPoint := containerPath + if !filepath.IsAbs(containerPath) { + // If the containerPath is not absolute, it's relative to the + // container's working dir. To be extra careful, let's first + // join the working dir with "/", and the add the containerPath + // to it. + pathRelativeToContainerMountPoint = filepath.Join(filepath.Join("/", container.WorkingDir()), containerPath) + } + // NOTE: the secure join makes sure that we follow symlinks. This way, + // we catch scenarios where the container path symlinks to a volume or + // bind mount. + resolvedPathOnTheContainerMountPoint, err := securejoin.SecureJoin(mountPoint, pathRelativeToContainerMountPoint) + if err != nil { + return "", "", err + } + pathRelativeToContainerMountPoint = strings.TrimPrefix(pathRelativeToContainerMountPoint, mountPoint) + pathRelativeToContainerMountPoint = filepath.Join("/", pathRelativeToContainerMountPoint) + + // Now we have an "absolute container Path" but not yet resolved on the + // host (e.g., "/foo/bar/file.txt"). As mentioned above, we need to + // check if "/foo/bar/file.txt" is on a volume or bind mount. To do + // that, we need to walk *down* the paths to the root. Assuming + // volume-1 is mounted to "/foo" and volume-2 is mounted to "/foo/bar", + // we must select "/foo/bar". Once selected, we need to rebase the + // remainder (i.e, "/file.txt") on the volume's mount point on the + // host. Same applies to bind mounts. + + searchPath := pathRelativeToContainerMountPoint + for { + volume, err := findVolume(container, searchPath) + if err != nil { + return "", "", err + } + if volume != nil { + logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath) + // We found a matching volume for searchPath. We now + // need to first find the relative path of our input + // path to the searchPath, and then join it with the + // volume's mount point. + pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath) + absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(volume.MountPoint(), pathRelativeToVolume) + if err != nil { + return "", "", err + } + return volume.MountPoint(), absolutePathOnTheVolumeMount, nil + } + + if mount := findBindMount(container, searchPath); mount != nil { + logrus.Debugf("Container path %q resolved to bind mount %q:%q on path %q", containerPath, mount.Source, mount.Destination, searchPath) + // We found a matching bind mount for searchPath. We + // now need to first find the relative path of our + // input path to the searchPath, and then join it with + // the source of the bind mount. + pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath) + absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount) + if err != nil { + return "", "", err + } + return mount.Source, absolutePathOnTheBindMount, nil + + } + + if searchPath == "/" { + // Cannot go beyond "/", so we're done. + break + } + + // Walk *down* the path (e.g., "/foo/bar/x" -> "/foo/bar"). + searchPath = filepath.Dir(searchPath) + } + + // No volume, no bind mount but just a normal path on the container. + return mountPoint, resolvedPathOnTheContainerMountPoint, nil +} + +// findVolume checks if the specified container path matches a volume inside +// the container. It returns a matching volume or nil. +func findVolume(c *libpod.Container, containerPath string) (*libpod.Volume, error) { + runtime := c.Runtime() + cleanedContainerPath := filepath.Clean(containerPath) + for _, vol := range c.Config().NamedVolumes { + if cleanedContainerPath == filepath.Clean(vol.Dest) { + return runtime.GetVolume(vol.Name) + } + } + return nil, nil +} + +// findBindMount checks if the specified container path matches a bind mount +// inside the container. It returns a matching mount or nil. +func findBindMount(c *libpod.Container, containerPath string) *specs.Mount { + cleanedPath := filepath.Clean(containerPath) + for _, m := range c.Config().Spec.Mounts { + if m.Type != "bind" { + continue + } + if cleanedPath == filepath.Clean(m.Destination) { + mount := m + return &mount + } + } + return nil +} + +// getIDMappingsAndPair returns the ID mappings for the container and the host +// ID pair. +func getIDMappingsAndPair(container *libpod.Container, containerMount string) (*storage.IDMappingOptions, *idtools.IDPair, error) { + user, err := getContainerUser(container, containerMount) + if err != nil { + return nil, nil, err + } + + idMappingOpts, err := container.IDMappings() + if err != nil { + return nil, nil, err + } + + hostUID, hostGID, err := util.GetHostIDs(idtoolsToRuntimeSpec(idMappingOpts.UIDMap), idtoolsToRuntimeSpec(idMappingOpts.GIDMap), user.UID, user.GID) + if err != nil { + return nil, nil, err + } + + idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)} + return &idMappingOpts, &idPair, nil +} + +// getContainerUser returns the specs.User of the container. +func getContainerUser(container *libpod.Container, mountPoint string) (specs.User, error) { + userspec := container.Config().User + + uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec) + u := specs.User{ + UID: uid, + GID: gid, + Username: userspec, + } + + if !strings.Contains(userspec, ":") { + groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) + if err2 != nil { + if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil { + err = err2 + } + } else { + u.AdditionalGids = groups + } + } + + return u, err +} + +// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec. +func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) { + for _, idmap := range idMaps { + tempIDMap := specs.LinuxIDMapping{ + ContainerID: uint32(idmap.ContainerID), + HostID: uint32(idmap.HostID), + Size: uint32(idmap.Size), + } + convertedIDMap = append(convertedIDMap, tempIDMap) + } + return convertedIDMap +} diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 3fd7c79f4..39d679eaf 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -403,16 +403,14 @@ type ContainerPortReport struct { Ports []ocicni.PortMapping } -// ContainerCpOptions describes input options for cp +// ContainerCpOptions describes input options for cp. type ContainerCpOptions struct { - Pause bool + // Pause the container while copying. + Pause bool + // Extract the tarfile into the destination directory. Extract bool } -// ContainerCpReport describes the output from a cp operation -type ContainerCpReport struct { -} - // ContainerStatsOptions describes input options for getting // stats on containers type ContainerStatsOptions struct { diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index a4f6c08e9..5ad475133 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -16,7 +16,7 @@ type ContainerEngine interface { ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error) ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error) ContainerCommit(ctx context.Context, nameOrID string, options CommitOptions) (*CommitReport, error) - ContainerCp(ctx context.Context, source, dest string, options ContainerCpOptions) (*ContainerCpReport, error) + ContainerCp(ctx context.Context, source, dest string, options ContainerCpOptions) error ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error) ContainerDiff(ctx context.Context, nameOrID string, options DiffOptions) (*DiffReport, error) ContainerExec(ctx context.Context, nameOrID string, options ExecOptions, streams define.AttachStreams) (int, error) @@ -46,7 +46,7 @@ type ContainerEngine interface { ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) Events(ctx context.Context, opts EventsOptions) error GenerateSystemd(ctx context.Context, nameOrID string, opts GenerateSystemdOptions) (*GenerateSystemdReport, error) - GenerateKube(ctx context.Context, nameOrID string, opts GenerateKubeOptions) (*GenerateKubeReport, error) + GenerateKube(ctx context.Context, nameOrIDs []string, opts GenerateKubeOptions) (*GenerateKubeReport, error) SystemPrune(ctx context.Context, options SystemPruneOptions) (*SystemPruneReport, error) HealthCheckRun(ctx context.Context, nameOrID string, options HealthCheckOptions) (*define.HealthCheckResults, error) Info(ctx context.Context) (*define.Info, error) diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index ab545d882..81f12bff7 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -51,22 +51,22 @@ func (i *Image) Id() string { // nolint } type ImageSummary struct { - ID string `json:"Id"` - ParentId string // nolint - RepoTags []string `json:",omitempty"` + ID string `json:"Id"` + ParentId string // nolint + RepoTags []string + RepoDigests []string Created int64 - Size int64 `json:",omitempty"` - SharedSize int `json:",omitempty"` - VirtualSize int64 `json:",omitempty"` - Labels map[string]string `json:",omitempty"` - Containers int `json:",omitempty"` - ReadOnly bool `json:",omitempty"` - Dangling bool `json:",omitempty"` + Size int64 + SharedSize int + VirtualSize int64 + Labels map[string]string + Containers int + ReadOnly bool `json:",omitempty"` + Dangling bool `json:",omitempty"` // Podman extensions Names []string `json:",omitempty"` Digest string `json:",omitempty"` - Digests []string `json:",omitempty"` ConfigDigest string `json:",omitempty"` History []string `json:",omitempty"` } diff --git a/pkg/domain/infra/abi/cp.go b/pkg/domain/infra/abi/cp.go index 8f4f5d3d7..9409df743 100644 --- a/pkg/domain/infra/abi/cp.go +++ b/pkg/domain/infra/abi/cp.go @@ -1,195 +1,70 @@ package abi import ( - "archive/tar" "context" - "fmt" - "io" - "os" - "path/filepath" "strings" - "github.com/containers/buildah/pkg/chrootuser" - "github.com/containers/buildah/util" "github.com/containers/podman/v2/libpod" - "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/pkg/copy" "github.com/containers/podman/v2/pkg/domain/entities" - "github.com/containers/storage" - "github.com/containers/storage/pkg/chrootarchive" - "github.com/containers/storage/pkg/idtools" - securejoin "github.com/cyphar/filepath-securejoin" - "github.com/docker/docker/pkg/archive" - "github.com/opencontainers/go-digest" - "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) -func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) (*entities.ContainerCpReport, error) { - extract := options.Extract - +func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error { srcCtr, srcPath := parsePath(ic.Libpod, source) destCtr, destPath := parsePath(ic.Libpod, dest) - if (srcCtr == nil && destCtr == nil) || (srcCtr != nil && destCtr != nil) { - return nil, errors.Errorf("invalid arguments %s, %s you must use just one container", source, dest) + if srcCtr != nil && destCtr != nil { + return errors.Errorf("invalid arguments %q, %q: you must use just one container", source, dest) } - - if len(srcPath) == 0 || len(destPath) == 0 { - return nil, errors.Errorf("invalid arguments %s, %s you must specify paths", source, dest) + if srcCtr == nil && destCtr == nil { + return errors.Errorf("invalid arguments %q, %q: you must specify one container", source, dest) } - ctr := srcCtr - isFromHostToCtr := ctr == nil - if isFromHostToCtr { - ctr = destCtr + if len(srcPath) == 0 || len(destPath) == 0 { + return errors.Errorf("invalid arguments %q, %q: you must specify paths", source, dest) } - mountPoint, err := ctr.Mount() - if err != nil { - return nil, err - } - defer func() { - if err := ctr.Unmount(false); err != nil { - logrus.Errorf("unable to umount container '%s': %q", ctr.ID(), err) + var sourceItem, destinationItem copy.CopyItem + var err error + // Copy from the container to the host. + if srcCtr != nil { + sourceItem, err = copy.CopyItemForContainer(srcCtr, srcPath, options.Pause, true) + defer sourceItem.CleanUp() + if err != nil { + return err } - }() - - if options.Pause { - if err := ctr.Pause(); err != nil { - // An invalid state error is fine. - // The container isn't running or is already paused. - // TODO: We can potentially start the container while - // the copy is running, which still allows a race where - // malicious code could mess with the symlink. - if errors.Cause(err) != define.ErrCtrStateInvalid { - return nil, err - } - } else { - // Only add the defer if we actually paused - defer func() { - if err := ctr.Unpause(); err != nil { - logrus.Errorf("Error unpausing container after copying: %v", err) - } - }() + } else { + sourceItem, err = copy.CopyItemForHost(srcPath, true) + if err != nil { + return err } } - user, err := getUser(mountPoint, ctr.User()) - if err != nil { - return nil, err - } - idMappingOpts, err := ctr.IDMappings() - if err != nil { - return nil, errors.Wrapf(err, "error getting IDMappingOptions") - } - destOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)} - hostUID, hostGID, err := util.GetHostIDs(convertIDMap(idMappingOpts.UIDMap), convertIDMap(idMappingOpts.GIDMap), user.UID, user.GID) - if err != nil { - return nil, err - } - - hostOwner := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)} - - if isFromHostToCtr { - if isVol, volDestName, volName := isVolumeDestName(destPath, ctr); isVol { //nolint(gocritic) - path, err := pathWithVolumeMount(ic.Libpod, volDestName, volName, destPath) - if err != nil { - return nil, errors.Wrapf(err, "error getting destination path from volume %s", volDestName) - } - destPath = path - } else if isBindMount, mount := isBindMountDestName(destPath, ctr); isBindMount { //nolint(gocritic) - path, err := pathWithBindMountSource(mount, destPath) - if err != nil { - return nil, errors.Wrapf(err, "error getting destination path from bind mount %s", mount.Destination) - } - destPath = path - } else if filepath.IsAbs(destPath) { //nolint(gocritic) - cleanedPath, err := securejoin.SecureJoin(mountPoint, destPath) - if err != nil { - return nil, err - } - destPath = cleanedPath - } else { //nolint(gocritic) - ctrWorkDir, err := securejoin.SecureJoin(mountPoint, ctr.WorkingDir()) - if err != nil { - return nil, err - } - if err = idtools.MkdirAllAndChownNew(ctrWorkDir, 0755, hostOwner); err != nil { - return nil, err - } - cleanedPath, err := securejoin.SecureJoin(mountPoint, filepath.Join(ctr.WorkingDir(), destPath)) - if err != nil { - return nil, err - } - destPath = cleanedPath + if destCtr != nil { + destinationItem, err = copy.CopyItemForContainer(destCtr, destPath, options.Pause, false) + defer destinationItem.CleanUp() + if err != nil { + return err } } else { - destOwner = idtools.IDPair{UID: os.Getuid(), GID: os.Getgid()} - if isVol, volDestName, volName := isVolumeDestName(srcPath, ctr); isVol { //nolint(gocritic) - path, err := pathWithVolumeMount(ic.Libpod, volDestName, volName, srcPath) - if err != nil { - return nil, errors.Wrapf(err, "error getting source path from volume %s", volDestName) - } - srcPath = path - } else if isBindMount, mount := isBindMountDestName(srcPath, ctr); isBindMount { //nolint(gocritic) - path, err := pathWithBindMountSource(mount, srcPath) - if err != nil { - return nil, errors.Wrapf(err, "error getting source path from bind mount %s", mount.Destination) - } - srcPath = path - } else if filepath.IsAbs(srcPath) { //nolint(gocritic) - cleanedPath, err := securejoin.SecureJoin(mountPoint, srcPath) - if err != nil { - return nil, err - } - srcPath = cleanedPath - } else { //nolint(gocritic) - cleanedPath, err := securejoin.SecureJoin(mountPoint, filepath.Join(ctr.WorkingDir(), srcPath)) - if err != nil { - return nil, err - } - srcPath = cleanedPath - } - } - - if !filepath.IsAbs(destPath) { - dir, err := os.Getwd() + destinationItem, err = copy.CopyItemForHost(destPath, false) + defer destinationItem.CleanUp() if err != nil { - return nil, errors.Wrapf(err, "err getting current working directory") + return err } - destPath = filepath.Join(dir, destPath) } - if source == "-" { - srcPath = os.Stdin.Name() - extract = true - } - err = containerCopy(srcPath, destPath, source, dest, idMappingOpts, &destOwner, extract, isFromHostToCtr) - return &entities.ContainerCpReport{}, err + // Copy from the host to the container. + return copy.Copy(&sourceItem, &destinationItem, options.Extract) } -func getUser(mountPoint string, userspec string) (specs.User, error) { - uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec) - u := specs.User{ - UID: uid, - GID: gid, - Username: userspec, +func parsePath(runtime *libpod.Runtime, path string) (*libpod.Container, string) { + if len(path) == 0 { + return nil, "" } - if !strings.Contains(userspec, ":") { - groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) - if err2 != nil { - if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil { - err = err2 - } - } else { - u.AdditionalGids = groups - } - + if path[0] == '.' || path[0] == '/' { // A path cannot point to a container. + return nil, path } - return u, err -} - -func parsePath(runtime *libpod.Runtime, path string) (*libpod.Container, string) { pathArr := strings.SplitN(path, ":", 2) if len(pathArr) == 2 { ctr, err := runtime.LookupContainer(pathArr[0]) @@ -199,247 +74,3 @@ func parsePath(runtime *libpod.Runtime, path string) (*libpod.Container, string) } return nil, path } - -func evalSymlinks(path string) (string, error) { - if path == os.Stdin.Name() { - return path, nil - } - return filepath.EvalSymlinks(path) -} - -func getPathInfo(path string) (string, os.FileInfo, error) { - path, err := evalSymlinks(path) - if err != nil { - return "", nil, errors.Wrapf(err, "error evaluating symlinks %q", path) - } - srcfi, err := os.Stat(path) - if err != nil { - return "", nil, err - } - return path, srcfi, nil -} - -func containerCopy(srcPath, destPath, src, dest string, idMappingOpts storage.IDMappingOptions, chownOpts *idtools.IDPair, extract, isFromHostToCtr bool) error { - srcPath, err := evalSymlinks(srcPath) - if err != nil { - return errors.Wrapf(err, "error evaluating symlinks %q", srcPath) - } - - srcPath, srcfi, err := getPathInfo(srcPath) - if err != nil { - return err - } - - filename := filepath.Base(destPath) - if filename == "-" && !isFromHostToCtr { - err := streamFileToStdout(srcPath, srcfi) - if err != nil { - return errors.Wrapf(err, "error streaming source file %s to Stdout", srcPath) - } - return nil - } - - destdir := destPath - if !srcfi.IsDir() { - destdir = filepath.Dir(destPath) - } - _, err = os.Stat(destdir) - if err != nil && !os.IsNotExist(err) { - return err - } - destDirIsExist := err == nil - if err = os.MkdirAll(destdir, 0755); err != nil { - return err - } - - // return functions for copying items - copyFileWithTar := chrootarchive.CopyFileWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) - copyWithTar := chrootarchive.CopyWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) - untarPath := chrootarchive.UntarPathAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) - - if srcfi.IsDir() { - logrus.Debugf("copying %q to %q", srcPath+string(os.PathSeparator)+"*", dest+string(os.PathSeparator)+"*") - if destDirIsExist && !strings.HasSuffix(src, fmt.Sprintf("%s.", string(os.PathSeparator))) { - srcPathBase := filepath.Base(srcPath) - if !isFromHostToCtr { - pathArr := strings.SplitN(src, ":", 2) - if len(pathArr) != 2 { - return errors.Errorf("invalid arguments %s, you must specify source path", src) - } - if pathArr[1] == "/" { - // If `srcPath` is the root directory of the container, - // `srcPath` will be `.../${sha256_ID}/merged/`, so do not join it - srcPathBase = "" - } - } - destPath = filepath.Join(destPath, srcPathBase) - } - if err = copyWithTar(srcPath, destPath); err != nil { - return errors.Wrapf(err, "error copying %q to %q", srcPath, dest) - } - return nil - } - - if extract { - // We're extracting an archive into the destination directory. - logrus.Debugf("extracting contents of %q into %q", srcPath, destPath) - if err = untarPath(srcPath, destPath); err != nil { - return errors.Wrapf(err, "error extracting %q into %q", srcPath, destPath) - } - return nil - } - - destfi, err := os.Stat(destPath) - if err != nil { - if !os.IsNotExist(err) || strings.HasSuffix(dest, string(os.PathSeparator)) { - return err - } - } - if destfi != nil && destfi.IsDir() { - destPath = filepath.Join(destPath, filepath.Base(srcPath)) - } - - // Copy the file, preserving attributes. - logrus.Debugf("copying %q to %q", srcPath, destPath) - if err = copyFileWithTar(srcPath, destPath); err != nil { - return errors.Wrapf(err, "error copying %q to %q", srcPath, destPath) - } - return nil -} - -func convertIDMap(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) { - for _, idmap := range idMaps { - tempIDMap := specs.LinuxIDMapping{ - ContainerID: uint32(idmap.ContainerID), - HostID: uint32(idmap.HostID), - Size: uint32(idmap.Size), - } - convertedIDMap = append(convertedIDMap, tempIDMap) - } - return convertedIDMap -} - -func streamFileToStdout(srcPath string, srcfi os.FileInfo) error { - if srcfi.IsDir() { - tw := tar.NewWriter(os.Stdout) - err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { - if err != nil || !info.Mode().IsRegular() || path == srcPath { - return err - } - hdr, err := tar.FileInfoHeader(info, "") - if err != nil { - return err - } - - if err = tw.WriteHeader(hdr); err != nil { - return err - } - fh, err := os.Open(path) - if err != nil { - return err - } - defer fh.Close() - - _, err = io.Copy(tw, fh) - return err - }) - if err != nil { - return errors.Wrapf(err, "error streaming directory %s to Stdout", srcPath) - } - return nil - } - - file, err := os.Open(srcPath) - if err != nil { - return err - } - defer file.Close() - if !archive.IsArchivePath(srcPath) { - tw := tar.NewWriter(os.Stdout) - hdr, err := tar.FileInfoHeader(srcfi, "") - if err != nil { - return err - } - err = tw.WriteHeader(hdr) - if err != nil { - return err - } - _, err = io.Copy(tw, file) - if err != nil { - return errors.Wrapf(err, "error streaming archive %s to Stdout", srcPath) - } - return nil - } - - _, err = io.Copy(os.Stdout, file) - if err != nil { - return errors.Wrapf(err, "error streaming file to Stdout") - } - return nil -} - -func isVolumeDestName(path string, ctr *libpod.Container) (bool, string, string) { - separator := string(os.PathSeparator) - if filepath.IsAbs(path) { - path = strings.TrimPrefix(path, separator) - } - if path == "" { - return false, "", "" - } - for _, vol := range ctr.Config().NamedVolumes { - volNamePath := strings.TrimPrefix(vol.Dest, separator) - if matchVolumePath(path, volNamePath) { - return true, vol.Dest, vol.Name - } - } - return false, "", "" -} - -// if SRCPATH or DESTPATH is from volume mount's destination -v or --mount type=volume, generates the path with volume mount point -func pathWithVolumeMount(runtime *libpod.Runtime, volDestName, volName, path string) (string, error) { - destVolume, err := runtime.GetVolume(volName) - if err != nil { - return "", errors.Wrapf(err, "error getting volume destination %s", volName) - } - if !filepath.IsAbs(path) { - path = filepath.Join(string(os.PathSeparator), path) - } - path, err = securejoin.SecureJoin(destVolume.MountPoint(), strings.TrimPrefix(path, volDestName)) - return path, err -} - -func isBindMountDestName(path string, ctr *libpod.Container) (bool, specs.Mount) { - separator := string(os.PathSeparator) - if filepath.IsAbs(path) { - path = strings.TrimPrefix(path, string(os.PathSeparator)) - } - if path == "" { - return false, specs.Mount{} - } - for _, m := range ctr.Config().Spec.Mounts { - if m.Type != "bind" { - continue - } - mDest := strings.TrimPrefix(m.Destination, separator) - if matchVolumePath(path, mDest) { - return true, m - } - } - return false, specs.Mount{} -} - -func matchVolumePath(path, target string) bool { - pathStr := filepath.Clean(path) - target = filepath.Clean(target) - for len(pathStr) > len(target) && strings.Contains(pathStr, string(os.PathSeparator)) { - pathStr = pathStr[:strings.LastIndex(pathStr, string(os.PathSeparator))] - } - return pathStr == target -} - -func pathWithBindMountSource(m specs.Mount, path string) (string, error) { - if !filepath.IsAbs(path) { - path = filepath.Join(string(os.PathSeparator), path) - } - return securejoin.SecureJoin(m.Source, strings.TrimPrefix(path, m.Destination)) -} diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index 79bf2291e..79f55e2bd 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -41,28 +41,48 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, return &entities.GenerateSystemdReport{Units: units}, nil } -func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { +func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { var ( - pod *libpod.Pod + pods []*libpod.Pod podYAML *k8sAPI.Pod err error - ctr *libpod.Container + ctrs []*libpod.Container servicePorts []k8sAPI.ServicePort serviceYAML k8sAPI.Service ) - // Get the container in question. - ctr, err = ic.Libpod.LookupContainer(nameOrID) - if err != nil { - pod, err = ic.Libpod.LookupPod(nameOrID) + for _, nameOrID := range nameOrIDs { + // Get the container in question + ctr, err := ic.Libpod.LookupContainer(nameOrID) if err != nil { - return nil, err + pod, err := ic.Libpod.LookupPod(nameOrID) + if err != nil { + return nil, err + } + pods = append(pods, pod) + if len(pods) > 1 { + return nil, errors.New("can only generate single pod at a time") + } + } else { + if len(ctr.Dependencies()) > 0 { + return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies") + } + // we cannot deal with ctrs already in a pod + if len(ctr.PodID()) > 0 { + return nil, errors.Errorf("container %s is associated with pod %s: use generate on the pod itself", ctr.ID(), ctr.PodID()) + } + ctrs = append(ctrs, ctr) } - podYAML, servicePorts, err = pod.GenerateForKube() + } + + // check our inputs + if len(pods) > 0 && len(ctrs) > 0 { + return nil, errors.New("cannot generate pods and containers at the same time") + } + + if len(pods) == 1 { + podYAML, servicePorts, err = pods[0].GenerateForKube() } else { - if len(ctr.Dependencies()) > 0 { - return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies") - } - podYAML, err = ctr.GenerateForKube() + podYAML, err = libpod.GenerateForKube(ctrs) } if err != nil { return nil, err @@ -72,7 +92,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, op serviceYAML = libpod.GenerateKubeServiceFromV1Pod(podYAML, servicePorts) } - content, err := generateKubeOutput(podYAML, &serviceYAML) + content, err := generateKubeOutput(podYAML, &serviceYAML, options.Service) if err != nil { return nil, err } @@ -80,7 +100,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, op return &entities.GenerateKubeReport{Reader: bytes.NewReader(content)}, nil } -func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service) ([]byte, error) { +func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service, hasService bool) ([]byte, error) { var ( output []byte marshalledPod []byte @@ -93,7 +113,7 @@ func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service) ([]byt return nil, err } - if serviceYAML != nil { + if hasService { marshalledService, err = yaml.Marshal(serviceYAML) if err != nil { return nil, err @@ -114,7 +134,7 @@ func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service) ([]byt output = append(output, []byte(fmt.Sprintf(header, podmanVersion.Version))...) output = append(output, marshalledPod...) - if serviceYAML != nil { + if hasService { output = append(output, []byte("---\n")...) output = append(output, marshalledService...) } diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 1b523f06a..57a2bc4cf 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -26,7 +26,6 @@ import ( "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" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -34,9 +33,6 @@ import ( "github.com/sirupsen/logrus" ) -// SignatureStoreDir defines default directory to store signatures -const SignatureStoreDir = "/var/lib/containers/sigstore" - func (ir *ImageEngine) Exists(_ context.Context, nameOrID string) (*entities.BoolReport, error) { _, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrID) if err != nil { @@ -707,90 +703,79 @@ func (ir *ImageEngine) Sign(ctx context.Context, names []string, options entitie sc := ir.Libpod.SystemContext() sc.DockerCertPath = options.CertDir - systemRegistriesDirPath := trust.RegistriesDirPath(sc) - registryConfigs, err := trust.LoadAndMergeConfig(systemRegistriesDirPath) - if err != nil { - return nil, errors.Wrapf(err, "error reading registry configuration") - } - for _, signimage := range names { - srcRef, err := alltransports.ParseImageName(signimage) - if err != nil { - return nil, errors.Wrapf(err, "error parsing image name") - } - rawSource, err := srcRef.NewImageSource(ctx, sc) - if err != nil { - return nil, errors.Wrapf(err, "error getting image source") - } - err = rawSource.Close() - if err != nil { - logrus.Errorf("unable to close new image source %q", err) - } - getManifest, _, err := rawSource.GetManifest(ctx, nil) - if err != nil { - return nil, errors.Wrapf(err, "error getting getManifest") - } - dockerReference := rawSource.Reference().DockerReference() - if dockerReference == nil { - return nil, errors.Errorf("cannot determine canonical Docker reference for destination %s", transports.ImageName(rawSource.Reference())) - } - var sigStoreDir string - if options.Directory != "" { - sigStoreDir = options.Directory - } - if sigStoreDir == "" { - if rootless.IsRootless() { - sigStoreDir = filepath.Join(filepath.Dir(ir.Libpod.StorageConfig().GraphRoot), "sigstore") - } else { - var sigStoreURI string - registryInfo := trust.HaveMatchRegistry(rawSource.Reference().DockerReference().String(), registryConfigs) - if registryInfo != nil { - if sigStoreURI = registryInfo.SigStoreStaging; sigStoreURI == "" { - sigStoreURI = registryInfo.SigStore - } + err = func() error { + srcRef, err := alltransports.ParseImageName(signimage) + if err != nil { + return errors.Wrapf(err, "error parsing image name") + } + rawSource, err := srcRef.NewImageSource(ctx, sc) + if err != nil { + return errors.Wrapf(err, "error getting image source") + } + defer func() { + if err = rawSource.Close(); err != nil { + logrus.Errorf("unable to close %s image source %q", srcRef.DockerReference().Name(), err) } - if sigStoreURI == "" { - return nil, errors.Errorf("no signature storage configuration found for %s", rawSource.Reference().DockerReference().String()) - + }() + getManifest, _, err := rawSource.GetManifest(ctx, nil) + if err != nil { + return errors.Wrapf(err, "error getting getManifest") + } + dockerReference := rawSource.Reference().DockerReference() + if dockerReference == nil { + return errors.Errorf("cannot determine canonical Docker reference for destination %s", transports.ImageName(rawSource.Reference())) + } + var sigStoreDir string + if options.Directory != "" { + repo := reference.Path(dockerReference) + if path.Clean(repo) != repo { // Coverage: This should not be reachable because /./ and /../ components are not valid in docker references + return errors.Errorf("Unexpected path elements in Docker reference %s for signature storage", dockerReference.String()) + } + sigStoreDir = filepath.Join(options.Directory, repo) + } else { + signatureURL, err := docker.SignatureStorageBaseURL(sc, rawSource.Reference(), true) + if err != nil { + return err } - sigStoreDir, err = localPathFromURI(sigStoreURI) + sigStoreDir, err = localPathFromURI(signatureURL) if err != nil { - return nil, errors.Wrapf(err, "invalid signature storage %s", sigStoreURI) + return err } } - } - manifestDigest, err := manifest.Digest(getManifest) - if err != nil { - return nil, err - } - repo := reference.Path(dockerReference) - if path.Clean(repo) != repo { // Coverage: This should not be reachable because /./ and /../ components are not valid in docker references - return nil, errors.Errorf("Unexpected path elements in Docker reference %s for signature storage", dockerReference.String()) - } + manifestDigest, err := manifest.Digest(getManifest) + if err != nil { + return err + } - // create signature - newSig, err := signature.SignDockerManifest(getManifest, dockerReference.String(), mech, options.SignBy) - if err != nil { - return nil, errors.Wrapf(err, "error creating new signature") - } - // create the signstore file - signatureDir := fmt.Sprintf("%s@%s=%s", filepath.Join(sigStoreDir, repo), manifestDigest.Algorithm(), manifestDigest.Hex()) - if err := os.MkdirAll(signatureDir, 0751); err != nil { - // The directory is allowed to exist - if !os.IsExist(err) { - logrus.Error(err) - continue + // create signature + newSig, err := signature.SignDockerManifest(getManifest, dockerReference.String(), mech, options.SignBy) + if err != nil { + return errors.Wrapf(err, "error creating new signature") } - } - sigFilename, err := getSigFilename(signatureDir) - if err != nil { - logrus.Errorf("error creating sigstore file: %v", err) - continue - } - err = ioutil.WriteFile(filepath.Join(signatureDir, sigFilename), newSig, 0644) + // create the signstore file + signatureDir := fmt.Sprintf("%s@%s=%s", sigStoreDir, manifestDigest.Algorithm(), manifestDigest.Hex()) + if err := os.MkdirAll(signatureDir, 0751); err != nil { + // The directory is allowed to exist + if !os.IsExist(err) { + logrus.Error(err) + return nil + } + } + sigFilename, err := getSigFilename(signatureDir) + if err != nil { + logrus.Errorf("error creating sigstore file: %v", err) + return nil + } + err = ioutil.WriteFile(filepath.Join(signatureDir, sigFilename), newSig, 0644) + if err != nil { + logrus.Errorf("error storing signature for %s", rawSource.Reference().DockerReference().String()) + return nil + } + return nil + }() if err != nil { - logrus.Errorf("error storing signature for %s", rawSource.Reference().DockerReference().String()) - continue + return nil, err } } return nil, nil @@ -815,14 +800,9 @@ func getSigFilename(sigStoreDirPath string) (string, error) { } } -func localPathFromURI(sigStoreDir string) (string, error) { - url, err := url.Parse(sigStoreDir) - if err != nil { - return sigStoreDir, errors.Wrapf(err, "invalid directory %s", sigStoreDir) - } +func localPathFromURI(url *url.URL) (string, error) { if url.Scheme != "file" { - return sigStoreDir, errors.Errorf("writing to %s is not supported. Use a supported scheme", sigStoreDir) + return "", errors.Errorf("writing to %s is not supported. Use a supported scheme", url.String()) } - sigStoreDir = url.Path - return sigStoreDir, nil + return url.Path, nil } diff --git a/pkg/domain/infra/abi/images_list.go b/pkg/domain/infra/abi/images_list.go index 281b04294..c4b0b7712 100644 --- a/pkg/domain/infra/abi/images_list.go +++ b/pkg/domain/infra/abi/images_list.go @@ -35,13 +35,11 @@ func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) Created: img.Created().Unix(), Dangling: img.Dangling(), Digest: string(img.Digest()), - Digests: digests, + RepoDigests: digests, History: img.NamesHistory(), Names: img.Names(), - ParentId: img.Parent, ReadOnly: img.IsReadOnly(), SharedSize: 0, - VirtualSize: img.VirtualSize, RepoTags: img.Names(), // may include tags and digests } e.Labels, err = img.Labels(ctx) @@ -60,6 +58,15 @@ func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) return nil, errors.Wrapf(err, "error retrieving size of image %q: you may need to remove the image to resolve the error", img.ID()) } e.Size = int64(*sz) + // This is good enough for now, but has to be + // replaced later with correct calculation logic + e.VirtualSize = int64(*sz) + + parent, err := img.ParentID(ctx) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving parent of image %q: you may need to remove the image to resolve the error", img.ID()) + } + e.ParentId = parent summaries = append(summaries, &e) } diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index 72fd98ac1..ec2532bea 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/containers/common/pkg/config" + "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/pkg/cgroups" "github.com/containers/podman/v2/pkg/domain/entities" @@ -86,7 +87,11 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, cmd *cobra.Command) return nil } - pausePidPath, err := util.GetRootlessPauseProcessPidPath() + tmpDir, err := ic.Libpod.TmpDir() + if err != nil { + return err + } + pausePidPath, err := util.GetRootlessPauseProcessPidPathGivenDir(tmpDir) if err != nil { return errors.Wrapf(err, "could not get pause process pid file path") } @@ -112,7 +117,7 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, cmd *cobra.Command) } became, ret, err = rootless.TryJoinFromFilePaths(pausePidPath, true, paths) - if err := movePauseProcessToScope(); err != nil { + if err := movePauseProcessToScope(ic.Libpod); err != nil { conf, err := ic.Config(context.Background()) if err != nil { return err @@ -133,8 +138,12 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, cmd *cobra.Command) return nil } -func movePauseProcessToScope() error { - pausePidPath, err := util.GetRootlessPauseProcessPidPath() +func movePauseProcessToScope(r *libpod.Runtime) error { + tmpDir, err := r.TmpDir() + if err != nil { + return err + } + pausePidPath, err := util.GetRootlessPauseProcessPidPathGivenDir(tmpDir) if err != nil { return errors.Wrapf(err, "could not get pause process pid file path") } diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 63677719b..3584668c7 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -731,8 +731,8 @@ func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrID string, o return reports, nil } -func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) (*entities.ContainerCpReport, error) { - return nil, errors.New("not implemented") +func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) error { + return errors.New("not implemented") } // Shutdown Libpod engine diff --git a/pkg/domain/infra/tunnel/generate.go b/pkg/domain/infra/tunnel/generate.go index 966f707b1..ebbfa143f 100644 --- a/pkg/domain/infra/tunnel/generate.go +++ b/pkg/domain/infra/tunnel/generate.go @@ -11,6 +11,6 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, return generate.Systemd(ic.ClientCxt, nameOrID, options) } -func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { - return generate.Kube(ic.ClientCxt, nameOrID, options) +func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { + return generate.Kube(ic.ClientCxt, nameOrIDs, options) } diff --git a/pkg/specgen/generate/config_linux.go b/pkg/specgen/generate/config_linux.go index 2d40dba8f..1808f99b8 100644 --- a/pkg/specgen/generate/config_linux.go +++ b/pkg/specgen/generate/config_linux.go @@ -4,13 +4,16 @@ import ( "fmt" "io/ioutil" "os" + "path" "path/filepath" "strings" "github.com/containers/podman/v2/pkg/rootless" + "github.com/containers/podman/v2/pkg/util" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) @@ -137,22 +140,33 @@ func DevicesFromPath(g *generate.Generator, devicePath string) error { return addDevice(g, strings.Join(append([]string{resolvedDevicePath}, devs[1:]...), ":")) } -func BlockAccessToKernelFilesystems(privileged, pidModeIsHost bool, g *generate.Generator) { +func BlockAccessToKernelFilesystems(privileged, pidModeIsHost bool, mask, unmask []string, g *generate.Generator) { + defaultMaskPaths := []string{"/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/fs/selinux", + "/sys/dev/block", + } + + unmaskAll := false + if unmask != nil && unmask[0] == "ALL" { + unmaskAll = true + } + if !privileged { - for _, mp := range []string{ - "/proc/acpi", - "/proc/kcore", - "/proc/keys", - "/proc/latency_stats", - "/proc/timer_list", - "/proc/timer_stats", - "/proc/sched_debug", - "/proc/scsi", - "/sys/firmware", - "/sys/fs/selinux", - "/sys/dev", - } { - g.AddLinuxMaskedPaths(mp) + if !unmaskAll { + for _, mp := range defaultMaskPaths { + // check that the path to mask is not in the list of paths to unmask + if !util.StringInSlice(mp, unmask) { + g.AddLinuxMaskedPaths(mp) + } + } } if pidModeIsHost && rootless.IsRootless() { @@ -170,6 +184,15 @@ func BlockAccessToKernelFilesystems(privileged, pidModeIsHost bool, g *generate. g.AddLinuxReadonlyPaths(rp) } } + + // mask the paths provided by the user + for _, mp := range mask { + if !path.IsAbs(mp) && mp != "" { + logrus.Errorf("Path %q is not an absolute path, skipping...", mp) + continue + } + g.AddLinuxMaskedPaths(mp) + } } // based on getDevices from runc (libcontainer/devices/devices.go) diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 95e4eeb8f..4f36744ca 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -98,7 +98,6 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener // present. imgName := newImage.InputName if s.Image == newImage.InputName && strings.HasPrefix(newImage.ID(), s.Image) { - imgName = "" names := newImage.Names() if len(names) > 0 { imgName = names[0] diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index ddc73ca61..036c7b7a1 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -233,6 +233,8 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. val = fmt.Sprintf("slirp4netns:%s", s.NetNS.Value) } toReturn = append(toReturn, libpod.WithNetNS(portMappings, postConfigureNetNS, val, nil)) + case specgen.Private: + fallthrough case specgen.Bridge: portMappings, err := createPortMappings(ctx, s, img) if err != nil { diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index 8454458a8..c24dcf4c0 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -165,7 +165,7 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt inUserNS = true } } - if inUserNS && s.NetNS.IsHost() { + if inUserNS && s.NetNS.NSMode != specgen.NoNetwork { canMountSys = false } @@ -298,7 +298,7 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt } } - BlockAccessToKernelFilesystems(s.Privileged, s.PidNS.IsHost(), &g) + BlockAccessToKernelFilesystems(s.Privileged, s.PidNS.IsHost(), s.Mask, s.Unmask, &g) for name, val := range s.Env { g.AddProcessEnv(name, val) diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index d15745fa0..9d78a0210 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -258,24 +258,22 @@ func ParseNetworkNamespace(ns string) (Namespace, []string, error) { var cniNetworks []string // Net defaults to Slirp on rootless switch { - case ns == "slirp4netns", strings.HasPrefix(ns, "slirp4netns:"): + case ns == string(Slirp), strings.HasPrefix(ns, string(Slirp)+":"): toReturn.NSMode = Slirp - case ns == "pod": + case ns == string(FromPod): toReturn.NSMode = FromPod - case ns == "": + case ns == "" || ns == string(Default) || ns == string(Private): if rootless.IsRootless() { toReturn.NSMode = Slirp } else { toReturn.NSMode = Bridge } - case ns == "bridge": + case ns == string(Bridge): toReturn.NSMode = Bridge - case ns == "none": + case ns == string(NoNetwork): toReturn.NSMode = NoNetwork - case ns == "host": + case ns == string(Host): toReturn.NSMode = Host - case ns == "private": - toReturn.NSMode = Private case strings.HasPrefix(ns, "ns:"): split := strings.SplitN(ns, ":", 2) if len(split) != 2 { @@ -283,7 +281,7 @@ func ParseNetworkNamespace(ns string) (Namespace, []string, error) { } toReturn.NSMode = Path toReturn.Value = split[1] - case strings.HasPrefix(ns, "container:"): + case strings.HasPrefix(ns, string(FromContainer)+":"): split := strings.SplitN(ns, ":", 2) if len(split) != 2 { return toReturn, nil, errors.Errorf("must provide name or ID or a container when specifying container:") diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index fad2406e5..964b89fa4 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -307,6 +307,13 @@ type ContainerSecurityConfig struct { Umask string `json:"umask,omitempty"` // ProcOpts are the options used for the proc mount. ProcOpts []string `json:"procfs_opts,omitempty"` + // Mask is the path we want to mask in the container. This masks the paths + // given in addition to the default list. + // Optional + Mask []string `json:"mask,omitempty"` + // Unmask is the path we want to unmask in the container. To override + // all the default paths that are masked, set unmask=ALL. + Unmask []string `json:"unmask,omitempty"` } // ContainerCgroupConfig contains configuration information about a container's diff --git a/pkg/systemd/generate/pods.go b/pkg/systemd/generate/pods.go index c0acba37d..234a60380 100644 --- a/pkg/systemd/generate/pods.go +++ b/pkg/systemd/generate/pods.go @@ -224,7 +224,7 @@ func executePodTemplate(info *podInfo, options entities.GenerateSystemdOptions) executable, err := os.Executable() if err != nil { executable = "/usr/bin/podman" - logrus.Warnf("Could not obtain podman executable location, using default %s", executable) + logrus.Warnf("Could not obtain podman executable location, using default %s: %v", executable, err) } info.Executable = executable } diff --git a/pkg/trust/trust.go b/pkg/trust/trust.go index a61e0ef10..a30611b74 100644 --- a/pkg/trust/trust.go +++ b/pkg/trust/trust.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/containers/image/v5/types" + "github.com/docker/docker/pkg/homedir" "github.com/ghodss/yaml" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -60,6 +61,12 @@ type ShowOutput struct { Sigstore string } +// systemRegistriesDirPath is the path to registries.d. +const systemRegistriesDirPath = "/etc/containers/registries.d" + +// userRegistriesDir is the path to the per user registries.d. +var userRegistriesDir = filepath.FromSlash(".config/containers/registries.d") + // DefaultPolicyPath returns a path to the default policy of the system. func DefaultPolicyPath(sys *types.SystemContext) string { systemDefaultPolicyPath := "/etc/containers/policy.json" @@ -76,15 +83,17 @@ func DefaultPolicyPath(sys *types.SystemContext) string { // RegistriesDirPath returns a path to registries.d func RegistriesDirPath(sys *types.SystemContext) string { - systemRegistriesDirPath := "/etc/containers/registries.d" - if sys != nil { - if sys.RegistriesDirPath != "" { - return sys.RegistriesDirPath - } - if sys.RootForImplicitAbsolutePaths != "" { - return filepath.Join(sys.RootForImplicitAbsolutePaths, systemRegistriesDirPath) - } + if sys != nil && sys.RegistriesDirPath != "" { + return sys.RegistriesDirPath + } + userRegistriesDirPath := filepath.Join(homedir.Get(), userRegistriesDir) + if _, err := os.Stat(userRegistriesDirPath); err == nil { + return userRegistriesDirPath } + if sys != nil && sys.RootForImplicitAbsolutePaths != "" { + return filepath.Join(sys.RootForImplicitAbsolutePaths, systemRegistriesDirPath) + } + return systemRegistriesDirPath } diff --git a/pkg/util/utils.go b/pkg/util/utils.go index f6a084c00..e0f631eb4 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -530,6 +530,11 @@ func ParseInputTime(inputTime string) (time.Time, error) { } } + unix_timestamp, err := strconv.ParseInt(inputTime, 10, 64) + if err == nil { + return time.Unix(unix_timestamp, 0), nil + } + // input might be a duration duration, err := time.ParseDuration(inputTime) if err != nil { diff --git a/pkg/util/utils_supported.go b/pkg/util/utils_supported.go index 2d636a7cb..a63c76415 100644 --- a/pkg/util/utils_supported.go +++ b/pkg/util/utils_supported.go @@ -99,7 +99,8 @@ func GetRootlessConfigHomeDir() (string, error) { } // GetRootlessPauseProcessPidPath returns the path to the file that holds the pid for -// the pause process +// the pause process. +// DEPRECATED - switch to GetRootlessPauseProcessPidPathGivenDir func GetRootlessPauseProcessPidPath() (string, error) { runtimeDir, err := GetRuntimeDir() if err != nil { @@ -107,3 +108,13 @@ func GetRootlessPauseProcessPidPath() (string, error) { } return filepath.Join(runtimeDir, "libpod", "pause.pid"), nil } + +// GetRootlessPauseProcessPidPathGivenDir returns the path to the file that +// holds the PID of the pause process, given the location of Libpod's temporary +// files. +func GetRootlessPauseProcessPidPathGivenDir(libpodTmpDir string) (string, error) { + if libpodTmpDir == "" { + return "", errors.Errorf("must provide non-empty tmporary directory") + } + return filepath.Join(libpodTmpDir, "pause.pid"), nil +} diff --git a/pkg/util/utils_windows.go b/pkg/util/utils_windows.go index 9bba2d1ee..46ca5e7f1 100644 --- a/pkg/util/utils_windows.go +++ b/pkg/util/utils_windows.go @@ -25,6 +25,12 @@ func GetRootlessPauseProcessPidPath() (string, error) { return "", errors.Wrap(errNotImplemented, "GetRootlessPauseProcessPidPath") } +// GetRootlessPauseProcessPidPath returns the path to the file that holds the pid for +// the pause process +func GetRootlessPauseProcessPidPathGivenDir(unused string) (string, error) { + return "", errors.Wrap(errNotImplemented, "GetRootlessPauseProcessPidPath") +} + // GetRuntimeDir returns the runtime directory func GetRuntimeDir() (string, error) { return "", errors.New("this function is not implemented for windows") diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index b35c27215..5c35edf2b 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -169,7 +169,7 @@ t GET containers/$cid/json 200 \ .Args[1]="param2" t DELETE containers/$cid 204 -# test only set the entrpoint, Cmd should be [] +# test only set the entrypoint, Cmd should be [] t POST containers/create '"Image":"'$IMAGE'","Entrypoint":["echo","param1"]' 201 \ .Id~[0-9a-f]\\{64\\} cid=$(jq -r '.Id' <<<"$output") diff --git a/test/apiv2/25-containersMore.at b/test/apiv2/25-containersMore.at index 4f6b80a5f..b88c798eb 100644 --- a/test/apiv2/25-containersMore.at +++ b/test/apiv2/25-containersMore.at @@ -65,13 +65,13 @@ t GET libpod/containers/json?last=1 200 \ cid=$(jq -r '.[0].Id' <<<"$output") -t GET libpod/generate/$cid/kube 200 +t GET libpod/generate/kube?names=$cid 200 like "$output" ".*apiVersion:.*" "Check generated kube yaml - apiVersion" like "$output" ".*kind:\\sPod.*" "Check generated kube yaml - kind: Pod" like "$output" ".*metadata:.*" "Check generated kube yaml - metadata" like "$output" ".*spec:.*" "Check generated kube yaml - spec" -t GET libpod/generate/$cid/kube?service=true 200 +t GET "libpod/generate/kube?service=true&names=$cid" 200 like "$output" ".*apiVersion:.*" "Check generated kube yaml(service=true) - apiVersion" like "$output" ".*kind:\\sPod.*" "Check generated kube yaml(service=true) - kind: Pod" like "$output" ".*metadata:.*" "Check generated kube yaml(service=true) - metadata" @@ -79,4 +79,13 @@ like "$output" ".*spec:.*" "Check generated kube yaml(service=true) - spec" like "$output" ".*kind:\\sService.*" "Check generated kube yaml(service=true) - kind: Service" t DELETE libpod/containers/$cid 204 + +# Create 3 stopped containers to test containers prune +podman run $IMAGE true +podman run $IMAGE true +podman run $IMAGE true + +t POST libpod/containers/prune '' 200 +t GET libpod/containers/json 200 \ + length=0 # vim: filetype=sh diff --git a/test/apiv2/rest_api/test_rest_v2_0_0.py b/test/apiv2/rest_api/test_rest_v2_0_0.py index 52348d4f4..2f9e62149 100644 --- a/test/apiv2/rest_api/test_rest_v2_0_0.py +++ b/test/apiv2/rest_api/test_rest_v2_0_0.py @@ -1,11 +1,13 @@ import json +import random +import string import subprocess -import sys -import time import unittest from multiprocessing import Process import requests +import sys +import time from dateutil.parser import parse from test.apiv2.rest_api import Podman @@ -91,14 +93,21 @@ class TestApi(unittest.TestCase): self.assertIsNotNone(r.content) _ = json.loads(r.text) + info = requests.get(PODMAN_URL + "/v1.40/info") + self.assertEqual(info.status_code, 200, info.content) + _ = json.loads(info.text) + def test_events(self): r = requests.get(_url("/events?stream=false")) self.assertEqual(r.status_code, 200, r.text) self.assertIsNotNone(r.content) - for line in r.text.splitlines(): + + report = r.text.splitlines() + self.assertGreater(len(report), 0, "No events found!") + for line in report: obj = json.loads(line) # Actor.ID is uppercase for compatibility - _ = obj["Actor"]["ID"] + self.assertIn("ID", obj["Actor"]) def test_containers(self): r = requests.get(_url("/containers/json"), timeout=5) @@ -172,22 +181,26 @@ class TestApi(unittest.TestCase): self.assertEqual(net_default.status_code, 201, net_default.text) create = requests.post( - PODMAN_URL + "/v1.40/containers/create?name=postCreate", + PODMAN_URL + "/v1.40/containers/create?name=postCreateConnect", json={ "Cmd": ["top"], "Image": "alpine:latest", "NetworkDisabled": False, # FIXME adding these 2 lines cause: (This is sampled from docker-py) # "network already exists","message":"container - # 01306e499df5441560d70071a54342611e422a94de20865add50a9565fd79fb9 is already connected to CNI network \"TestDefaultNetwork\": network already exists" + # 01306e499df5441560d70071a54342611e422a94de20865add50a9565fd79fb9 is already connected to CNI + # network \"TestDefaultNetwork\": network already exists" # "HostConfig": {"NetworkMode": "TestDefaultNetwork"}, # "NetworkingConfig": {"EndpointsConfig": {"TestDefaultNetwork": None}}, # FIXME These two lines cause: - # CNI network \"TestNetwork\" not found","message":"error configuring network namespace for container 369ddfa7d3211ebf1fbd5ddbff91bd33fa948858cea2985c133d6b6507546dff: CNI network \"TestNetwork\" not found" + # CNI network \"TestNetwork\" not found","message":"error configuring network namespace for container + # 369ddfa7d3211ebf1fbd5ddbff91bd33fa948858cea2985c133d6b6507546dff: CNI network \"TestNetwork\" not + # found" # "HostConfig": {"NetworkMode": "TestNetwork"}, # "NetworkingConfig": {"EndpointsConfig": {"TestNetwork": None}}, # FIXME no networking defined cause: (note this error is from the container inspect below) - # "internal libpod error","message":"network inspection mismatch: asked to join 2 CNI network(s) [TestDefaultNetwork podman], but have information on 1 network(s): internal libpod error" + # "internal libpod error","message":"network inspection mismatch: asked to join 2 CNI network(s) [ + # TestDefaultNetwork podman], but have information on 1 network(s): internal libpod error" }, ) self.assertEqual(create.status_code, 201, create.text) @@ -255,23 +268,68 @@ class TestApi(unittest.TestCase): def test_commit(self): r = requests.post(_url(ctnr("/commit?container={}"))) self.assertEqual(r.status_code, 200, r.text) - validateObjectFields(r.text) - def test_images(self): - r = requests.get(_url("/images/json")) + obj = json.loads(r.content) + self.assertIsInstance(obj, dict) + self.assertIn("Id", obj) + + def test_images_compat(self): + r = requests.get(PODMAN_URL + "/v1.40/images/json") self.assertEqual(r.status_code, 200, r.text) - validateObjectFields(r.content) - def test_inspect_image(self): - r = requests.get(_url("/images/alpine/json")) + # See https://docs.docker.com/engine/api/v1.40/#operation/ImageList + required_keys = ( + "Id", + "ParentId", + "RepoTags", + "RepoDigests", + "Created", + "Size", + "SharedSize", + "VirtualSize", + "Labels", + "Containers", + ) + objs = json.loads(r.content) + self.assertIn(type(objs), (list,)) + for o in objs: + self.assertIsInstance(o, dict) + for k in required_keys: + self.assertIn(k, o) + + def test_inspect_image_compat(self): + r = requests.get(PODMAN_URL + "/v1.40/images/alpine/json") self.assertEqual(r.status_code, 200, r.text) - obj = validateObjectFields(r.content) + + # See https://docs.docker.com/engine/api/v1.40/#operation/ImageInspect + required_keys = ( + "Id", + "Parent", + "Comment", + "Created", + "Container", + "DockerVersion", + "Author", + "Architecture", + "Os", + "Size", + "VirtualSize", + "GraphDriver", + "RootFS", + "Metadata", + ) + + obj = json.loads(r.content) + self.assertIn(type(obj), (dict,)) + for k in required_keys: + self.assertIn(k, obj) _ = parse(obj["Created"]) - def test_delete_image(self): - r = requests.delete(_url("/images/alpine?force=true")) + def test_delete_image_compat(self): + r = requests.delete(PODMAN_URL + "/v1.40/images/alpine?force=true") self.assertEqual(r.status_code, 200, r.text) - json.loads(r.text) + obj = json.loads(r.content) + self.assertIn(type(obj), (list,)) def test_pull(self): r = requests.post(_url("/images/pull?reference=alpine"), timeout=15) @@ -295,12 +353,13 @@ class TestApi(unittest.TestCase): self.assertTrue(keys["images"], "Expected to find images stanza") self.assertTrue(keys["stream"], "Expected to find stream progress stanza's") - def test_search(self): + def test_search_compat(self): # Had issues with this test hanging when repositories not happy def do_search(): - r = requests.get(_url("/images/search?term=alpine"), timeout=5) + r = requests.get(PODMAN_URL + "/v1.40/images/search?term=alpine", timeout=5) self.assertEqual(r.status_code, 200, r.text) - json.loads(r.text) + objs = json.loads(r.text) + self.assertIn(type(objs), (list,)) search = Process(target=do_search) search.start() @@ -308,17 +367,168 @@ class TestApi(unittest.TestCase): self.assertFalse(search.is_alive(), "/images/search took too long") def test_ping(self): + required_headers = ( + "API-Version", + "Builder-Version", + "Docker-Experimental", + "Cache-Control", + "Pragma", + "Pragma", + ) + + def check_headers(req): + for k in required_headers: + self.assertIn(k, req.headers) + r = requests.get(PODMAN_URL + "/_ping") self.assertEqual(r.status_code, 200, r.text) + self.assertEqual(r.text, "OK") + check_headers(r) r = requests.head(PODMAN_URL + "/_ping") self.assertEqual(r.status_code, 200, r.text) + self.assertEqual(r.text, "") + check_headers(r) r = requests.get(_url("/_ping")) self.assertEqual(r.status_code, 200, r.text) + self.assertEqual(r.text, "OK") + check_headers(r) - r = requests.get(_url("/_ping")) + r = requests.head(_url("/_ping")) self.assertEqual(r.status_code, 200, r.text) + self.assertEqual(r.text, "") + check_headers(r) + + def test_history_compat(self): + r = requests.get(PODMAN_URL + "/v1.40/images/alpine/history") + self.assertEqual(r.status_code, 200, r.text) + + # See https://docs.docker.com/engine/api/v1.40/#operation/ImageHistory + required_keys = ("Id", "Created", "CreatedBy", "Tags", "Size", "Comment") + + objs = json.loads(r.content) + self.assertIn(type(objs), (list,)) + for o in objs: + self.assertIsInstance(o, dict) + for k in required_keys: + self.assertIn(k, o) + + def test_network_compat(self): + name = "Network_" + "".join(random.choice(string.ascii_letters) for i in range(10)) + + # Cannot test for 0 existing networks because default "podman" network always exists + + create = requests.post(PODMAN_URL + "/v1.40/networks/create", json={"Name": name}) + self.assertEqual(create.status_code, 201, create.content) + obj = json.loads(create.content) + self.assertIn(type(obj), (dict,)) + self.assertIn("Id", obj) + ident = obj["Id"] + self.assertNotEqual(name, ident) + + ls = requests.get(PODMAN_URL + "/v1.40/networks") + self.assertEqual(ls.status_code, 200, ls.content) + objs = json.loads(ls.content) + self.assertIn(type(objs), (list,)) + + found = False + for network in objs: + if network["Name"] == name: + found = True + self.assertTrue(found, f"Network {name} not found") + + inspect = requests.get(PODMAN_URL + f"/v1.40/networks/{ident}") + self.assertEqual(inspect.status_code, 200, inspect.content) + obj = json.loads(create.content) + self.assertIn(type(obj), (dict,)) + + inspect = requests.delete(PODMAN_URL + f"/v1.40/networks/{ident}") + self.assertEqual(inspect.status_code, 204, inspect.content) + inspect = requests.get(PODMAN_URL + f"/v1.40/networks/{ident}") + self.assertEqual(inspect.status_code, 404, inspect.content) + + prune = requests.post(PODMAN_URL + "/v1.40/networks/prune") + self.assertEqual(prune.status_code, 405, prune.content) + + def test_volumes_compat(self): + name = "Volume_" + "".join(random.choice(string.ascii_letters) for i in range(10)) + + ls = requests.get(PODMAN_URL + "/v1.40/volumes") + self.assertEqual(ls.status_code, 200, ls.content) + + # See https://docs.docker.com/engine/api/v1.40/#operation/VolumeList + required_keys = ( + "Volumes", + "Warnings", + ) + + obj = json.loads(ls.content) + self.assertIn(type(obj), (dict,)) + for k in required_keys: + self.assertIn(k, obj) + + create = requests.post(PODMAN_URL + "/v1.40/volumes/create", json={"Name": name}) + self.assertEqual(create.status_code, 201, create.content) + + # See https://docs.docker.com/engine/api/v1.40/#operation/VolumeCreate + # and https://docs.docker.com/engine/api/v1.40/#operation/VolumeInspect + required_keys = ( + "Name", + "Driver", + "Mountpoint", + "Labels", + "Scope", + "Options", + ) + + obj = json.loads(create.content) + self.assertIn(type(obj), (dict,)) + for k in required_keys: + self.assertIn(k, obj) + self.assertEqual(obj["Name"], name) + + inspect = requests.get(PODMAN_URL + f"/v1.40/volumes/{name}") + self.assertEqual(inspect.status_code, 200, inspect.content) + + obj = json.loads(create.content) + self.assertIn(type(obj), (dict,)) + for k in required_keys: + self.assertIn(k, obj) + + rm = requests.delete(PODMAN_URL + f"/v1.40/volumes/{name}") + self.assertEqual(rm.status_code, 204, rm.content) + + prune = requests.post(PODMAN_URL + "/v1.40/volumes/prune") + self.assertEqual(prune.status_code, 200, prune.content) + + def test_auth_compat(self): + r = requests.post( + PODMAN_URL + "/v1.40/auth", + json={ + "username": "bozo", + "password": "wedontneednopasswords", + "serveraddress": "https://localhost/v1.40/", + }, + ) + self.assertEqual(r.status_code, 404, r.content) + + def test_version(self): + r = requests.get(PODMAN_URL + "/v1.40/version") + self.assertEqual(r.status_code, 200, r.content) + + r = requests.get(_url("/version")) + self.assertEqual(r.status_code, 200, r.content) + + def test_df_compat(self): + r = requests.get(PODMAN_URL + "/v1.40/system/df") + self.assertEqual(r.status_code, 200, r.content) + + obj = json.loads(r.content) + self.assertIn("Images", obj) + self.assertIn("Containers", obj) + self.assertIn("Volumes", obj) + self.assertIn("BuildCache", obj) if __name__ == "__main__": diff --git a/test/e2e/build_test.go b/test/e2e/build_test.go index 63a2df67a..ac9481797 100644 --- a/test/e2e/build_test.go +++ b/test/e2e/build_test.go @@ -239,7 +239,7 @@ RUN printenv http_proxy` Expect(session.ExitCode()).To(Equal(0)) // Verify that OS and Arch are being set - inspect := podmanTest.PodmanNoCache([]string{"image", "inspect", "--format", "{{ index .Config.Labels }}", "test"}) + inspect := podmanTest.Podman([]string{"image", "inspect", "--format", "{{ index .Config.Labels }}", "test"}) inspect.WaitWithDefaultTimeout() data := inspect.OutputToString() Expect(data).To(ContainSubstring(buildah.Version)) diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 16d8bb770..d7bbdc633 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -317,7 +317,7 @@ func (p *PodmanTestIntegration) createArtifact(image string) { fmt.Printf("Caching %s at %s...", image, destName) if _, err := os.Stat(destName); os.IsNotExist(err) { pull := p.PodmanNoCache([]string{"pull", image}) - pull.Wait(90) + pull.Wait(240) Expect(pull.ExitCode()).To(Equal(0)) save := p.PodmanNoCache([]string{"save", "-o", destName, image}) diff --git a/test/e2e/config/containers.conf b/test/e2e/config/containers.conf index 5f852468d..35153ba05 100644 --- a/test/e2e/config/containers.conf +++ b/test/e2e/config/containers.conf @@ -52,3 +52,7 @@ dns_options=[ "debug", ] tz = "Pacific/Honolulu" umask = "0002" + +[engine] + +network_cmd_options=["allow_host_loopback=true"] diff --git a/test/e2e/containers_conf_test.go b/test/e2e/containers_conf_test.go index 866162f7f..28672cfc6 100644 --- a/test/e2e/containers_conf_test.go +++ b/test/e2e/containers_conf_test.go @@ -258,6 +258,12 @@ var _ = Describe("Podman run", func() { Expect(session.OutputToString()).To(Equal("0002")) }) + It("podman set network cmd options slirp options to allow host loopback", func() { + session := podmanTest.Podman([]string{"run", "--network", "slirp4netns", ALPINE, "ping", "-c1", "10.0.2.2"}) + session.Wait(30) + Expect(session.ExitCode()).To(Equal(0)) + }) + It("podman-remote test localcontainers.conf versus remote containers.conf", func() { if !IsRemote() { Skip("this test is only for remote") @@ -311,4 +317,5 @@ var _ = Describe("Podman run", func() { Expect(session.ExitCode()).To(Equal(0)) Expect(session.OutputToString()).To(Equal("0022")) }) + }) diff --git a/test/e2e/cp_test.go b/test/e2e/cp_test.go index c1d3be5ab..33908b60e 100644 --- a/test/e2e/cp_test.go +++ b/test/e2e/cp_test.go @@ -4,14 +4,18 @@ import ( "io/ioutil" "os" "os/exec" + "os/user" "path/filepath" - "strings" . "github.com/containers/podman/v2/test/utils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) +// NOTE: Only smoke tests. The system tests (i.e., "./test/system/*") take +// care of function and regression tests. Please consider adding system tests +// rather than e2e tests. System tests are used in RHEL gating. + var _ = Describe("Podman cp", func() { var ( tempdir string @@ -37,240 +41,108 @@ var _ = Describe("Podman cp", func() { }) + // Copy a file to the container, then back to the host and make sure + // that the contents match. It("podman cp file", func() { - srcPath := filepath.Join(podmanTest.RunRoot, "cp_test.txt") - dstPath := filepath.Join(podmanTest.RunRoot, "cp_from_container") - fromHostToContainer := []byte("copy from host to container") - - session := podmanTest.Podman([]string{"create", ALPINE, "cat", "foo"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - name := session.OutputToString() - - err := ioutil.WriteFile(srcPath, fromHostToContainer, 0644) + srcFile, err := ioutil.TempFile("", "") Expect(err).To(BeNil()) + defer srcFile.Close() + defer os.Remove(srcFile.Name()) - session = podmanTest.Podman([]string{"cp", srcPath, name + ":foo/"}) - session.WaitWithDefaultTimeout() - Expect(session).To(ExitWithError()) - - session = podmanTest.Podman([]string{"cp", srcPath, name + ":foo"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"cp", name + ":foo", dstPath}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"start", name}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - }) - - It("podman cp file to dir", func() { - name := "testctr" - setup := podmanTest.RunTopContainer(name) - setup.WaitWithDefaultTimeout() - Expect(setup.ExitCode()).To(Equal(0)) - - srcPath := "/tmp/cp_test.txt" - fromHostToContainer := []byte("copy from host to container directory") - err := ioutil.WriteFile(srcPath, fromHostToContainer, 0644) + originalContent := []byte("podman cp file test") + err = ioutil.WriteFile(srcFile.Name(), originalContent, 0644) Expect(err).To(BeNil()) - session := podmanTest.Podman([]string{"exec", name, "mkdir", "foodir"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"cp", srcPath, name + ":foodir/"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"exec", name, "ls", "foodir/cp_test.txt"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - os.Remove("/tmp/cp_test.txt") - }) - - It("podman cp dir to dir", func() { - testDirPath := filepath.Join(podmanTest.RunRoot, "TestDir1") - - session := podmanTest.Podman([]string{"create", ALPINE, "ls", "/foodir"}) + // Create a container. NOTE that container mustn't be running for copying. + session := podmanTest.Podman([]string{"create", ALPINE}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) name := session.OutputToString() - err := os.Mkdir(testDirPath, 0755) - Expect(err).To(BeNil()) - defer os.RemoveAll(testDirPath) - - session = podmanTest.Podman([]string{"cp", testDirPath, name + ":/foodir"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"cp", testDirPath, name + ":/foodir"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - testctr := "testctr" - setup := podmanTest.RunTopContainer(testctr) - setup.WaitWithDefaultTimeout() - Expect(setup.ExitCode()).To(Equal(0)) + // Copy TO the container. - session = podmanTest.Podman([]string{"exec", testctr, "mkdir", "foo"}) + // Cannot copy to a non-existent path (note the trailing "/"). + session = podmanTest.Podman([]string{"cp", srcFile.Name(), name + ":foo/"}) session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"cp", testDirPath + "/.", testctr + ":/foo"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - session = podmanTest.Podman([]string{"exec", testctr, "ls", "foo"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - Expect(len(session.OutputToString())).To(Equal(0)) + Expect(session).To(ExitWithError()) - session = podmanTest.Podman([]string{"cp", testctr + ":/foo/.", testDirPath}) + // The file will now be created (and written to). + session = podmanTest.Podman([]string{"cp", srcFile.Name(), name + ":foo"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - cmd := exec.Command("ls", testDirPath) - res, err := cmd.Output() - Expect(err).To(BeNil()) - Expect(len(res)).To(Equal(0)) - }) - It("podman cp stdin/stdout", func() { - SkipIfRemote("FIXME: podman-remote cp not implemented yet") - session := podmanTest.Podman([]string{"create", ALPINE, "ls", "foo"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - name := session.OutputToString() + // Copy FROM the container. - testDirPath := filepath.Join(podmanTest.RunRoot, "TestDir2") - err := os.Mkdir(testDirPath, 0755) - Expect(err).To(BeNil()) - defer os.RemoveAll(testDirPath) - cmd := exec.Command("tar", "-zcvf", "file.tar.gz", testDirPath) - _, err = cmd.Output() + destFile, err := ioutil.TempFile("", "") Expect(err).To(BeNil()) + defer destFile.Close() + defer os.Remove(destFile.Name()) - data, err := ioutil.ReadFile("foo.tar.gz") - reader := strings.NewReader(string(data)) - cmd.Stdin = reader - session = podmanTest.Podman([]string{"cp", "-", name + ":/foo"}) + session = podmanTest.Podman([]string{"cp", name + ":foo", destFile.Name()}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session = podmanTest.Podman([]string{"cp", "file.tar.gz", name + ":/foo.tar.gz"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - session = podmanTest.Podman([]string{"cp", name + ":/foo.tar.gz", "-"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - os.Remove("file.tar.gz") - }) - - It("podman cp tar", func() { - testctr := "testctr" - setup := podmanTest.RunTopContainer(testctr) - setup.WaitWithDefaultTimeout() - Expect(setup.ExitCode()).To(Equal(0)) - - session := podmanTest.Podman([]string{"exec", testctr, "mkdir", "foo"}) + session = podmanTest.Podman([]string{"start", name}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - path, err := os.Getwd() - Expect(err).To(BeNil()) - testDirPath := filepath.Join(path, "TestDir3") - err = os.Mkdir(testDirPath, 0777) + // Now make sure the content matches. + roundtripContent, err := ioutil.ReadFile(destFile.Name()) Expect(err).To(BeNil()) - defer os.RemoveAll(testDirPath) - cmd := exec.Command("tar", "-cvf", "file.tar", testDirPath) - _, err = cmd.Output() - Expect(err).To(BeNil()) - - session = podmanTest.Podman([]string{"cp", "file.tar", "testctr:/foo/"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"exec", testctr, "ls", "-l", "foo"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - Expect(session.OutputToString()).To(ContainSubstring("file.tar")) - - os.Remove("file.tar") + Expect(roundtripContent).To(Equal(originalContent)) }) - It("podman cp tar --extract", func() { - testctr := "testctr" - setup := podmanTest.RunTopContainer(testctr) - setup.WaitWithDefaultTimeout() - Expect(setup.ExitCode()).To(Equal(0)) - - session := podmanTest.Podman([]string{"exec", testctr, "mkdir", "/foo"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - path, err := os.Getwd() - Expect(err).To(BeNil()) - testDirPath := filepath.Join(path, "TestDir4") - err = os.Mkdir(testDirPath, 0777) - Expect(err).To(BeNil()) - defer os.RemoveAll(testDirPath) - f, err := os.Create(filepath.Join(testDirPath, "a.txt")) - Expect(err).To(BeNil()) - _, err = f.Write([]byte("Hello World!!!\n")) - f.Close() - cmd := exec.Command("tar", "-cvf", "file.tar", "TestDir4") - exec.Command("tar", "-cvf", "/home/mvasek/file.tar", testDirPath) - _, err = cmd.Output() + // Create a symlink in the container, use it as a copy destination and + // make sure that the link and the resolved path are accessible and + // give the right content. + It("podman cp symlink", func() { + srcFile, err := ioutil.TempFile("", "") Expect(err).To(BeNil()) - defer os.Remove("file.tar") + defer srcFile.Close() + defer os.Remove(srcFile.Name()) - session = podmanTest.Podman([]string{"cp", "--extract", "file.tar", "testctr:/foo/"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"exec", testctr, "cat", "/foo/TestDir4/a.txt"}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - Expect(session.OutputToString()).To(ContainSubstring("Hello World!!!")) - }) + originalContent := []byte("podman cp symlink test") + err = ioutil.WriteFile(srcFile.Name(), originalContent, 0644) + Expect(err).To(BeNil()) - It("podman cp symlink", func() { session := podmanTest.Podman([]string{"run", "-d", ALPINE, "top"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) name := session.OutputToString() - srcPath := filepath.Join(podmanTest.RunRoot, "cp_test.txt") - fromHostToContainer := []byte("copy from host to container") - err := ioutil.WriteFile(srcPath, fromHostToContainer, 0644) - Expect(err).To(BeNil()) - session = podmanTest.Podman([]string{"exec", name, "ln", "-s", "/tmp", "/test"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session = podmanTest.Podman([]string{"cp", "--pause=false", srcPath, name + ":/test"}) + session = podmanTest.Podman([]string{"cp", "--pause=false", srcFile.Name(), name + ":/test"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - _, err = os.Stat("/tmp/cp_test.txt") - Expect(err).To(Not(BeNil())) - - session = podmanTest.Podman([]string{"exec", name, "ln", "-s", "/tmp/nonesuch", "/test1"}) + session = podmanTest.Podman([]string{"exec", name, "cat", "/tmp/" + filepath.Base(srcFile.Name())}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(string(originalContent))) - session = podmanTest.Podman([]string{"cp", "--pause=false", srcPath, name + ":/test1/"}) + session = podmanTest.Podman([]string{"exec", name, "cat", "/test/" + filepath.Base(srcFile.Name())}) session.WaitWithDefaultTimeout() - Expect(session).To(ExitWithError()) - + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(string(originalContent))) }) + + // Copy a file to a volume in the container. The tricky part is that + // containers mustn't be running for copying, so Podman has to do some + // intense Yoga and 1) detect volume paths on the container, 2) resolve + // the path to the volume's mount point on the host, and 3) copy the + // data to the volume and not the container. It("podman cp volume", func() { + srcFile, err := ioutil.TempFile("", "") + Expect(err).To(BeNil()) + defer srcFile.Close() + defer os.Remove(srcFile.Name()) + + originalContent := []byte("podman cp volume") + err = ioutil.WriteFile(srcFile.Name(), originalContent, 0644) + Expect(err).To(BeNil()) session := podmanTest.Podman([]string{"volume", "create", "data"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) @@ -279,23 +151,31 @@ var _ = Describe("Podman cp", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - err = ioutil.WriteFile("cp_vol", []byte("copy to the volume"), 0644) - if err != nil { - os.Exit(1) - } - session = podmanTest.Podman([]string{"cp", "cp_vol", "container1" + ":/data/cp_vol1"}) + session = podmanTest.Podman([]string{"cp", srcFile.Name(), "container1" + ":/data/file.txt"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session = podmanTest.Podman([]string{"cp", "container1" + ":/data/cp_vol1", "cp_vol2"}) + // Now get the volume's mount point, read the file and make + // sure the contents match. + session = podmanTest.Podman([]string{"volume", "inspect", "data", "--format", "{{.Mountpoint}}"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - os.Remove("cp_vol") - os.Remove("cp_vol2") + volumeMountPoint := session.OutputToString() + copiedContent, err := ioutil.ReadFile(filepath.Join(volumeMountPoint, "file.txt")) + Expect(err).To(BeNil()) + Expect(copiedContent).To(Equal(originalContent)) }) + // Create another user in the container, let them create a file, copy + // it to the host and back to the container and make sure that we can + // access it, and (roughly) the right users own it. It("podman cp from ctr chown ", func() { + srcFile, err := ioutil.TempFile("", "") + Expect(err).To(BeNil()) + defer srcFile.Close() + defer os.Remove(srcFile.Name()) + setup := podmanTest.RunTopContainer("testctr") setup.WaitWithDefaultTimeout() Expect(setup.ExitCode()).To(Equal(0)) @@ -308,17 +188,19 @@ var _ = Describe("Podman cp", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session = podmanTest.Podman([]string{"cp", "--pause=false", "testctr:/tmp/testfile", "testfile1"}) + session = podmanTest.Podman([]string{"cp", "--pause=false", "testctr:/tmp/testfile", srcFile.Name()}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) // owner of the file copied to local machine is not testuser - cmd := exec.Command("ls", "-l", "testfile1") + u, err := user.Current() + Expect(err).To(BeNil()) + cmd := exec.Command("ls", "-l", srcFile.Name()) cmdRet, err := cmd.Output() Expect(err).To(BeNil()) - Expect(strings.Contains(string(cmdRet), "testuser")).To(BeFalse()) + Expect(string(cmdRet)).To(ContainSubstring(u.Username)) - session = podmanTest.Podman([]string{"cp", "--pause=false", "testfile1", "testctr:testfile2"}) + session = podmanTest.Podman([]string{"cp", "--pause=false", srcFile.Name(), "testctr:testfile2"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) @@ -327,45 +209,35 @@ var _ = Describe("Podman cp", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.OutputToString()).To(ContainSubstring("root")) - - os.Remove("testfile1") }) - It("podman cp the root directory from the ctr to an existing directory on the host ", func() { - imgName := "test-cp-root-dir:latest" - DockerfileName := "Dockerfile.test-cp-root-dir" - ctrName := "test-container-cp-root" - session := podmanTest.Podman([]string{"build", "-f", "build/" + DockerfileName, "-t", imgName, "build/"}) + // Copy the root dir "/" of a container to the host. + It("podman cp the root directory from the ctr to an existing directory on the host ", func() { + container := "copyroottohost" + session := podmanTest.RunTopContainer(container) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - testDirPath := filepath.Join(podmanTest.RunRoot, "TestDirForCp") - - session = podmanTest.Podman([]string{"create", "--name", ctrName, imgName, "dummy"}) + session = podmanTest.Podman([]string{"exec", container, "touch", "/dummy.txt"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - err := os.Mkdir(testDirPath, 0755) + tmpDir, err := ioutil.TempDir("", "") Expect(err).To(BeNil()) - defer os.RemoveAll(testDirPath) - // Copy the root directory of the container to an existing directory - session = podmanTest.Podman([]string{"cp", ctrName + ":/", testDirPath}) + session = podmanTest.Podman([]string{"cp", container + ":/", tmpDir}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - // The file should be in the directory, - // not one layer too much of the directory called merged - checkFile := filepath.Join(testDirPath, DockerfileName) - _, err = os.Stat(checkFile) + cmd := exec.Command("ls", "-la", tmpDir) + output, err := cmd.Output() + lsOutput := string(output) Expect(err).To(BeNil()) - - session = podmanTest.Podman([]string{"container", "rm", ctrName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) - - session = podmanTest.Podman([]string{"rmi", "-f", imgName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(Equal(0)) + Expect(lsOutput).To(ContainSubstring("dummy.txt")) + Expect(lsOutput).To(ContainSubstring("tmp")) + Expect(lsOutput).To(ContainSubstring("etc")) + Expect(lsOutput).To(ContainSubstring("var")) + Expect(lsOutput).To(ContainSubstring("bin")) + Expect(lsOutput).To(ContainSubstring("usr")) }) }) diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index c8782c743..0950a9321 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -469,4 +469,74 @@ var _ = Describe("Podman generate kube", func() { Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.OutputToString()).To(ContainSubstring(`"pid"`)) }) + + It("podman generate kube multiple pods should fail", func() { + pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", ALPINE, "top"}) + pod1.WaitWithDefaultTimeout() + Expect(pod1.ExitCode()).To(Equal(0)) + + pod2 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod2", ALPINE, "top"}) + pod2.WaitWithDefaultTimeout() + Expect(pod2.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "pod2"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).ToNot(Equal(0)) + }) + + It("podman generate kube with pods and containers should fail", func() { + pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", ALPINE, "top"}) + pod1.WaitWithDefaultTimeout() + Expect(pod1.ExitCode()).To(Equal(0)) + + pod2 := podmanTest.Podman([]string{"run", "-dt", "--name", "top", ALPINE, "top"}) + pod2.WaitWithDefaultTimeout() + Expect(pod2.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "top"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).ToNot(Equal(0)) + }) + + It("podman generate kube with containers in a pod should fail", func() { + pod1 := podmanTest.Podman([]string{"pod", "create", "--name", "pod1"}) + pod1.WaitWithDefaultTimeout() + Expect(pod1.ExitCode()).To(Equal(0)) + + con := podmanTest.Podman([]string{"run", "-dt", "--pod", "pod1", "--name", "top", ALPINE, "top"}) + con.WaitWithDefaultTimeout() + Expect(con.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "top"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).ToNot(Equal(0)) + }) + + It("podman generate kube with multiple containers", func() { + con1 := podmanTest.Podman([]string{"run", "-dt", "--name", "con1", ALPINE, "top"}) + con1.WaitWithDefaultTimeout() + Expect(con1.ExitCode()).To(Equal(0)) + + con2 := podmanTest.Podman([]string{"run", "-dt", "--name", "con2", ALPINE, "top"}) + con2.WaitWithDefaultTimeout() + Expect(con2.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "con1", "con2"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + }) + + It("podman generate kube with containers in a pod should fail", func() { + pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", "--name", "top1", ALPINE, "top"}) + pod1.WaitWithDefaultTimeout() + Expect(pod1.ExitCode()).To(Equal(0)) + + pod2 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod2", "--name", "top2", ALPINE, "top"}) + pod2.WaitWithDefaultTimeout() + Expect(pod2.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "pod2"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).ToNot(Equal(0)) + }) }) diff --git a/test/e2e/network_test.go b/test/e2e/network_test.go index ffc914bc2..4e8ab5ad5 100644 --- a/test/e2e/network_test.go +++ b/test/e2e/network_test.go @@ -119,7 +119,13 @@ var _ = Describe("Podman network", func() { }) It("podman network list --filter invalid value", func() { - session := podmanTest.Podman([]string{"network", "ls", "--filter", "namr=ab"}) + net := "net" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", net}) + session.WaitWithDefaultTimeout() + defer podmanTest.removeCNINetwork(net) + Expect(session.ExitCode()).To(BeZero()) + + session = podmanTest.Podman([]string{"network", "ls", "--filter", "namr=ab"}) session.WaitWithDefaultTimeout() Expect(session).To(ExitWithError()) Expect(session.ErrorToString()).To(ContainSubstring(`invalid filter "namr"`)) diff --git a/test/e2e/run_memory_test.go b/test/e2e/run_memory_test.go index b3913c1e6..ad3a2b54f 100644 --- a/test/e2e/run_memory_test.go +++ b/test/e2e/run_memory_test.go @@ -38,7 +38,7 @@ var _ = Describe("Podman run memory", func() { var session *PodmanSessionIntegration if CGROUPSV2 { - session = podmanTest.Podman([]string{"run", "--memory=40m", ALPINE, "sh", "-c", "cat /sys/fs/cgroup/$(sed -e 's|0::||' < /proc/self/cgroup)/memory.max"}) + session = podmanTest.Podman([]string{"run", "--memory=40m", "--net=none", ALPINE, "sh", "-c", "cat /sys/fs/cgroup/$(sed -e 's|0::||' < /proc/self/cgroup)/memory.max"}) } else { session = podmanTest.Podman([]string{"run", "--memory=40m", ALPINE, "cat", "/sys/fs/cgroup/memory/memory.limit_in_bytes"}) } @@ -55,7 +55,7 @@ var _ = Describe("Podman run memory", func() { var session *PodmanSessionIntegration if CGROUPSV2 { - session = podmanTest.Podman([]string{"run", "--memory-reservation=40m", ALPINE, "sh", "-c", "cat /sys/fs/cgroup/$(sed -e 's|0::||' < /proc/self/cgroup)/memory.low"}) + session = podmanTest.Podman([]string{"run", "--memory-reservation=40m", "--net=none", ALPINE, "sh", "-c", "cat /sys/fs/cgroup/$(sed -e 's|0::||' < /proc/self/cgroup)/memory.low"}) } else { session = podmanTest.Podman([]string{"run", "--memory-reservation=40m", ALPINE, "cat", "/sys/fs/cgroup/memory/memory.soft_limit_in_bytes"}) } @@ -81,7 +81,7 @@ var _ = Describe("Podman run memory", func() { var session *PodmanSessionIntegration if CGROUPSV2 { - session = podmanTest.Podman([]string{"run", "--memory-reservation=40m", ALPINE, "sh", "-c", "cat /sys/fs/cgroup/$(sed -e 's|0::||' < /proc/self/cgroup)/memory.low"}) + session = podmanTest.Podman([]string{"run", "--net=none", "--memory-reservation=40m", ALPINE, "sh", "-c", "cat /sys/fs/cgroup/$(sed -e 's|0::||' < /proc/self/cgroup)/memory.low"}) } else { session = podmanTest.Podman([]string{"run", "--memory-reservation=40m", ALPINE, "cat", "/sys/fs/cgroup/memory/memory.soft_limit_in_bytes"}) } diff --git a/test/e2e/run_networking_test.go b/test/e2e/run_networking_test.go index 3e80e953e..3fb00a28b 100644 --- a/test/e2e/run_networking_test.go +++ b/test/e2e/run_networking_test.go @@ -49,9 +49,28 @@ var _ = Describe("Podman run networking", func() { Expect(session.ExitCode()).To(Equal(0)) }) + It("podman run network connection with default", func() { + session := podmanTest.Podman([]string{"run", "--network", "default", ALPINE, "wget", "www.podman.io"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + + It("podman run network connection with none", func() { + session := podmanTest.Podman([]string{"run", "--network", "none", ALPINE, "wget", "www.podman.io"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(1)) + Expect(session.ErrorToString()).To(ContainSubstring("wget: bad address 'www.podman.io'")) + }) + + It("podman run network connection with private", func() { + session := podmanTest.Podman([]string{"run", "--network", "private", ALPINE, "wget", "www.podman.io"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + }) + It("podman run network connection with loopback", func() { - session := podmanTest.Podman([]string{"run", "-dt", "--network", "host", ALPINE, "wget", "www.podman.io"}) - session.Wait(90) + session := podmanTest.Podman([]string{"run", "--network", "host", ALPINE, "wget", "www.podman.io"}) + session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) }) diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 0d65a3e59..58ef9a647 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -233,6 +233,39 @@ var _ = Describe("Podman run", func() { return jsonFile } + It("podman run mask and unmask path test", func() { + session := podmanTest.Podman([]string{"run", "-d", "--name=maskCtr1", "--security-opt", "unmask=ALL", "--security-opt", "mask=/proc/acpi", ALPINE, "sleep", "200"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"exec", "maskCtr1", "ls", "/sys/firmware"}) + session.WaitWithDefaultTimeout() + Expect(session.OutputToString()).To(Not(BeEmpty())) + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"exec", "maskCtr1", "ls", "/proc/acpi"}) + session.WaitWithDefaultTimeout() + Expect(session.OutputToString()).To(BeEmpty()) + + session = podmanTest.Podman([]string{"run", "-d", "--name=maskCtr2", "--security-opt", "unmask=/proc/acpi:/sys/firmware", ALPINE, "sleep", "200"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"exec", "maskCtr2", "ls", "/sys/firmware"}) + session.WaitWithDefaultTimeout() + Expect(session.OutputToString()).To(Not(BeEmpty())) + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"exec", "maskCtr2", "ls", "/proc/acpi"}) + session.WaitWithDefaultTimeout() + Expect(session.OutputToString()).To(Not(BeEmpty())) + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "-d", "--name=maskCtr3", "--security-opt", "mask=/sys/power/disk", ALPINE, "sleep", "200"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"exec", "maskCtr3", "cat", "/sys/power/disk"}) + session.WaitWithDefaultTimeout() + Expect(session.OutputToString()).To(BeEmpty()) + Expect(session.ExitCode()).To(Equal(0)) + }) + It("podman run seccomp test", func() { session := podmanTest.Podman([]string{"run", "-it", "--security-opt", strings.Join([]string{"seccomp=", forbidGetCWDSeccompProfile()}, ""), ALPINE, "pwd"}) session.WaitWithDefaultTimeout() @@ -1267,7 +1300,7 @@ USER mail` It("podman run verify pids-limit", func() { SkipIfCgroupV1("pids-limit not supported on cgroup V1") limit := "4321" - session := podmanTest.Podman([]string{"run", "--pids-limit", limit, "--rm", ALPINE, "cat", "/sys/fs/cgroup/pids.max"}) + session := podmanTest.Podman([]string{"run", "--pids-limit", limit, "--net=none", "--rm", ALPINE, "cat", "/sys/fs/cgroup/pids.max"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(session.OutputToString()).To(ContainSubstring(limit)) diff --git a/test/system/030-run.bats b/test/system/030-run.bats index 37695f205..3ee141f5f 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -536,6 +536,43 @@ json-file | f run_podman untag $IMAGE $newtag $newtag2 } +# Regression test for issue #8558 +@test "podman run on untagged image: make sure that image metadata is set" { + run_podman inspect $IMAGE --format "{{.ID}}" + imageID="$output" + + # prior to #8623 `podman run` would error out on untagged images with: + # Error: both RootfsImageName and RootfsImageID must be set if either is set: invalid argument + run_podman untag $IMAGE + run_podman run --rm $imageID ls + + run_podman tag $imageID $IMAGE +} + +@test "Verify /run/.containerenv exist" { + run_podman run --rm $IMAGE ls -1 /run/.containerenv + is "$output" "/run/.containerenv" + + run_podman run --privileged --rm $IMAGE sh -c '. /run/.containerenv; echo $engine' + is "$output" ".*podman.*" "failed to identify engine" + + run_podman run --privileged --name "testcontainerenv" --rm $IMAGE sh -c '. /run/.containerenv; echo $name' + is "$output" ".*testcontainerenv.*" + + run_podman run --privileged --rm $IMAGE sh -c '. /run/.containerenv; echo $image' + is "$output" ".*$IMAGE.*" "failed to idenitfy image" + + run_podman run --privileged --rm $IMAGE sh -c '. /run/.containerenv; echo $rootless' + # FIXME: on some CI systems, 'run --privileged' emits a spurious + # warning line about dup devices. Ignore it. + remove_same_dev_warning + if is_rootless; then + is "$output" "1" + else + is "$output" "0" + fi +} + @test "podman run with --net=host and --port prints warning" { rand=$(random_string 10) diff --git a/test/system/035-logs.bats b/test/system/035-logs.bats index a3d6a5800..a081a7ce1 100644 --- a/test/system/035-logs.bats +++ b/test/system/035-logs.bats @@ -21,6 +21,9 @@ load helpers run_podman logs $cid is "$output" "$rand_string" "output from podman-logs after container is run" + # test --since with Unix timestamps + run_podman logs --since 1000 $cid + run_podman rm $cid } diff --git a/test/system/065-cp.bats b/test/system/065-cp.bats index 6bf897790..43bdf217d 100644 --- a/test/system/065-cp.bats +++ b/test/system/065-cp.bats @@ -7,6 +7,290 @@ load helpers +@test "podman cp file from host to container" { + skip_if_remote "podman-remote does not yet handle cp" + + srcdir=$PODMAN_TMPDIR/cp-test-file-host-to-ctr + mkdir -p $srcdir + local -a randomcontent=( + random-0-$(random_string 10) + random-1-$(random_string 15) + random-2-$(random_string 20) + ) + echo "${randomcontent[0]}" > $srcdir/hostfile0 + echo "${randomcontent[1]}" > $srcdir/hostfile1 + echo "${randomcontent[2]}" > $srcdir/hostfile2 + + run_podman run -d --name cpcontainer --workdir=/srv $IMAGE sleep infinity + run_podman exec cpcontainer mkdir /srv/subdir + + # format is: <id> | <destination arg to cp> | <full dest path> | <test name> + # where: + # id is 0-2, one of the random strings/files + # dest arg is the right-hand argument to 'podman cp' (may be implicit) + # dest path is the full explicit path we expect to see + # test name is a short description of what we're testing here + tests=" +0 | / | /hostfile0 | copy to root +0 | /anotherbase.txt | /anotherbase.txt | copy to root, new name +0 | /tmp | /tmp/hostfile0 | copy to /tmp +1 | /tmp/ | /tmp/hostfile1 | copy to /tmp/ +2 | /tmp/. | /tmp/hostfile2 | copy to /tmp/. +0 | /tmp/hostfile2 | /tmp/hostfile2 | overwrite previous copy +0 | /tmp/anotherbase.txt | /tmp/anotherbase.txt | copy to /tmp, new name +0 | . | /srv/hostfile0 | copy to workdir (rel path), new name +1 | ./ | /srv/hostfile1 | copy to workdir (rel path), new name +0 | anotherbase.txt | /srv/anotherbase.txt | copy to workdir (rel path), new name +0 | subdir | /srv/subdir/hostfile0 | copy to workdir/subdir +" + + # Copy one of the files into container, exec+cat, confirm the file + # is there and matches what we expect + while read id dest dest_fullname description; do + run_podman cp $srcdir/hostfile$id cpcontainer:$dest + run_podman exec cpcontainer cat $dest_fullname + is "$output" "${randomcontent[$id]}" "$description (cp -> ctr:$dest)" + done < <(parse_table "$tests") + + # Host path does not exist. + run_podman 125 cp $srcdir/IdoNotExist cpcontainer:/tmp + is "$output" 'Error: ".*/IdoNotExist" could not be found on the host' \ + "copy nonexistent host path" + + # Container path does not exist. Notice that the error message shows how + # the specified container is resolved. + run_podman 125 cp $srcdir/hostfile0 cpcontainer:/IdoNotExist/ + is "$output" 'Error: "/IdoNotExist/" could not be found on container.*(resolved to .*/IdoNotExist.*' \ + "copy into nonexistent path in container" + + run_podman rm -f cpcontainer +} + + +@test "podman cp --extract=true tar archive to container" { + skip_if_remote "podman-remote does not yet handle cp" + + # Create tempfile with random name and content + dirname=cp-test-extract + srcdir=$PODMAN_TMPDIR/$dirname + mkdir -p $srcdir + rand_filename=$(random_string 20) + rand_content=$(random_string 50) + echo $rand_content > $srcdir/$rand_filename + chmod 644 $srcdir/$rand_filename + + # Now tar it up! + tar_file=$PODMAN_TMPDIR/archive.tar.gz + tar -C $PODMAN_TMPDIR -zvcf $tar_file $dirname + + run_podman run -d --name cpcontainer $IMAGE sleep infinity + + # First just copy without extracting the archive. + run_podman cp $tar_file cpcontainer:/tmp + # Now remove the archive which will also test if it exists and is a file. + # To save expensive exec'ing, create a file for the next tests. + run_podman exec cpcontainer sh -c "rm /tmp/archive.tar.gz; touch /tmp/file.txt" + + # Now copy with extracting the archive. NOTE that Podman should + # auto-decompress the file if needed. + run_podman cp --extract=true $tar_file cpcontainer:/tmp + run_podman exec cpcontainer cat /tmp/$dirname/$rand_filename + is "$output" "$rand_content" + + # Test extract on non archive. + run_podman cp --extract=true $srcdir/$rand_filename cpcontainer:/foo.txt + + # Cannot extract an archive to a file! + run_podman 125 cp --extract=true $tar_file cpcontainer:/tmp/file.txt + is "$output" 'Error: cannot extract archive .* to file "/tmp/file.txt"' + + run_podman rm -f cpcontainer +} + + +@test "podman cp file from container to host" { + skip_if_remote "podman-remote does not yet handle cp" + + srcdir=$PODMAN_TMPDIR/cp-test-file-ctr-to-host + mkdir -p $srcdir + + # Create 3 files with random content in the container. + local -a randomcontent=( + random-0-$(random_string 10) + random-1-$(random_string 15) + random-2-$(random_string 20) + ) + run_podman run -d --name cpcontainer --workdir=/srv $IMAGE sleep infinity + run_podman exec cpcontainer sh -c "echo ${randomcontent[0]} > /tmp/containerfile" + run_podman exec cpcontainer sh -c "echo ${randomcontent[1]} > /srv/containerfile1" + run_podman exec cpcontainer sh -c "mkdir /srv/subdir; echo ${randomcontent[2]} > /srv/subdir/containerfile2" + + # format is: <id> | <source arg to cp> | <destination arg (appended to $srcdir) to cp> | <full dest path (appended to $srcdir)> | <test name> + tests=" +0 | /tmp/containerfile | | /containerfile | copy to srcdir/ +0 | /tmp/containerfile | / | /containerfile | copy to srcdir/ +0 | /tmp/containerfile | /. | /containerfile | copy to srcdir/. +0 | /tmp/containerfile | /newfile | /newfile | copy to srcdir/newfile +1 | containerfile1 | / | /containerfile1 | copy from workdir (rel path) to srcdir +2 | subdir/containerfile2 | / | /containerfile2 | copy from workdir/subdir (rel path) to srcdir +" + + # Copy one of the files to the host, cat, confirm the file + # is there and matches what we expect + while read id src dest dest_fullname description; do + # dest may be "''" for empty table cells + if [[ $dest == "''" ]];then + unset dest + fi + run_podman cp cpcontainer:$src "$srcdir$dest" + run cat $srcdir$dest_fullname + is "$output" "${randomcontent[$id]}" "$description (cp ctr:$src to \$srcdir$dest)" + rm $srcdir/$dest_fullname + done < <(parse_table "$tests") + + run_podman rm -f cpcontainer +} + + +@test "podman cp dir from host to container" { + skip_if_remote "podman-remote does not yet handle cp" + + dirname=dir-test + srcdir=$PODMAN_TMPDIR/$dirname + mkdir -p $srcdir + local -a randomcontent=( + random-0-$(random_string 10) + random-1-$(random_string 15) + ) + echo "${randomcontent[0]}" > $srcdir/hostfile0 + echo "${randomcontent[1]}" > $srcdir/hostfile1 + + run_podman run -d --name cpcontainer --workdir=/srv $IMAGE sleep infinity + run_podman exec cpcontainer mkdir /srv/subdir + + # format is: <source arg to cp (appended to srcdir)> | <destination arg to cp> | <full dest path> | <test name> + tests=" + | / | /dir-test | copy to root + / | /tmp | /tmp/dir-test | copy to tmp + /. | /usr/ | /usr/ | copy contents of dir to usr/ + | . | /srv/dir-test | copy to workdir (rel path) + | subdir/. | /srv/subdir/dir-test | copy to workdir subdir (rel path) +" + + while read src dest dest_fullname description; do + # src may be "''" for empty table cells + if [[ $src == "''" ]];then + unset src + fi + run_podman cp $srcdir$src cpcontainer:$dest + run_podman exec cpcontainer ls $dest_fullname + run_podman exec cpcontainer cat $dest_fullname/hostfile0 + is "$output" "${randomcontent[0]}" "$description (cp -> ctr:$dest)" + run_podman exec cpcontainer cat $dest_fullname/hostfile1 + is "$output" "${randomcontent[1]}" "$description (cp -> ctr:$dest)" + done < <(parse_table "$tests") + + run_podman rm -f cpcontainer +} + + +@test "podman cp dir from container to host" { + skip_if_remote "podman-remote does not yet handle cp" + + srcdir=$PODMAN_TMPDIR/dir-test + mkdir -p $srcdir + + run_podman run -d --name cpcontainer --workdir=/srv $IMAGE sleep infinity + run_podman exec cpcontainer sh -c 'mkdir /srv/subdir; echo "This first file is on the container" > /srv/subdir/containerfile1' + run_podman exec cpcontainer sh -c 'echo "This second file is on the container as well" > /srv/subdir/containerfile2' + + run_podman cp cpcontainer:/srv $srcdir + run cat $srcdir/srv/subdir/containerfile1 + is "$output" "This first file is on the container" + run cat $srcdir/srv/subdir/containerfile2 + is "$output" "This second file is on the container as well" + rm -rf $srcdir/srv/subdir + + run_podman cp cpcontainer:/srv/. $srcdir + run ls $srcdir/subdir + run cat $srcdir/subdir/containerfile1 + is "$output" "This first file is on the container" + run cat $srcdir/subdir/containerfile2 + is "$output" "This second file is on the container as well" + rm -rf $srcdir/subdir + + run_podman cp cpcontainer:/srv/subdir/. $srcdir + run cat $srcdir/containerfile1 + is "$output" "This first file is on the container" + run cat $srcdir/containerfile2 + is "$output" "This second file is on the container as well" + + run_podman rm -f cpcontainer +} + + +@test "podman cp file from host to container volume" { + skip_if_remote "podman-remote does not yet handle cp" + + srcdir=$PODMAN_TMPDIR/cp-test-volume + mkdir -p $srcdir + echo "This file should be in volume2" > $srcdir/hostfile + volume1=$(random_string 20) + volume2=$(random_string 20) + + run_podman volume create $volume1 + run_podman volume inspect $volume1 --format "{{.Mountpoint}}" + volume1_mount="$output" + run_podman volume create $volume2 + run_podman volume inspect $volume2 --format "{{.Mountpoint}}" + volume2_mount="$output" + + # Create a container using the volume. Note that copying on not-running + # containers is allowed, so Podman has to analyze the container paths and + # check if they are hitting a volume, and eventually resolve to the path on + # the *host*. + # This test is extra tricky, as volume2 is mounted into a sub-directory of + # volume1. Podman must copy the file into volume2 and not volume1. + run_podman create --name cpcontainer -v $volume1:/tmp/volume -v $volume2:/tmp/volume/sub-volume $IMAGE + + run_podman cp $srcdir/hostfile cpcontainer:/tmp/volume/sub-volume + + run cat $volume2_mount/hostfile + is "$output" "This file should be in volume2" + + # Volume 1 must be empty. + run ls $volume1_mount + is "$output" "" + + run_podman rm -f cpcontainer + run_podman volume rm $volume1 $volume2 +} + + +@test "podman cp file from host to container mount" { + skip_if_remote "podman-remote does not yet handle cp" + + srcdir=$PODMAN_TMPDIR/cp-test-mount-src + mountdir=$PODMAN_TMPDIR/cp-test-mount + mkdir -p $srcdir $mountdir + echo "This file should be in the mount" > $srcdir/hostfile + + volume=$(random_string 20) + run_podman volume create $volume + + # Make it a bit more complex and put the mount on a volume. + run_podman create --name cpcontainer -v $volume:/tmp/volume -v $mountdir:/tmp/volume/mount $IMAGE + + run_podman cp $srcdir/hostfile cpcontainer:/tmp/volume/mount + + run cat $mountdir/hostfile + is "$output" "This file should be in the mount" + + run_podman rm -f cpcontainer + run_podman volume rm $volume +} + + # Create two random-name random-content files in /tmp in the container # podman-cp them into the host using '/tmp/*', i.e. asking podman to # perform wildcard expansion in the container. We should get both @@ -51,8 +335,7 @@ load helpers run_podman 125 cp 'cpcontainer:/tmp/*' $dstdir/ # FIXME: this might not be the exactly correct error message - is "$output" ".*error evaluating symlinks.*lstat.*no such file or dir" \ - "Expected error from copying invalid symlink" + is "$output" 'Error: "/tmp/\*" could not be found on container.*' # make sure there are no files in dstdir is "$(/bin/ls -1 $dstdir)" "" "incorrectly copied symlink from host" @@ -78,8 +361,7 @@ load helpers sh -c "ln -s $srcdir/hostfile file1;ln -s file\* copyme" run_podman 125 cp cpcontainer:copyme $dstdir - is "$output" ".*error evaluating symlinks.*lstat.*no such file or dir" \ - "Expected error from copying invalid symlink" + is "$output" 'Error: "copyme*" could not be found on container.*' # make sure there are no files in dstdir is "$(/bin/ls -1 $dstdir)" "" "incorrectly copied symlink from host" @@ -101,8 +383,7 @@ load helpers sh -c "ln -s $srcdir/hostfile /tmp/\*" run_podman 125 cp 'cpcontainer:/tmp/*' $dstdir - is "$output" ".*error evaluating symlinks.*lstat.*no such file or dir" \ - "Expected error from copying invalid symlink" + is "$output" 'Error: "/tmp/\*" could not be found on container.*' # dstdir must be empty is "$(/bin/ls -1 $dstdir)" "" "incorrectly copied symlink from host" @@ -110,8 +391,6 @@ load helpers run_podman rm cpcontainer } -############################################################################### -# cp INTO container # THIS IS EXTREMELY WEIRD. Podman expands symlinks in weird ways. @test "podman cp into container: weird symlink expansion" { @@ -148,7 +427,7 @@ load helpers is "$output" "" "output from podman cp 1" run_podman 125 cp --pause=false $srcdir/$rand_filename2 cpcontainer:/tmp/d2/x/ - is "$output" ".*stat.* no such file or directory" "cp will not create nonexistent destination directory" + is "$output" 'Error: "/tmp/d2/x/" could not be found on container.*' "cp will not create nonexistent destination directory" run_podman cp --pause=false $srcdir/$rand_filename3 cpcontainer:/tmp/d3/x is "$output" "" "output from podman cp 3" @@ -160,6 +439,7 @@ load helpers run_podman exec cpcontainer cat /tmp/nonesuch1 is "$output" "$rand_content1" "cp creates destination file" + # cp into nonexistent directory should not mkdir nonesuch2 directory run_podman 1 exec cpcontainer test -e /tmp/nonesuch2 @@ -168,8 +448,6 @@ load helpers is "$output" "$rand_content3" "cp creates file named x" run_podman rm -f cpcontainer - - } @@ -212,6 +490,103 @@ load helpers } +@test "podman cp from stdin to container" { + skip_if_remote "podman-remote does not yet handle cp" + + # Create tempfile with random name and content + srcdir=$PODMAN_TMPDIR/cp-test-stdin + mkdir -p $srcdir + rand_filename=$(random_string 20) + rand_content=$(random_string 50) + echo $rand_content > $srcdir/$rand_filename + chmod 644 $srcdir/$rand_filename + + # Now tar it up! + tar_file=$PODMAN_TMPDIR/archive.tar.gz + tar -zvcf $tar_file $srcdir + + run_podman run -d --name cpcontainer $IMAGE sleep infinity + + # NOTE: podman is supposed to auto-detect the gzip compression and + # decompress automatically. + # + # "-" will evaluate to "/dev/stdin" when used a source. + run_podman cp - cpcontainer:/tmp < $tar_file + run_podman exec cpcontainer cat /tmp/$srcdir/$rand_filename + is "$output" "$rand_content" + run_podman exec cpcontainer rm -rf /tmp/$srcdir + + # Now for "/dev/stdin". + run_podman cp /dev/stdin cpcontainer:/tmp < $tar_file + run_podman exec cpcontainer cat /tmp/$srcdir/$rand_filename + is "$output" "$rand_content" + + # Error checks below ... + + # Input stream must be a (compressed) tar archive. + run_podman 125 cp - cpcontainer:/tmp < $srcdir/$rand_filename + is "$output" "Error:.*: error reading tar stream.*" "input stream must be a (compressed) tar archive" + + # Destination must be a directory (on an existing file). + run_podman exec cpcontainer touch /tmp/file.txt + run_podman 125 cp /dev/stdin cpcontainer:/tmp/file.txt < $tar_file + is "$output" 'Error: destination must be a directory or stream when copying from a stream' + + # Destination must be a directory (on an absent path). + run_podman 125 cp /dev/stdin cpcontainer:/tmp/IdoNotExist < $tar_file + is "$output" 'Error: destination must be a directory or stream when copying from a stream' + + run_podman rm -f cpcontainer +} + + +@test "podman cp from container to stdout" { + skip_if_remote "podman-remote does not yet handle cp" + + srcdir=$PODMAN_TMPDIR/cp-test-stdout + mkdir -p $srcdir + rand_content=$(random_string 50) + + run_podman run -d --name cpcontainer $IMAGE sleep infinity + + run_podman exec cpcontainer sh -c "echo '$rand_content' > /tmp/file.txt" + run_podman exec cpcontainer touch /tmp/empty.txt + + # Copying from stdout will always compress. So let's copy the previously + # created file from the container via stdout, untar the archive and make + # sure the file exists with the expected content. + # + # NOTE that we can't use run_podman because that uses the BATS 'run' + # function which redirects stdout and stderr. Here we need to guarantee + # that podman's stdout is a pipe, not any other form of redirection. + + # Copy file. + $PODMAN cp cpcontainer:/tmp/file.txt - > $srcdir/stdout.tar + if [ $? -ne 0 ]; then + die "Command failed: podman cp ... - | cat" + fi + + tar xvf $srcdir/stdout.tar -C $srcdir + run cat $srcdir/file.txt + is "$output" "$rand_content" + run 1 ls $srcfir/empty.txt + rm -f $srcdir/* + + # Copy directory. + $PODMAN cp cpcontainer:/tmp - > $srcdir/stdout.tar + if [ $? -ne 0 ]; then + die "Command failed: podman cp ... - | cat : $output" + fi + + tar xvf $srcdir/stdout.tar -C $srcdir + run cat $srcdir/file.txt + is "$output" "$rand_content" + run cat $srcdir/empty.txt + is "$output" "" + + run_podman rm -f cpcontainer +} + function teardown() { # In case any test fails, clean up the container we left behind run_podman rm -f cpcontainer diff --git a/test/system/120-load.bats b/test/system/120-load.bats index 8ea9b1c69..272e2ae93 100644 --- a/test/system/120-load.bats +++ b/test/system/120-load.bats @@ -28,12 +28,15 @@ verify_iid_and_name() { @test "podman save to pipe and load" { # Generate a random name and tag (must be lower-case) - local random_name=x$(random_string 12 | tr A-Z a-z) - local random_tag=t$(random_string 7 | tr A-Z a-z) + local random_name=x0$(random_string 12 | tr A-Z a-z) + local random_tag=t0$(random_string 7 | tr A-Z a-z) local fqin=localhost/$random_name:$random_tag run_podman tag $IMAGE $fqin - archive=$PODMAN_TMPDIR/myimage-$(random_string 8).tar + # Believe it or not, 'podman load' would barf if any path element + # included a capital letter + archive=$PODMAN_TMPDIR/MySubDirWithCaps/MyImage-$(random_string 8).tar + mkdir -p $(dirname $archive) # We can't use run_podman because that uses the BATS 'run' function # which redirects stdout and stderr. Here we need to guarantee @@ -51,19 +54,20 @@ verify_iid_and_name() { run_podman images $fqin --format '{{.Repository}}:{{.Tag}}' is "$output" "$fqin" "image preserves name across save/load" - # FIXME: when/if 7337 gets fixed, load with a new tag - if false; then - local new_name=x$(random_string 14 | tr A-Z a-z) - local new_tag=t$(random_string 6 | tr A-Z a-z) + # Load with a new tag + local new_name=x1$(random_string 14 | tr A-Z a-z) + local new_tag=t1$(random_string 6 | tr A-Z a-z) run_podman rmi $fqin - fqin=localhost/$new_name:$new_tag - run_podman load -i $archive $fqin - run_podman images $fqin --format '{{.Repository}}:{{.Tag}}' - is "$output" "$fqin" "image can be loaded with new name:tag" - fi + + new_fqin=localhost/$new_name:$new_tag + run_podman load -i $archive $new_fqin + run_podman images --format '{{.Repository}}:{{.Tag}}' --sort tag + is "${lines[0]}" "$IMAGE" "image is preserved" + is "${lines[1]}" "$fqin" "image is reloaded with old fqin" + is "${lines[2]}" "$new_fqin" "image is reloaded with new fqin too" # Clean up - run_podman rmi $fqin + run_podman rmi $fqin $new_fqin } diff --git a/test/system/400-unprivileged-access.bats b/test/system/400-unprivileged-access.bats index 142d7dcd9..20fdd068f 100644 --- a/test/system/400-unprivileged-access.bats +++ b/test/system/400-unprivileged-access.bats @@ -118,7 +118,7 @@ EOF /proc/scsi /sys/firmware /sys/fs/selinux - /sys/dev + /sys/dev/block ) # Some of the above may not exist on our host. Find only the ones that do. diff --git a/troubleshooting.md b/troubleshooting.md index 3ff578142..78e22fa2f 100644 --- a/troubleshooting.md +++ b/troubleshooting.md @@ -248,7 +248,10 @@ This means johndoe is allocated UIDS 100000-165535 as well as his standard UID i /etc/passwd file. You should ensure that each user has a unique range of uids, because overlapping UIDs, -would potentially allow one user to attack another user. +would potentially allow one user to attack another user. In addition, make sure +that the range of uids you allocate can cover all uids that the container +requires. For example, if the container has a user with uid 10000, ensure you +have at least 10001 subuids. You could also use the usermod program to assign UIDs to a user. diff --git a/utils/utils_supported.go b/utils/utils_supported.go index bcaa2c61a..6f517dc72 100644 --- a/utils/utils_supported.go +++ b/utils/utils_supported.go @@ -43,6 +43,15 @@ func RunUnderSystemdScope(pid int, slice string, unitName string) error { ch := make(chan string) _, err = conn.StartTransientUnit(unitName, "replace", properties, ch) if err != nil { + // On errors check if the cgroup already exists, if it does move the process there + if props, err := conn.GetUnitTypeProperties(unitName, "Scope"); err == nil { + if cgroup, ok := props["ControlGroup"].(string); ok && cgroup != "" { + if err := moveUnderCgroup(cgroup, "", []uint32{uint32(pid)}); err != nil { + return err + } + return nil + } + } return err } defer conn.Close() @@ -101,6 +110,13 @@ func GetCgroupProcess(pid int) (string, error) { // MoveUnderCgroupSubtree moves the PID under a cgroup subtree. func MoveUnderCgroupSubtree(subtree string) error { + return moveUnderCgroup("", subtree, nil) +} + +// moveUnderCgroup moves a group of processes to a new cgroup. +// If cgroup is the empty string, then the current calling process cgroup is used. +// If processes is empty, then the processes from the current cgroup are moved. +func moveUnderCgroup(cgroup, subtree string, processes []uint32) error { procFile := "/proc/self/cgroup" f, err := os.Open(procFile) if err != nil { @@ -140,13 +156,12 @@ func MoveUnderCgroupSubtree(subtree string) error { cgroupRoot = filepath.Join(cgroupRoot, controller) } - processes, err := ioutil.ReadFile(filepath.Join(cgroupRoot, parts[2], "cgroup.procs")) - if err != nil { - return err + parentCgroup := cgroup + if parentCgroup == "" { + parentCgroup = parts[2] } - - newCgroup := filepath.Join(cgroupRoot, parts[2], subtree) - if err := os.Mkdir(newCgroup, 0755); err != nil { + newCgroup := filepath.Join(cgroupRoot, parentCgroup, subtree) + if err := os.Mkdir(newCgroup, 0755); err != nil && !os.IsExist(err) { return err } @@ -156,9 +171,21 @@ func MoveUnderCgroupSubtree(subtree string) error { } defer f.Close() - for _, pid := range bytes.Split(processes, []byte("\n")) { - if _, err := f.Write(pid); err != nil { - logrus.Warnf("Cannot move process %s to cgroup %q", pid, newCgroup) + if len(processes) > 0 { + for _, pid := range processes { + if _, err := f.Write([]byte(fmt.Sprintf("%d\n", pid))); err != nil { + logrus.Warnf("Cannot move process %d to cgroup %q", pid, newCgroup) + } + } + } else { + processesData, err := ioutil.ReadFile(filepath.Join(cgroupRoot, parts[2], "cgroup.procs")) + if err != nil { + return err + } + for _, pid := range bytes.Split(processesData, []byte("\n")) { + if _, err := f.Write(pid); err != nil { + logrus.Warnf("Cannot move process %s to cgroup %q", string(pid), newCgroup) + } } } } diff --git a/vendor/github.com/containers/common/pkg/config/config.go b/vendor/github.com/containers/common/pkg/config/config.go index 2769781f2..320d5e0e5 100644 --- a/vendor/github.com/containers/common/pkg/config/config.go +++ b/vendor/github.com/containers/common/pkg/config/config.go @@ -268,6 +268,10 @@ type EngineConfig struct { // NetworkCmdPath is the path to the slirp4netns binary. NetworkCmdPath string `toml:"network_cmd_path,omitempty"` + // NetworkCmdOptions is the default options to pass to the slirp4netns binary. + // For example "allow_host_loopback=true" + NetworkCmdOptions []string `toml:"network_cmd_options,omitempty"` + // NoPivotRoot sets whether to set no-pivot-root in the OCI runtime. NoPivotRoot bool `toml:"no_pivot_root,omitempty"` @@ -359,6 +363,12 @@ type EngineConfig struct { // under. This convention is followed by the default volume driver, but // may not be by other drivers. VolumePath string `toml:"volume_path,omitempty"` + + // VolumePlugins is a set of plugins that can be used as the backend for + // Podman named volumes. Each volume is specified as a name (what Podman + // will refer to the plugin as) mapped to a path, which must point to a + // Unix socket that conforms to the Volume Plugin specification. + VolumePlugins map[string]string `toml:"volume_plugins,omitempty"` } // SetOptions contains a subset of options in a Config. It's used to indicate if @@ -441,11 +451,6 @@ func NewConfig(userConfigPath string) (*Config, error) { return nil, err } - // read libpod.conf and convert the config to *Config - if err = newLibpodConfig(config); err != nil && !os.IsNotExist(err) { - logrus.Errorf("error reading libpod.conf: %v", err) - } - // Now, gather the system configs and merge them as needed. configs, err := systemConfigs() if err != nil { diff --git a/vendor/github.com/containers/common/pkg/config/containers.conf b/vendor/github.com/containers/common/pkg/config/containers.conf index ed7c91931..12fbecc22 100644 --- a/vendor/github.com/containers/common/pkg/config/containers.conf +++ b/vendor/github.com/containers/common/pkg/config/containers.conf @@ -348,6 +348,11 @@ default_sysctls = [ # # network_cmd_path="" +# Default options to pass to the slirp4netns binary. +# For example "allow_host_loopback=true" +# +# network_cmd_options=[] + # Whether to use chroot instead of pivot_root in the runtime # # no_pivot_root = false @@ -386,7 +391,7 @@ default_sysctls = [ # Default OCI runtime # -# runtime = "runc" +# runtime = "crun" # List of the OCI runtimes that support --format=json. When json is supported # engine will use it for reporting nicer errors. @@ -453,8 +458,11 @@ default_sysctls = [ # "/usr/bin/kata-fc", # ] -# The [engine.runtimes] table MUST be the last entry in this file. +[engine.volume_plugins] +# testplugin = "/run/podman/plugins/test.sock" + +# The [engine.volume_plugins] table MUST be the last entry in this file. # (Unless another table is added) # TOML does not provide a way to end a table other than a further table being -# defined, so every key hereafter will be part of [runtimes] and not the main -# config. +# defined, so every key hereafter will be part of [volume_plugins] and not the +# main config. diff --git a/vendor/github.com/containers/common/pkg/config/default.go b/vendor/github.com/containers/common/pkg/config/default.go index 4f1460e3b..2b3a098a7 100644 --- a/vendor/github.com/containers/common/pkg/config/default.go +++ b/vendor/github.com/containers/common/pkg/config/default.go @@ -242,11 +242,7 @@ func defaultConfigFromMemory() (*EngineConfig, error) { c.ImageDefaultTransport = _defaultTransport c.StateType = BoltDBStateStore - c.OCIRuntime = "runc" - // If we're running on cgroupv2 v2, default to using crun. - if cgroup2, _ := cgroupv2.Enabled(); cgroup2 { - c.OCIRuntime = "crun" - } + c.OCIRuntime = "crun" c.ImageBuildFormat = "oci" c.CgroupManager = defaultCgroupManager() diff --git a/vendor/github.com/containers/common/pkg/config/libpodConfig.go b/vendor/github.com/containers/common/pkg/config/libpodConfig.go deleted file mode 100644 index 2df3d6077..000000000 --- a/vendor/github.com/containers/common/pkg/config/libpodConfig.go +++ /dev/null @@ -1,407 +0,0 @@ -package config - -/* libpodConfig.go contains deprecated functionality and should not be used any longer */ - -import ( - "os" - "os/exec" - "path/filepath" - - "github.com/BurntSushi/toml" - "github.com/containers/common/pkg/cgroupv2" - "github.com/containers/storage/pkg/unshare" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const ( - // _rootlessConfigPath is the path to the rootless libpod.conf in $HOME. - _rootlessConfigPath = ".config/containers/libpod.conf" - - // _rootConfigPath is the path to the libpod configuration file - // This file is loaded to replace the builtin default config before - // runtime options (e.g. WithStorageConfig) are applied. - // If it is not present, the builtin default config is used instead - // This path can be overridden when the runtime is created by using - // NewRuntimeFromConfig() instead of NewRuntime(). - _rootConfigPath = _installPrefix + "/share/containers/libpod.conf" - - // _rootOverrideConfigPath is the path to an override for the default libpod - // configuration file. If OverrideConfigPath exists, it will be used in - // place of the configuration file pointed to by ConfigPath. - _rootOverrideConfigPath = _etcDir + "/containers/libpod.conf" -) - -// ConfigFromLibpod contains configuration options used to set up a libpod runtime -type ConfigFromLibpod struct { - // NOTE: when changing this struct, make sure to update (*Config).Merge(). - - // SetOptions contains a subset of config options. It's used to indicate if - // a given option has either been set by the user or by a parsed libpod - // configuration file. If not, the corresponding option might be - // overwritten by values from the database. This behavior guarantees - // backwards compat with older version of libpod and Podman. - SetOptions - - // VolumePath is the default location that named volumes will be created - // under. This convention is followed by the default volume driver, but - // may not be by other drivers. - VolumePath string `toml:"volume_path,omitempty"` - - // ImageDefaultTransport is the default transport method used to fetch - // images. - ImageDefaultTransport string `toml:"image_default_transport,omitempty"` - - // SignaturePolicyPath is the path to a signature policy to use for - // validating images. If left empty, the containers/image default signature - // policy will be used. - SignaturePolicyPath string `toml:"signature_policy_path,omitempty"` - - // OCIRuntime is the OCI runtime to use. - OCIRuntime string `toml:"runtime,omitempty"` - - // OCIRuntimes are the set of configured OCI runtimes (default is runc). - OCIRuntimes map[string][]string `toml:"runtimes,omitempty"` - - // RuntimeSupportsJSON is the list of the OCI runtimes that support - // --format=json. - RuntimeSupportsJSON []string `toml:"runtime_supports_json,omitempty"` - - // RuntimeSupportsNoCgroups is a list of OCI runtimes that support - // running containers without CGroups. - RuntimeSupportsNoCgroups []string `toml:"runtime_supports_nocgroupv2,omitempty"` - - // RuntimePath is the path to OCI runtime binary for launching containers. - // The first path pointing to a valid file will be used This is used only - // when there are no OCIRuntime/OCIRuntimes defined. It is used only to be - // backward compatible with older versions of Podman. - RuntimePath []string `toml:"runtime_path,omitempty"` - - // ConmonPath is the path to the Conmon binary used for managing containers. - // The first path pointing to a valid file will be used. - ConmonPath []string `toml:"conmon_path,omitempty"` - - // ConmonEnvVars are environment variables to pass to the Conmon binary - // when it is launched. - ConmonEnvVars []string `toml:"conmon_env_vars,omitempty"` - - // CGroupManager is the CGroup Manager to use Valid values are "cgroupfs" - // and "systemd". - CgroupManager string `toml:"cgroup_manager,omitempty"` - - // InitPath is the path to the container-init binary. - InitPath string `toml:"init_path,omitempty"` - - // StaticDir is the path to a persistent directory to store container - // files. - StaticDir string `toml:"static_dir,omitempty"` - - // TmpDir is the path to a temporary directory to store per-boot container - // files. Must be stored in a tmpfs. - TmpDir string `toml:"tmp_dir,omitempty"` - - // MaxLogSize is the maximum size of container logfiles. - MaxLogSize int64 `toml:"max_log_size,omitempty"` - - // NoPivotRoot sets whether to set no-pivot-root in the OCI runtime. - NoPivotRoot bool `toml:"no_pivot_root,omitempty"` - - // CNIConfigDir sets the directory where CNI configuration files are - // stored. - CNIConfigDir string `toml:"cni_config_dir,omitempty"` - - // CNIPluginDir sets a number of directories where the CNI network - // plugins can be located. - CNIPluginDir []string `toml:"cni_plugin_dir,omitempty"` - - // CNIDefaultNetwork is the network name of the default CNI network - // to attach pods to. - CNIDefaultNetwork string `toml:"cni_default_network,omitempty"` - - // HooksDir holds paths to the directories containing hooks - // configuration files. When the same filename is present in in - // multiple directories, the file in the directory listed last in - // this slice takes precedence. - HooksDir []string `toml:"hooks_dir,omitempty"` - - // Namespace is the libpod namespace to use. Namespaces are used to create - // scopes to separate containers and pods in the state. When namespace is - // set, libpod will only view containers and pods in the same namespace. All - // containers and pods created will default to the namespace set here. A - // namespace of "", the empty string, is equivalent to no namespace, and all - // containers and pods will be visible. The default namespace is "". - Namespace string `toml:"namespace,omitempty"` - - // InfraImage is the image a pod infra container will use to manage - // namespaces. - InfraImage string `toml:"infra_image,omitempty"` - - // InfraCommand is the command run to start up a pod infra container. - InfraCommand string `toml:"infra_command,omitempty"` - - // EnablePortReservation determines whether libpod will reserve ports on the - // host when they are forwarded to containers. When enabled, when ports are - // forwarded to containers, they are held open by conmon as long as the - // container is running, ensuring that they cannot be reused by other - // programs on the host. However, this can cause significant memory usage if - // a container has many ports forwarded to it. Disabling this can save - // memory. - EnablePortReservation bool `toml:"enable_port_reservation,omitempty"` - - // EnableLabeling indicates whether libpod will support container labeling. - EnableLabeling bool `toml:"label,omitempty"` - - // NetworkCmdPath is the path to the slirp4netns binary. - NetworkCmdPath string `toml:"network_cmd_path,omitempty"` - - // NumLocks is the number of locks to make available for containers and - // pods. - NumLocks uint32 `toml:"num_locks,omitempty"` - - // LockType is the type of locking to use. - LockType string `toml:"lock_type,omitempty"` - - // EventsLogger determines where events should be logged. - EventsLogger string `toml:"events_logger,omitempty"` - - // EventsLogFilePath is where the events log is stored. - EventsLogFilePath string `toml:"events_logfile_path,omitempty"` - - // DetachKeys is the sequence of keys used to detach a container. - DetachKeys string `toml:"detach_keys,omitempty"` - - // SDNotify tells Libpod to allow containers to notify the host systemd of - // readiness using the SD_NOTIFY mechanism. - SDNotify bool `toml:",omitempty"` - - // CgroupCheck indicates the configuration has been rewritten after an - // upgrade to Fedora 31 to change the default OCI runtime for cgroupv2v2. - CgroupCheck bool `toml:"cgroup_check,omitempty"` -} - -// newLibpodConfig creates a new ConfigFromLibpod and converts it to Config. -// Depending if we're running as root or rootless, we then merge the system configuration followed -// by merging the default config (hard-coded default in memory). -// Note that the OCI runtime is hard-set to `crun` if we're running on a system -// with cgroupv2v2. Other OCI runtimes are not yet supporting cgroupv2v2. This -// might change in the future. -func newLibpodConfig(c *Config) error { - // Start with the default config and iteratively merge - // fields in the system configs. - config := c.libpodConfig() - - // Now, check if the user can access system configs and merge them if needed. - configs, err := systemLibpodConfigs() - if err != nil { - return errors.Wrapf(err, "error finding config on system") - } - - if len(configs) == 0 { - return nil - } - - for _, path := range configs { - config, err = readLibpodConfigFromFile(path, config) - if err != nil { - return errors.Wrapf(err, "error reading system config %q", path) - } - } - - // Since runc does not currently support cgroupV2 - // Change to default crun on first running of libpod.conf - // TODO Once runc has support for cgroupv2, this function should be removed. - if !config.CgroupCheck && unshare.IsRootless() { - cgroup2, err := cgroupv2.Enabled() - if err != nil { - return err - } - if cgroup2 { - path, err := exec.LookPath("crun") - if err != nil { - // Can't find crun path so do nothing - logrus.Warnf("Can not find crun package on the host, containers might fail to run on cgroup V2 systems without crun: %q", err) - } else { - config.CgroupCheck = true - config.OCIRuntime = path - } - } - } - - // hard code EventsLogger to "file" to match older podman versions. - if config.EventsLogger != "file" { - logrus.Warnf("Ignoring libpod.conf EventsLogger setting %q. Use %q if you want to change this setting and remove libpod.conf files.", config.EventsLogger, Path()) - config.EventsLogger = "file" - } - - c.libpodToContainersConfig(config) - - return nil -} - -// readConfigFromFile reads the specified config file at `path` and attempts to -// unmarshal its content into a Config. The config param specifies the previous -// default config. If the path, only specifies a few fields in the Toml file -// the defaults from the config parameter will be used for all other fields. -func readLibpodConfigFromFile(path string, config *ConfigFromLibpod) (*ConfigFromLibpod, error) { - logrus.Debugf("Reading configuration file %q", path) - _, err := toml.DecodeFile(path, config) - if err != nil { - return nil, errors.Wrapf(err, "decode configuration %s", path) - } - - return config, err -} - -func systemLibpodConfigs() ([]string, error) { - if unshare.IsRootless() { - path, err := rootlessLibpodConfigPath() - if err != nil { - return nil, err - } - if _, err := os.Stat(path); err == nil { - containersConfPath, err := rootlessConfigPath() - if err != nil { - containersConfPath = filepath.Join("$HOME", UserOverrideContainersConfig) - } - logrus.Warnf("Found deprecated file %s, please remove. Use %s to override defaults.\n", path, containersConfPath) - return []string{path}, nil - } - return nil, err - } - - configs := []string{} - if _, err := os.Stat(_rootConfigPath); err == nil { - logrus.Warnf("Found deprecated file %s, please remove. Use %s to override defaults.\n", _rootConfigPath, OverrideContainersConfig) - configs = append(configs, _rootConfigPath) - } - if _, err := os.Stat(_rootOverrideConfigPath); err == nil { - logrus.Warnf("Found deprecated file %s, please remove. Use %s to override defaults.\n", _rootOverrideConfigPath, OverrideContainersConfig) - configs = append(configs, _rootOverrideConfigPath) - } - return configs, nil -} - -func rootlessLibpodConfigPath() (string, error) { - home, err := unshare.HomeDir() - if err != nil { - return "", err - } - - return filepath.Join(home, _rootlessConfigPath), nil -} - -func (c *Config) libpodConfig() *ConfigFromLibpod { - return &ConfigFromLibpod{ - InitPath: c.Containers.InitPath, - MaxLogSize: c.Containers.LogSizeMax, - EnableLabeling: c.Containers.EnableLabeling, - - SetOptions: c.Engine.SetOptions, - VolumePath: c.Engine.VolumePath, - ImageDefaultTransport: c.Engine.ImageDefaultTransport, - OCIRuntime: c.Engine.OCIRuntime, - OCIRuntimes: c.Engine.OCIRuntimes, - RuntimeSupportsJSON: c.Engine.RuntimeSupportsJSON, - RuntimeSupportsNoCgroups: c.Engine.RuntimeSupportsNoCgroups, - RuntimePath: c.Engine.RuntimePath, - ConmonPath: c.Engine.ConmonPath, - ConmonEnvVars: c.Engine.ConmonEnvVars, - CgroupManager: c.Engine.CgroupManager, - StaticDir: c.Engine.StaticDir, - TmpDir: c.Engine.TmpDir, - NoPivotRoot: c.Engine.NoPivotRoot, - HooksDir: c.Engine.HooksDir, - Namespace: c.Engine.Namespace, - InfraImage: c.Engine.InfraImage, - InfraCommand: c.Engine.InfraCommand, - EnablePortReservation: c.Engine.EnablePortReservation, - NetworkCmdPath: c.Engine.NetworkCmdPath, - NumLocks: c.Engine.NumLocks, - LockType: c.Engine.LockType, - EventsLogger: c.Engine.EventsLogger, - EventsLogFilePath: c.Engine.EventsLogFilePath, - DetachKeys: c.Engine.DetachKeys, - SDNotify: c.Engine.SDNotify, - CgroupCheck: c.Engine.CgroupCheck, - SignaturePolicyPath: c.Engine.SignaturePolicyPath, - - CNIConfigDir: c.Network.NetworkConfigDir, - CNIPluginDir: c.Network.CNIPluginDirs, - CNIDefaultNetwork: c.Network.DefaultNetwork, - } -} - -func (c *Config) libpodToContainersConfig(libpodConf *ConfigFromLibpod) { - - if libpodConf.InitPath != "" { - c.Containers.InitPath = libpodConf.InitPath - } - c.Containers.LogSizeMax = libpodConf.MaxLogSize - c.Containers.EnableLabeling = libpodConf.EnableLabeling - - if libpodConf.SignaturePolicyPath != "" { - c.Engine.SignaturePolicyPath = libpodConf.SignaturePolicyPath - } - c.Engine.SetOptions = libpodConf.SetOptions - if libpodConf.VolumePath != "" { - c.Engine.VolumePath = libpodConf.VolumePath - } - if libpodConf.ImageDefaultTransport != "" { - c.Engine.ImageDefaultTransport = libpodConf.ImageDefaultTransport - } - if libpodConf.OCIRuntime != "" { - c.Engine.OCIRuntime = libpodConf.OCIRuntime - } - c.Engine.OCIRuntimes = libpodConf.OCIRuntimes - c.Engine.RuntimeSupportsJSON = libpodConf.RuntimeSupportsJSON - c.Engine.RuntimeSupportsNoCgroups = libpodConf.RuntimeSupportsNoCgroups - c.Engine.RuntimePath = libpodConf.RuntimePath - c.Engine.ConmonPath = libpodConf.ConmonPath - c.Engine.ConmonEnvVars = libpodConf.ConmonEnvVars - if libpodConf.CgroupManager != "" { - c.Engine.CgroupManager = libpodConf.CgroupManager - } - if libpodConf.StaticDir != "" { - c.Engine.StaticDir = libpodConf.StaticDir - } - if libpodConf.TmpDir != "" { - c.Engine.TmpDir = libpodConf.TmpDir - } - c.Engine.NoPivotRoot = libpodConf.NoPivotRoot - c.Engine.HooksDir = libpodConf.HooksDir - if libpodConf.Namespace != "" { - c.Engine.Namespace = libpodConf.Namespace - } - if libpodConf.InfraImage != "" { - c.Engine.InfraImage = libpodConf.InfraImage - } - if libpodConf.InfraCommand != "" { - c.Engine.InfraCommand = libpodConf.InfraCommand - } - - c.Engine.EnablePortReservation = libpodConf.EnablePortReservation - if libpodConf.NetworkCmdPath != "" { - c.Engine.NetworkCmdPath = libpodConf.NetworkCmdPath - } - c.Engine.NumLocks = libpodConf.NumLocks - c.Engine.LockType = libpodConf.LockType - if libpodConf.EventsLogger != "" { - c.Engine.EventsLogger = libpodConf.EventsLogger - } - if libpodConf.EventsLogFilePath != "" { - c.Engine.EventsLogFilePath = libpodConf.EventsLogFilePath - } - if libpodConf.DetachKeys != "" { - c.Engine.DetachKeys = libpodConf.DetachKeys - } - c.Engine.SDNotify = libpodConf.SDNotify - c.Engine.CgroupCheck = libpodConf.CgroupCheck - - if libpodConf.CNIConfigDir != "" { - c.Network.NetworkConfigDir = libpodConf.CNIConfigDir - } - c.Network.CNIPluginDirs = libpodConf.CNIPluginDir - if libpodConf.CNIDefaultNetwork != "" { - c.Network.DefaultNetwork = libpodConf.CNIDefaultNetwork - } -} diff --git a/vendor/github.com/containers/common/version/version.go b/vendor/github.com/containers/common/version/version.go index 72f4e00f7..8df453484 100644 --- a/vendor/github.com/containers/common/version/version.go +++ b/vendor/github.com/containers/common/version/version.go @@ -1,4 +1,4 @@ package version // Version is the version of the build. -const Version = "0.29.0" +const Version = "0.31.0" diff --git a/vendor/github.com/cri-o/ocicni/pkg/ocicni/ocicni.go b/vendor/github.com/cri-o/ocicni/pkg/ocicni/ocicni.go index ee09adc8d..7a9f97d1c 100644 --- a/vendor/github.com/cri-o/ocicni/pkg/ocicni/ocicni.go +++ b/vendor/github.com/cri-o/ocicni/pkg/ocicni/ocicni.go @@ -149,7 +149,7 @@ func (plugin *cniNetworkPlugin) monitorConfDir(start *sync.WaitGroup) { for { select { case event := <-plugin.watcher.Events: - logrus.Warningf("CNI monitoring event %v", event) + logrus.Infof("CNI monitoring event %v", event) var defaultDeleted bool createWrite := (event.Op&fsnotify.Create == fsnotify.Create || @@ -295,7 +295,7 @@ func loadNetworks(confDir string, cni *libcni.CNIConfig) (map[string]*cniNetwork } } if len(confList.Plugins) == 0 { - logrus.Warningf("CNI config list %s has no networks, skipping", confFile) + logrus.Infof("CNI config list %s has no networks, skipping", confFile) continue } @@ -350,7 +350,7 @@ func (plugin *cniNetworkPlugin) syncNetworkConfig() error { plugin.defaultNetName.name = defaultNetName logrus.Infof("Update default CNI network name to %s", defaultNetName) } else { - logrus.Warnf("Default CNI network name %s is unchangeable", plugin.defaultNetName.name) + logrus.Debugf("Default CNI network name %s is unchangeable", plugin.defaultNetName.name) } plugin.networks = networks diff --git a/vendor/modules.txt b/vendor/modules.txt index 9de30e604..3ad53c73c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -86,7 +86,7 @@ github.com/containers/buildah/pkg/parse github.com/containers/buildah/pkg/rusage github.com/containers/buildah/pkg/supplemented github.com/containers/buildah/util -# github.com/containers/common v0.29.0 +# github.com/containers/common v0.31.0 github.com/containers/common/pkg/apparmor github.com/containers/common/pkg/apparmor/internal/supported github.com/containers/common/pkg/auth @@ -220,7 +220,7 @@ github.com/coreos/go-systemd/v22/dbus github.com/coreos/go-systemd/v22/internal/dlopen github.com/coreos/go-systemd/v22/journal github.com/coreos/go-systemd/v22/sdjournal -# github.com/cri-o/ocicni v0.2.1-0.20201102180012-75c612fda1a2 => github.com/cri-o/ocicni v0.2.1-0.20201109200316-afdc16ba66df +# github.com/cri-o/ocicni v0.2.1-0.20201125151022-df072ea5421c github.com/cri-o/ocicni/pkg/ocicni # github.com/cyphar/filepath-securejoin v0.2.2 github.com/cyphar/filepath-securejoin |