diff options
65 files changed, 1569 insertions, 901 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 10470e14e..cd6bac265 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -31,9 +31,9 @@ env: #### #### Cache-image names to test with ### - FEDORA_CACHE_IMAGE_NAME: "fedora-30-libpod-5925244995371008" - PRIOR_FEDORA_CACHE_IMAGE_NAME: "fedora-29-libpod-5925244995371008" - UBUNTU_CACHE_IMAGE_NAME: "ubuntu-18-libpod-5925244995371008" + FEDORA_CACHE_IMAGE_NAME: "fedora-30-libpod-5479475851034624" + PRIOR_FEDORA_CACHE_IMAGE_NAME: "fedora-29-libpod-5479475851034624" + UBUNTU_CACHE_IMAGE_NAME: "ubuntu-18-libpod-5479475851034624" #### #### Variables for composing new cache-images (used in PR testing) from @@ -43,7 +43,7 @@ env: # Git commits to use while building dependencies into cache-images FEDORA_CNI_COMMIT: "412b6d31280682bb4fab4446f113c22ff1886554" CNI_COMMIT: "7480240de9749f9a0a5c8614b17f1f03e0c06ab9" - CONMON_COMMIT: "3e47d8dd45cdd973dbe256292d5e9c0bff195e56" + CONMON_COMMIT: "6f3572558b97bc60dd8f8c7f0807748e6ce2c440" CRIU_COMMIT: "c74b83cd49c00589c0c0468ba5fe685b67fdbd0a" # Special image w/ nested-libvirt + tools for creating new cache and base images IMAGE_BUILDER_CACHE_IMAGE_NAME: "image-builder-image-1541772081" @@ -103,7 +103,7 @@ gating_task: # N/B: entrypoint.sh resets $GOSRC (same as make clean) - '/usr/local/bin/entrypoint.sh install.tools |& ${TIMESTAMP}' - '/usr/local/bin/entrypoint.sh validate |& ${TIMESTAMP}' - - '/usr/local/bin/entrypoint.sh lint |& ${TIMESTAMP}' + - '/usr/local/bin/entrypoint.sh golangci-lint |& ${TIMESTAMP}' # This task builds Podman with different buildtags to ensure the build does # not break. It also verifies all sub-commands have man pages. diff --git a/.copr/prepare.sh b/.copr/prepare.sh index 02444efa2..d8ad34d08 100644 --- a/.copr/prepare.sh +++ b/.copr/prepare.sh @@ -29,4 +29,4 @@ fi mkdir build/ git archive --prefix "libpod-${COMMIT_SHORT}/" --format "tar.gz" HEAD -o "build/libpod-${COMMIT_SHORT}.tar.gz" git clone https://github.com/containers/conmon -cd conmon && git checkout 3e47d8dd45cdd973dbe256292d5e9c0bff195e56 && git archive --prefix "conmon/" --format "tar.gz" HEAD -o "../build/conmon.tar.gz" +cd conmon && git checkout 6f3572558b97bc60dd8f8c7f0807748e6ce2c440 && git archive --prefix "conmon/" --format "tar.gz" HEAD -o "../build/conmon.tar.gz" @@ -41,6 +41,8 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in [func Diff(name: string) DiffInfo](#Diff) +[func ExecContainer(opts: ExecOpts) ](#ExecContainer) + [func ExportContainer(name: string, path: string) string](#ExportContainer) [func ExportImage(name: string, destination: string, compress: bool, tags: []string) string](#ExportImage) @@ -203,6 +205,8 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in [type Event](#Event) +[type ExecOpts](#ExecOpts) + [type Image](#Image) [type ImageHistory](#ImageHistory) @@ -439,6 +443,11 @@ $ varlink call -m unix:/run/podman/io.podman/io.podman.DeleteUnusedImages method Diff(name: [string](https://godoc.org/builtin#string)) [DiffInfo](#DiffInfo)</div> Diff returns a diff between libpod objects +### <a name="ExecContainer"></a>func ExecContainer +<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;"> + +method ExecContainer(opts: [ExecOpts](#ExecOpts)) </div> +ExecContainer executes a command in the given container. ### <a name="ExportContainer"></a>func ExportContainer <div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;"> @@ -1565,6 +1574,23 @@ status [string](https://godoc.org/builtin#string) time [string](https://godoc.org/builtin#string) type [string](https://godoc.org/builtin#string) +### <a name="ExecOpts"></a>type ExecOpts + + + +name [string](https://godoc.org/builtin#string) + +tty [bool](https://godoc.org/builtin#bool) + +privileged [bool](https://godoc.org/builtin#bool) + +cmd [[]string](#[]string) + +user [?string](#?string) + +workdir [?string](#?string) + +env [?[]string](#?[]string) ### <a name="Image"></a>type Image diff --git a/Dockerfile b/Dockerfile index cc3ee00e0..ca7807fa1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,7 +56,7 @@ RUN set -x \ && rm -rf "$GOPATH" # Install conmon -ENV CONMON_COMMIT 3e47d8dd45cdd973dbe256292d5e9c0bff195e56 +ENV CONMON_COMMIT 6f3572558b97bc60dd8f8c7f0807748e6ce2c440 RUN set -x \ && export GOPATH="$(mktemp -d)" \ && git clone https://github.com/containers/conmon.git "$GOPATH/src/github.com/containers/conmon.git" \ diff --git a/Dockerfile.centos b/Dockerfile.centos index 65dbefb38..bbb582840 100644 --- a/Dockerfile.centos +++ b/Dockerfile.centos @@ -50,7 +50,7 @@ RUN set -x \ && go get github.com/onsi/gomega/... # Install conmon -ENV CONMON_COMMIT 3e47d8dd45cdd973dbe256292d5e9c0bff195e56 +ENV CONMON_COMMIT 6f3572558b97bc60dd8f8c7f0807748e6ce2c440 RUN set -x \ && export GOPATH="$(mktemp -d)" \ && git clone https://github.com/containers/conmon.git "$GOPATH/src/github.com/containers/conmon.git" \ diff --git a/Dockerfile.fedora b/Dockerfile.fedora index dafd3b6ad..54ab690e9 100644 --- a/Dockerfile.fedora +++ b/Dockerfile.fedora @@ -53,7 +53,7 @@ RUN set -x \ && go get github.com/onsi/gomega/... # Install conmon -ENV CONMON_COMMIT 3e47d8dd45cdd973dbe256292d5e9c0bff195e56 +ENV CONMON_COMMIT 6f3572558b97bc60dd8f8c7f0807748e6ce2c440 RUN set -x \ && export GOPATH="$(mktemp -d)" \ && git clone https://github.com/containers/conmon.git "$GOPATH/src/github.com/containers/conmon.git" \ @@ -136,6 +136,9 @@ lint: .gopathok varlink_generate ## Execute the source code linter @echo "checking lint" @./.tool/lint +golangci-lint: .gopathok varlink_generate .install.golangci-lint + $(GOBIN)/golangci-lint run --tests=false + gofmt: ## Verify the source code gofmt find . -name '*.go' ! -path './vendor/*' -exec gofmt -s -w {} \+ git diff --exit-code @@ -360,7 +363,7 @@ uninstall: GIT_CHECK_EXCLUDE="./vendor" $(GOBIN)/git-validation -v -run DCO,short-subject,dangling-whitespace -range $(EPOCH_TEST_COMMIT)..$(HEAD) .PHONY: install.tools -install.tools: .install.gitvalidation .install.gometalinter .install.md2man .install.ginkgo ## Install needed tools +install.tools: .install.gitvalidation .install.gometalinter .install.md2man .install.ginkgo .install.golangci-lint ## Install needed tools define go-get env GO111MODULE=off \ @@ -386,6 +389,11 @@ endef $(GOBIN)/gometalinter --install; \ fi +.install.golangci-lint: .gopathok + if [ ! -x "$(GOBIN)/golangci-lint" ]; then \ + curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOBIN)/ v1.17.1; \ + fi + .install.md2man: .gopathok if [ ! -x "$(GOBIN)/go-md2man" ]; then \ $(call go-get,github.com/cpuguy83/go-md2man); \ @@ -444,6 +452,7 @@ vendor: gofmt \ help \ install \ + golangci-lint \ lint \ pause \ uninstall \ diff --git a/cmd/podman/build.go b/cmd/podman/build.go index 5ee35dea0..19337985d 100644 --- a/cmd/podman/build.go +++ b/cmd/podman/build.go @@ -308,16 +308,6 @@ func buildCmd(c *cliconfig.BuildValues) error { return runtime.Build(getContext(), c, options, dockerfiles) } -// Tail returns a string slice after the first element unless there are -// not enough elements, then it returns an empty slice. This is to replace -// the urfavecli Tail method for args -func Tail(a []string) []string { - if len(a) >= 2 { - return a[1:] - } - return []string{} -} - // useLayers returns false if BUILDAH_LAYERS is set to "0" or "false" // otherwise it returns true func useLayers() string { diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index be380cda0..025f40cf6 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -119,14 +119,15 @@ type DiffValues struct { type ExecValues struct { PodmanCommand - Env []string - Privileged bool - Interfactive bool - Tty bool - User string - Latest bool - Workdir string - PreserveFDs int + DetachKeys string + Env []string + Privileged bool + Interactive bool + Tty bool + User string + Latest bool + Workdir string + PreserveFDs int } type ImageExistsValues struct { diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index 9229191ff..27f3fc214 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -11,7 +11,6 @@ const remoteclient = false // Commands that the local client implements func getMainCommands() []*cobra.Command { rootCommands := []*cobra.Command{ - _execCommand, _playCommand, _loginCommand, _logoutCommand, @@ -41,7 +40,6 @@ func getContainerSubCommands() []*cobra.Command { return []*cobra.Command{ _cleanupCommand, - _execCommand, _mountCommand, _refreshCommand, _runlabelCommand, diff --git a/cmd/podman/container.go b/cmd/podman/container.go index cb54317c0..557f5fafa 100644 --- a/cmd/podman/container.go +++ b/cmd/podman/container.go @@ -57,6 +57,7 @@ var ( _contInspectSubCommand, _cpCommand, _diffCommand, + _execCommand, _exportCommand, _createCommand, _initCommand, diff --git a/cmd/podman/exec.go b/cmd/podman/exec.go index 799ed9f38..2e9b9e47e 100644 --- a/cmd/podman/exec.go +++ b/cmd/podman/exec.go @@ -35,8 +35,9 @@ func init() { execCommand.SetUsageTemplate(UsageTemplate()) flags := execCommand.Flags() flags.SetInterspersed(false) + flags.StringVar(&execCommand.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container. Format is a single character [a-Z] or ctrl-<value> where <value> is one of: a-z, @, ^, [, , or _") flags.StringArrayVarP(&execCommand.Env, "env", "e", []string{}, "Set environment variables") - flags.BoolVarP(&execCommand.Interfactive, "interactive", "i", false, "Not supported. All exec commands are interactive by default") + flags.BoolVarP(&execCommand.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached") flags.BoolVarP(&execCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of") flags.BoolVar(&execCommand.Privileged, "privileged", false, "Give the process extended Linux capabilities inside the container. The default is false") flags.BoolVarP(&execCommand.Tty, "tty", "t", false, "Allocate a pseudo-TTY. The default is false") @@ -45,30 +46,35 @@ func init() { flags.IntVar(&execCommand.PreserveFDs, "preserve-fds", 0, "Pass N additional file descriptors to the container") flags.StringVarP(&execCommand.Workdir, "workdir", "w", "", "Working directory inside the container") markFlagHiddenForRemoteClient("latest", flags) + markFlagHiddenForRemoteClient("preserve-fds", flags) } func execCmd(c *cliconfig.ExecValues) error { - args := c.InputArgs - argStart := 1 - if len(args) < 1 && !c.Latest { - return errors.Errorf("you must provide one container name or id") - } - if len(args) < 2 && !c.Latest { - return errors.Errorf("you must provide a command to exec") - } + argLen := len(c.InputArgs) if c.Latest { - argStart = 0 + if argLen < 1 { + return errors.Errorf("you must provide a command to exec") + } + } else { + if argLen < 1 { + return errors.Errorf("you must provide one container name or id") + } + if argLen < 2 { + return errors.Errorf("you must provide a command to exec") + } } - cmd := args[argStart:] runtime, err := adapter.GetRuntimeNoStore(getContext(), &c.PodmanCommand) if err != nil { return errors.Wrapf(err, "error creating libpod runtime") } defer runtime.DeferredShutdown(false) - err = runtime.Exec(c, cmd) - if errors.Cause(err) == define.ErrCtrStateInvalid { + exitCode, err = runtime.ExecContainer(getContext(), c) + if errors.Cause(err) == define.ErrOCIRuntimePermissionDenied { exitCode = 126 } + if errors.Cause(err) == define.ErrOCIRuntimeNotFound { + exitCode = 127 + } return err } diff --git a/cmd/podman/healthcheck_run.go b/cmd/podman/healthcheck_run.go index 66ca4580a..3a2a8f333 100644 --- a/cmd/podman/healthcheck_run.go +++ b/cmd/podman/healthcheck_run.go @@ -44,6 +44,9 @@ func healthCheckCmd(c *cliconfig.HealthCheckValues) error { } defer runtime.DeferredShutdown(false) status, err := runtime.HealthCheck(c) + if err == nil && status == "unhealthy" { + exitCode = 1 + } fmt.Println(status) return err } diff --git a/cmd/podman/images.go b/cmd/podman/images.go index 3b32ee3dd..fe7c89b5c 100644 --- a/cmd/podman/images.go +++ b/cmd/podman/images.go @@ -51,7 +51,6 @@ type imagesOptions struct { outputformat string sort string all bool - useReadOnly bool } // Type declaration and functions for sorting the images output diff --git a/cmd/podman/main.go b/cmd/podman/main.go index a8478bda6..72d1754ac 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -35,6 +35,7 @@ var mainCommands = []*cobra.Command{ _diffCommand, _createCommand, _eventsCommand, + _execCommand, _exportCommand, _generateCommand, _historyCommand, diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink index 72b15c328..cc66e7df0 100644 --- a/cmd/podman/varlink/io.podman.varlink +++ b/cmd/podman/varlink/io.podman.varlink @@ -500,6 +500,23 @@ type DiffInfo( changeType: string ) +type ExecOpts( + # container name or id + name: string, + # Create pseudo tty + tty: bool, + # privileged access in container + privileged: bool, + # command to execute in container + cmd: []string, + # user to use in container + user: ?string, + # workdir to run command in container + workdir: ?string, + # slice of keyword=value environment variables + env: ?[]string +) + # GetVersion returns version and build information of the podman service method GetVersion() -> ( version: string, @@ -1100,6 +1117,9 @@ method ContainerRestore(name: string, keep: bool, tcpEstablished: bool) -> (id: # ContainerRunlabel runs executes a command as described by a given container image label. method ContainerRunlabel(runlabel: Runlabel) -> () +# ExecContainer executes a command in the given container. +method ExecContainer(opts: ExecOpts) -> () + # ListContainerMounts gathers all the mounted container mount points and returns them as an array # of strings # #### Example diff --git a/completions/bash/podman b/completions/bash/podman index 969f1054e..c8f00a3cb 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1234,6 +1234,7 @@ _podman_diff() { _podman_exec() { local options_with_args=" + --detach-keys -e --env --user diff --git a/contrib/podmanimage/README.md b/contrib/podmanimage/README.md index 79484d4a3..3dc07ad63 100644 --- a/contrib/podmanimage/README.md +++ b/contrib/podmanimage/README.md @@ -30,7 +30,7 @@ mkdir /var/lib/mycontainer # Run the image detached using the host's network in a container name # podmanctr, turn off label and seccomp confinement in the container # and then do a little shell hackery to keep the container up and running. -podman run --detach --name=podmanctr --net=host --security-opt label=disable --security-opt seccomp=unconfined --device /dev/fuse:rw -v /var/lib/mycontainer:/var/lib/containers:Z --privileged stable sh -c 'while true ;do wait; done' +podman run --detach --name=podmanctr --net=host --security-opt label=disable --security-opt seccomp=unconfined --device /dev/fuse:rw -v /var/lib/mycontainer:/var/lib/containers:Z --privileged stable sh -c 'while true ;do sleep 100000 ; done' podman exec -it podmanctr /bin/sh diff --git a/contrib/spec/podman.spec.in b/contrib/spec/podman.spec.in index ce5901f69..9d8467783 100644 --- a/contrib/spec/podman.spec.in +++ b/contrib/spec/podman.spec.in @@ -35,7 +35,7 @@ # People want conmon packaged with the copr rpm %global import_path_conmon github.com/containers/conmon %global git_conmon https://%{import_path_conmon} -%global commit_conmon 3e47d8dd45cdd973dbe256292d5e9c0bff195e56 +%global commit_conmon 6f3572558b97bc60dd8f8c7f0807748e6ce2c440 %global shortcommit_conmon %(c=%{commit_conmon}; echo ${c:0:7}) Name: podman diff --git a/docs/podman-exec.1.md b/docs/podman-exec.1.md index 07d24e295..ab8d75626 100644 --- a/docs/podman-exec.1.md +++ b/docs/podman-exec.1.md @@ -10,19 +10,24 @@ podman\-exec - Execute a command in a running container **podman exec** executes a command in a running container. ## OPTIONS + +**--detach-keys**=*sequence* + +Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-<value>` where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. + **--env**, **-e** You may specify arbitrary environment variables that are available for the command to be executed. -**--interactive**, **-i** +**--interactive**, **-i**=*true|false* -Not supported. All exec commands are interactive by default. +When set to true, keep stdin open even if not attached. The default is *false*. **--latest**, **-l** Instead of providing the container name or ID, use the last created container. If you use methods other than Podman -to run containers such as CRI-O, the last started container could be from either of those methods. +to run containers such as CRI-O, the last started container could be from either of those methods. The latest option is not supported on the remote client. diff --git a/docs/podman-run.1.md b/docs/podman-run.1.md index 6d95d6045..d6c7ae055 100644 --- a/docs/podman-run.1.md +++ b/docs/podman-run.1.md @@ -366,9 +366,7 @@ Path to the container-init binary. **--interactive**, **-i**=*true|false* -Keep STDIN open even if not attached. The default is *false*. - -When set to true, keep stdin open even if not attached. The default is false. +When set to true, keep stdin open even if not attached. The default is *false*. **--ip6**=*ip* @@ -18,7 +18,7 @@ require ( github.com/containernetworking/cni v0.7.1 github.com/containernetworking/plugins v0.8.1 github.com/containers/buildah v1.9.0 - github.com/containers/image v2.0.0+incompatible + github.com/containers/image v2.0.1+incompatible github.com/containers/psgo v1.3.1 github.com/containers/storage v1.12.13 github.com/coreos/bbolt v1.3.3 // indirect @@ -70,6 +70,8 @@ github.com/containers/buildah v1.9.0 h1:ktVRCGNoVfW8PlTuCKUeh+zGdqn1Nik80DSWvGX+ github.com/containers/buildah v1.9.0/go.mod h1:1CsiLJvyU+h+wOjnqJJOWuJCVcMxZOr5HN/gHGdzJxY= github.com/containers/image v2.0.0+incompatible h1:FTr6Br7jlIKNCKMjSOMbAxKp2keQ0//jzJaYNTVhauk= github.com/containers/image v2.0.0+incompatible/go.mod h1:8Vtij257IWSanUQKe1tAeNOm2sRVkSqQTVQ1IlwI3+M= +github.com/containers/image v2.0.1+incompatible h1:w39mlElA/aSFZ6moFa5N+A4MWu9c8hgdMiMMYnH94Hs= +github.com/containers/image v2.0.1+incompatible/go.mod h1:8Vtij257IWSanUQKe1tAeNOm2sRVkSqQTVQ1IlwI3+M= github.com/containers/psgo v1.3.0 h1:kDhiA4gNNyJ2qCzmOuBf6AmrF/Pp+6Jo98P68R7fB8I= github.com/containers/psgo v1.3.0/go.mod h1:7MELvPTW1fj6yMrwD9I1Iasx1vU+hKlRkHXAJ51sFtU= github.com/containers/psgo v1.3.1-0.20190626112706-fbef66e4ce92 h1:aVJs/Av0Yc9uNoWnIwmG+6Z+XozuRXFwvLwAOVmwlvI= diff --git a/hack/ostree_tag.sh b/hack/ostree_tag.sh index b08441ef9..06a5bc68c 100755 --- a/hack/ostree_tag.sh +++ b/hack/ostree_tag.sh @@ -2,5 +2,5 @@ if ! pkg-config glib-2.0 gobject-2.0 ostree-1 libselinux 2> /dev/null ; then echo containers_image_ostree_stub else - echo ostree + echo containers_image_ostree fi diff --git a/libpod/container_api.go b/libpod/container_api.go index 50fa1759d..0cce6ca22 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -2,16 +2,13 @@ package libpod import ( "context" - "fmt" "io" "io/ioutil" "os" - "strconv" "time" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/events" - "github.com/containers/libpod/pkg/lookup" "github.com/containers/storage/pkg/stringid" "github.com/docker/docker/oci/caps" "github.com/opentracing/opentracing-go" @@ -21,6 +18,11 @@ import ( "k8s.io/client-go/tools/remotecommand" ) +const ( + defaultExecExitCode = 125 + defaultExecExitCodeCannotInvoke = 126 +) + // Init creates a container in the OCI runtime func (c *Container) Init(ctx context.Context) (err error) { span, _ := opentracing.StartSpanFromContext(ctx, "containerInit") @@ -220,23 +222,19 @@ func (c *Container) Kill(signal uint) error { } // Exec starts a new process inside the container +// Returns an exit code and an error. If Exec was not able to exec in the container before a failure, an exit code of 126 is returned. +// If another generic error happens, an exit code of 125 is returned. +// Sometimes, the $RUNTIME exec call errors, and if that is the case, the exit code is the exit code of the call. +// Otherwise, the exit code will be the exit code of the executed call inside of the container. // TODO investigate allowing exec without attaching -func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir string, streams *AttachStreams, preserveFDs int) error { +func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir string, streams *AttachStreams, preserveFDs int, resize chan remotecommand.TerminalSize, detachKeys string) (int, error) { var capList []string - - locked := false if !c.batched { - locked = true - c.lock.Lock() - defer func() { - if locked { - c.lock.Unlock() - } - }() + defer c.lock.Unlock() if err := c.syncContainer(); err != nil { - return err + return defaultExecExitCodeCannotInvoke, err } } @@ -244,25 +242,13 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir // TODO can probably relax this once we track exec sessions if conState != define.ContainerStateRunning { - return errors.Wrapf(define.ErrCtrStateInvalid, "cannot exec into container that is not running") + return defaultExecExitCodeCannotInvoke, errors.Wrapf(define.ErrCtrStateInvalid, "cannot exec into container that is not running") } + if privileged || c.config.Privileged { capList = caps.GetAllCapabilities() } - // If user was set, look it up in the container to get a UID to use on - // the host - hostUser := "" - if user != "" { - execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, user, nil) - if err != nil { - return err - } - - // runc expects user formatted as uid:gid - hostUser = fmt.Sprintf("%d:%d", execUser.Uid, execUser.Gid) - } - // Generate exec session ID // Ensure we don't conflict with an existing session ID sessionID := stringid.GenerateNonCryptoID() @@ -282,55 +268,27 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir } logrus.Debugf("Creating new exec session in container %s with session id %s", c.ID(), sessionID) - - execCmd, err := c.ociRuntime.execContainer(c, cmd, capList, env, tty, workDir, hostUser, sessionID, streams, preserveFDs) - if err != nil { - return errors.Wrapf(err, "error exec %s", c.ID()) + if err := c.createExecBundle(sessionID); err != nil { + return defaultExecExitCodeCannotInvoke, err } - chWait := make(chan error) - go func() { - chWait <- execCmd.Wait() - close(chWait) - }() - pidFile := c.execPidPath(sessionID) - // 60 second seems a reasonable time to wait - // https://github.com/containers/libpod/issues/1495 - // https://github.com/containers/libpod/issues/1816 - const pidWaitTimeout = 60000 - - // Wait until the runtime makes the pidfile - exited, err := WaitForFile(pidFile, chWait, pidWaitTimeout*time.Millisecond) - if err != nil { - if exited { - // If the runtime exited, propagate the error we got from the process. - // We need to remove PID files to ensure no memory leaks - if err2 := os.Remove(pidFile); err2 != nil { - logrus.Errorf("Error removing exit file for container %s exec session %s: %v", c.ID(), sessionID, err2) - } - - return err + defer func() { + // cleanup exec bundle + if err := c.cleanupExecBundle(sessionID); err != nil { + logrus.Errorf("Error removing exec session %s bundle path for container %s: %v", sessionID, c.ID(), err) } - return errors.Wrapf(err, "timed out waiting for runtime to create pidfile for exec session in container %s", c.ID()) - } + }() - // Pidfile exists, read it - contents, err := ioutil.ReadFile(pidFile) - // We need to remove PID files to ensure no memory leaks - if err2 := os.Remove(pidFile); err2 != nil { - logrus.Errorf("Error removing exit file for container %s exec session %s: %v", c.ID(), sessionID, err2) - } - if err != nil { - // We don't know the PID of the exec session - // However, it may still be alive - // TODO handle this better - return errors.Wrapf(err, "could not read pidfile for exec session %s in container %s", sessionID, c.ID()) - } - pid, err := strconv.ParseInt(string(contents), 10, 32) + pid, attachChan, err := c.ociRuntime.execContainer(c, cmd, capList, env, tty, workDir, user, sessionID, streams, preserveFDs, resize, detachKeys) if err != nil { - // As above, we don't have a valid PID, but the exec session is likely still alive - // TODO handle this better - return errors.Wrapf(err, "error parsing PID of exec session %s in container %s", sessionID, c.ID()) + ec := defaultExecExitCode + // Conmon will pass a non-zero exit code from the runtime as a pid here. + // we differentiate a pid with an exit code by sending it as negative, so reverse + // that change and return the exit code the runtime failed with. + if pid < 0 { + ec = -1 * pid + } + return ec, err } // We have the PID, add it to state @@ -340,12 +298,12 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir session := new(ExecSession) session.ID = sessionID session.Command = cmd - session.PID = int(pid) + session.PID = pid c.state.ExecSessions[sessionID] = session if err := c.save(); err != nil { // Now we have a PID but we can't save it in the DB // TODO handle this better - return errors.Wrapf(err, "error saving exec sessions %s for container %s", sessionID, c.ID()) + return defaultExecExitCode, errors.Wrapf(err, "error saving exec sessions %s for container %s", sessionID, c.ID()) } c.newContainerEvent(events.Exec) logrus.Debugf("Successfully started exec session %s in container %s", sessionID, c.ID()) @@ -353,23 +311,33 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir // Unlock so other processes can use the container if !c.batched { c.lock.Unlock() - locked = false } - var waitErr error - if !exited { - waitErr = <-chWait + lastErr := <-attachChan + + exitCode, err := c.readExecExitCode(sessionID) + if err != nil { + if lastErr != nil { + logrus.Errorf(lastErr.Error()) + } + lastErr = err + } + if exitCode != 0 { + if lastErr != nil { + logrus.Errorf(lastErr.Error()) + } + lastErr = errors.Wrapf(define.ErrOCIRuntime, "non zero exit code: %d", exitCode) } // Lock again if !c.batched { - locked = true c.lock.Lock() } // Sync the container again to pick up changes in state if err := c.syncContainer(); err != nil { - return errors.Wrapf(err, "error syncing container %s state to remove exec session %s", c.ID(), sessionID) + logrus.Errorf("error syncing container %s state to remove exec session %s", c.ID(), sessionID) + return exitCode, lastErr } // Remove the exec session from state @@ -377,7 +345,7 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir if err := c.save(); err != nil { logrus.Errorf("Error removing exec session %s from container %s state: %v", sessionID, c.ID(), err) } - return waitErr + return exitCode, lastErr } // AttachStreams contains streams that will be attached to the container diff --git a/libpod/container_attach_linux.go b/libpod/container_attach_linux.go deleted file mode 100644 index 837cbf916..000000000 --- a/libpod/container_attach_linux.go +++ /dev/null @@ -1,179 +0,0 @@ -//+build linux - -package libpod - -import ( - "fmt" - "io" - "net" - "os" - "path/filepath" - - "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/errorhandling" - "github.com/containers/libpod/pkg/kubeutils" - "github.com/containers/libpod/utils" - "github.com/docker/docker/pkg/term" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" - "k8s.io/client-go/tools/remotecommand" -) - -/* Sync with stdpipe_t in conmon.c */ -const ( - AttachPipeStdin = 1 - AttachPipeStdout = 2 - AttachPipeStderr = 3 -) - -// Attach to the given container -// Does not check if state is appropriate -func (c *Container) attach(streams *AttachStreams, keys string, resize <-chan remotecommand.TerminalSize, startContainer bool, started chan bool) error { - if !streams.AttachOutput && !streams.AttachError && !streams.AttachInput { - return errors.Wrapf(define.ErrInvalidArg, "must provide at least one stream to attach to") - } - - logrus.Debugf("Attaching to container %s", c.ID()) - - return c.attachContainerSocket(resize, keys, streams, startContainer, started) -} - -// attachContainerSocket connects to the container's attach socket and deals with the IO. -// started is only required if startContainer is true -// TODO add a channel to allow interrupting -func (c *Container) attachContainerSocket(resize <-chan remotecommand.TerminalSize, keys string, streams *AttachStreams, startContainer bool, started chan bool) error { - if startContainer && started == nil { - return errors.Wrapf(define.ErrInternal, "started chan not passed when startContainer set") - } - - // Use default detach keys when keys aren't passed or specified in libpod.conf - if len(keys) == 0 { - keys = DefaultDetachKeys - } - - // Check the validity of the provided keys - detachKeys := []byte{} - var err error - detachKeys, err = term.ToBytes(keys) - if err != nil { - return errors.Wrapf(err, "invalid detach keys") - } - - kubeutils.HandleResizing(resize, func(size remotecommand.TerminalSize) { - controlPath := filepath.Join(c.bundlePath(), "ctl") - controlFile, err := os.OpenFile(controlPath, unix.O_WRONLY, 0) - if err != nil { - logrus.Debugf("Could not open ctl file: %v", err) - return - } - defer errorhandling.CloseQuiet(controlFile) - - logrus.Debugf("Received a resize event: %+v", size) - if _, err = fmt.Fprintf(controlFile, "%d %d %d\n", 1, size.Height, size.Width); err != nil { - logrus.Warnf("Failed to write to control file to resize terminal: %v", err) - } - }) - - socketPath := c.AttachSocketPath() - - maxUnixLength := unixPathLength() - if maxUnixLength < len(socketPath) { - socketPath = socketPath[0:maxUnixLength] - } - - logrus.Debug("connecting to socket ", socketPath) - - conn, err := net.DialUnix("unixpacket", nil, &net.UnixAddr{Name: socketPath, Net: "unixpacket"}) - if err != nil { - return errors.Wrapf(err, "failed to connect to container's attach socket: %v", socketPath) - } - defer func() { - if err := conn.Close(); err != nil { - logrus.Errorf("unable to close socket: %q", err) - } - }() - - // If starting was requested, start the container and notify when that's - // done. - if startContainer { - if err := c.start(); err != nil { - return err - } - started <- true - } - - receiveStdoutError := make(chan error) - go func() { - receiveStdoutError <- redirectResponseToOutputStreams(streams.OutputStream, streams.ErrorStream, streams.AttachOutput, streams.AttachError, conn) - }() - - stdinDone := make(chan error) - go func() { - var err error - if streams.AttachInput { - _, err = utils.CopyDetachable(conn, streams.InputStream, detachKeys) - if err := conn.CloseWrite(); err != nil { - logrus.Error("failed to close write in attach") - } - } - stdinDone <- err - }() - - select { - case err := <-receiveStdoutError: - return err - case err := <-stdinDone: - if err == define.ErrDetach { - return err - } - if streams.AttachOutput || streams.AttachError { - return <-receiveStdoutError - } - } - return nil -} - -func redirectResponseToOutputStreams(outputStream, errorStream io.Writer, writeOutput, writeError bool, conn io.Reader) error { - var err error - buf := make([]byte, 8192+1) /* Sync with conmon STDIO_BUF_SIZE */ - for { - nr, er := conn.Read(buf) - if nr > 0 { - var dst io.Writer - var doWrite bool - switch buf[0] { - case AttachPipeStdout: - dst = outputStream - doWrite = writeOutput - case AttachPipeStderr: - dst = errorStream - doWrite = writeError - default: - logrus.Infof("Received unexpected attach type %+d", buf[0]) - } - if dst == nil { - return errors.New("output destination cannot be nil") - } - if doWrite { - nw, ew := dst.Write(buf[1:nr]) - if ew != nil { - err = ew - break - } - if nr != nw+1 { - err = io.ErrShortWrite - break - } - } - } - if er == io.EOF { - break - } - if er != nil { - err = er - break - } - } - return err -} diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index c4d2af66e..aee8c4657 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -963,24 +963,16 @@ func (c *Container) generateInspectContainerHostConfig(ctrSpec *spec.Spec, named } hostConfig.DnsOptions = make([]string, 0, len(c.config.DNSOption)) - for _, opt := range c.config.DNSOption { - hostConfig.DnsOptions = append(hostConfig.DnsOptions, opt) - } + hostConfig.DnsOptions = append(hostConfig.DnsOptions, c.config.DNSOption...) hostConfig.DnsSearch = make([]string, 0, len(c.config.DNSSearch)) - for _, search := range c.config.DNSSearch { - hostConfig.DnsSearch = append(hostConfig.DnsSearch, search) - } + hostConfig.DnsSearch = append(hostConfig.DnsSearch, c.config.DNSSearch...) hostConfig.ExtraHosts = make([]string, 0, len(c.config.HostAdd)) - for _, host := range c.config.HostAdd { - hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, host) - } + hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, c.config.HostAdd...) hostConfig.GroupAdd = make([]string, 0, len(c.config.Groups)) - for _, group := range c.config.Groups { - hostConfig.GroupAdd = append(hostConfig.GroupAdd, group) - } + hostConfig.GroupAdd = append(hostConfig.GroupAdd, c.config.Groups...) hostConfig.SecurityOpt = []string{} if ctrSpec.Process != nil { diff --git a/libpod/container_internal.go b/libpod/container_internal.go index ca27f5814..36732685b 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -31,7 +31,8 @@ import ( const ( // name of the directory holding the artifacts - artifactsDir = "artifacts" + artifactsDir = "artifacts" + execDirPermission = 0755 ) // rootFsSize gets the size of the container's root filesystem @@ -132,14 +133,91 @@ func (c *Container) AttachSocketPath() string { return filepath.Join(c.ociRuntime.socketsDir, c.ID(), "attach") } +// exitFilePath gets the path to the container's exit file +func (c *Container) exitFilePath() string { + return filepath.Join(c.ociRuntime.exitsDir, c.ID()) +} + +// create a bundle path and associated files for an exec session +func (c *Container) createExecBundle(sessionID string) (err error) { + bundlePath := c.execBundlePath(sessionID) + if createErr := os.MkdirAll(bundlePath, execDirPermission); createErr != nil { + return createErr + } + defer func() { + if err != nil { + if err2 := os.RemoveAll(bundlePath); err != nil { + logrus.Warnf("error removing exec bundle after creation caused another error: %v", err2) + } + } + }() + if err2 := os.MkdirAll(c.execExitFileDir(sessionID), execDirPermission); err2 != nil { + // The directory is allowed to exist + if !os.IsExist(err2) { + err = errors.Wrapf(err2, "error creating OCI runtime exit file path %s", c.execExitFileDir(sessionID)) + } + } + return +} + +// cleanup an exec session after its done +func (c *Container) cleanupExecBundle(sessionID string) error { + return os.RemoveAll(c.execBundlePath(sessionID)) +} + +// the path to a containers exec session bundle +func (c *Container) execBundlePath(sessionID string) string { + return filepath.Join(c.bundlePath(), sessionID) +} + // Get PID file path for a container's exec session func (c *Container) execPidPath(sessionID string) string { - return filepath.Join(c.state.RunDir, "exec_pid_"+sessionID) + return filepath.Join(c.execBundlePath(sessionID), "exec_pid") } -// exitFilePath gets the path to the container's exit file -func (c *Container) exitFilePath() string { - return filepath.Join(c.ociRuntime.exitsDir, c.ID()) +// the log path for an exec session +func (c *Container) execLogPath(sessionID string) string { + return filepath.Join(c.execBundlePath(sessionID), "exec_log") +} + +// the socket conmon creates for an exec session +func (c *Container) execAttachSocketPath(sessionID string) string { + return filepath.Join(c.ociRuntime.socketsDir, sessionID, "attach") +} + +// execExitFileDir gets the path to the container's exit file +func (c *Container) execExitFileDir(sessionID string) string { + return filepath.Join(c.execBundlePath(sessionID), "exit") +} + +// execOCILog returns the file path for the exec sessions oci log +func (c *Container) execOCILog(sessionID string) string { + if !c.ociRuntime.supportsJSON { + return "" + } + return filepath.Join(c.execBundlePath(sessionID), "oci-log") +} + +// readExecExitCode reads the exit file for an exec session and returns +// the exit code +func (c *Container) readExecExitCode(sessionID string) (int, error) { + exitFile := filepath.Join(c.execExitFileDir(sessionID), c.ID()) + chWait := make(chan error) + defer close(chWait) + + _, err := WaitForFile(exitFile, chWait, time.Second*5) + if err != nil { + return -1, err + } + ec, err := ioutil.ReadFile(exitFile) + if err != nil { + return -1, err + } + ecInt, err := strconv.Atoi(string(ec)) + if err != nil { + return -1, err + } + return ecInt, nil } // Wait for the container's exit file to appear. @@ -849,8 +927,8 @@ func (c *Container) init(ctx context.Context, retainRetries bool) error { return err } - // With the newSpec complete, do an OCI create - if err := c.ociRuntime.createContainer(c, c.config.CgroupParent, nil); err != nil { + // With the spec complete, do an OCI create + if err := c.ociRuntime.createContainer(c, nil); err != nil { return err } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 3dfd4c9e9..6e775cd28 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -834,7 +834,7 @@ func (c *Container) restore(ctx context.Context, options ContainerCheckpointOpti } } - if err := c.ociRuntime.createContainer(c, c.config.CgroupParent, &options); err != nil { + if err := c.ociRuntime.createContainer(c, &options); err != nil { return err } diff --git a/libpod/define/errors.go b/libpod/define/errors.go index 3b8313855..a4368a9aa 100644 --- a/libpod/define/errors.go +++ b/libpod/define/errors.go @@ -97,6 +97,14 @@ var ( // OS. ErrOSNotSupported = errors.New("no support for this OS yet") - // ErrOCIRuntime indicates an error from the OCI runtime + // ErrOCIRuntime indicates a generic error from the OCI runtime ErrOCIRuntime = errors.New("OCI runtime error") + + // ErrOCIRuntimePermissionDenied indicates the OCI runtime attempted to invoke a command that returned + // a permission denied error + ErrOCIRuntimePermissionDenied = errors.New("OCI runtime permission denied error") + + // ErrOCIRuntimeNotFound indicates the OCI runtime attempted to invoke a command + // that was not found + ErrOCIRuntimeNotFound = errors.New("OCI runtime command not found error") ) diff --git a/libpod/healthcheck.go b/libpod/healthcheck.go index 1a19b88bb..0338828e4 100644 --- a/libpod/healthcheck.go +++ b/libpod/healthcheck.go @@ -141,10 +141,18 @@ func (c *Container) runHealthCheck() (HealthCheckStatus, error) { logrus.Debugf("executing health check command %s for %s", strings.Join(newCommand, " "), c.ID()) timeStart := time.Now() hcResult := HealthCheckSuccess - hcErr := c.Exec(false, false, []string{}, newCommand, "", "", streams, 0) + _, hcErr := c.Exec(false, false, []string{}, newCommand, "", "", streams, 0, nil, "") if hcErr != nil { + errCause := errors.Cause(hcErr) hcResult = HealthCheckFailure - returnCode = 1 + if errCause == define.ErrOCIRuntimeNotFound || + errCause == define.ErrOCIRuntimePermissionDenied || + errCause == define.ErrOCIRuntime { + returnCode = 1 + hcErr = nil + } else { + returnCode = 125 + } } timeEnd := time.Now() if c.HealthCheckConfig().StartPeriod > 0 { diff --git a/libpod/image/image.go b/libpod/image/image.go index a057bc720..db50e3dbd 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -38,26 +38,19 @@ import ( "github.com/sirupsen/logrus" ) -// imageConversions is used to cache image "cast" types -type imageConversions struct { - imgRef types.Image - storeRef types.ImageReference -} - // Image is the primary struct for dealing with images // It is still very much a work in progress type Image struct { // Adding these two structs for now but will cull when we near // completion of this library. - imageConversions + imgRef types.Image + storeRef types.ImageReference inspect.ImageData inspect.ImageResult - inspectInfo *types.ImageInspectInfo - InputName string - //runtime *libpod.Runtime + inspectInfo *types.ImageInspectInfo + InputName string image *storage.Image imageruntime *Runtime - repotagsMap map[string][]string } // Runtime contains the store diff --git a/libpod/oci.go b/libpod/oci.go index 3daf9f834..193e66aaf 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -62,12 +62,6 @@ type OCIRuntime struct { supportsJSON bool } -// syncInfo is used to return data from monitor process to daemon -type syncInfo struct { - Pid int `json:"pid"` - Message string `json:"message,omitempty"` -} - // ociError is used to parse the OCI runtime JSON log. It is not part of the // OCI runtime specifications, it follows what runc does type ociError struct { @@ -245,6 +239,7 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container, useRuntime bool) erro cmd := exec.Command(r.path, "state", ctr.ID()) cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) + outPipe, err := cmd.StdoutPipe() if err != nil { return errors.Wrapf(err, "getting stdout pipe") @@ -390,103 +385,6 @@ func (r *OCIRuntime) unpauseContainer(ctr *Container) error { return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "resume", ctr.ID()) } -// execContainer executes a command in a running container -// TODO: Add --detach support -// TODO: Convert to use conmon -// TODO: add --pid-file and use that to generate exec session tracking -func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty bool, cwd, user, sessionID string, streams *AttachStreams, preserveFDs int) (*exec.Cmd, error) { - if len(cmd) == 0 { - return nil, errors.Wrapf(define.ErrInvalidArg, "must provide a command to execute") - } - - if sessionID == "" { - return nil, errors.Wrapf(define.ErrEmptyID, "must provide a session ID for exec") - } - - runtimeDir, err := util.GetRootlessRuntimeDir() - if err != nil { - return nil, err - } - - args := []string{} - - // TODO - should we maintain separate logpaths for exec sessions? - args = append(args, "exec") - - if cwd != "" { - args = append(args, "--cwd", cwd) - } - - args = append(args, "--pid-file", c.execPidPath(sessionID)) - - if tty { - args = append(args, "--tty") - } else { - args = append(args, "--tty=false") - } - - if user != "" { - args = append(args, "--user", user) - } - - if preserveFDs > 0 { - args = append(args, fmt.Sprintf("--preserve-fds=%d", preserveFDs)) - } - if c.config.Spec.Process.NoNewPrivileges { - args = append(args, "--no-new-privs") - } - - for _, capabilityAdd := range capAdd { - args = append(args, "--cap", capabilityAdd) - } - - for _, envVar := range env { - args = append(args, "--env", envVar) - } - - // Append container ID, name and command - args = append(args, c.ID()) - args = append(args, cmd...) - - logrus.Debugf("Starting runtime %s with following arguments: %v", r.path, args) - - execCmd := exec.Command(r.path, args...) - - if streams.AttachOutput { - execCmd.Stdout = streams.OutputStream - } - if streams.AttachInput { - execCmd.Stdin = streams.InputStream - } - if streams.AttachError { - execCmd.Stderr = streams.ErrorStream - } - - execCmd.Env = append(execCmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) - - if preserveFDs > 0 { - for fd := 3; fd < 3+preserveFDs; fd++ { - execCmd.ExtraFiles = append(execCmd.ExtraFiles, os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd))) - } - } - - if err := execCmd.Start(); err != nil { - return nil, errors.Wrapf(err, "cannot start container %s", c.ID()) - } - - if preserveFDs > 0 { - for fd := 3; fd < 3+preserveFDs; fd++ { - // These fds were passed down to the runtime. Close them - // and not interfere - if err := os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd)).Close(); err != nil { - logrus.Debugf("unable to close file fd-%d", fd) - } - } - } - - return execCmd, nil -} - // checkpointContainer checkpoints the given container func (r *OCIRuntime) checkpointContainer(ctr *Container, options ContainerCheckpointOptions) error { if err := label.SetSocketLabel(ctr.ProcessLabel()); err != nil { diff --git a/libpod/oci_attach_linux.go b/libpod/oci_attach_linux.go new file mode 100644 index 000000000..22afa7416 --- /dev/null +++ b/libpod/oci_attach_linux.go @@ -0,0 +1,260 @@ +//+build linux + +package libpod + +import ( + "fmt" + "io" + "net" + "os" + "path/filepath" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/errorhandling" + "github.com/containers/libpod/pkg/kubeutils" + "github.com/containers/libpod/utils" + "github.com/docker/docker/pkg/term" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + "k8s.io/client-go/tools/remotecommand" +) + +/* Sync with stdpipe_t in conmon.c */ +const ( + AttachPipeStdin = 1 + AttachPipeStdout = 2 + AttachPipeStderr = 3 +) + +// Attach to the given container +// Does not check if state is appropriate +// started is only required if startContainer is true +func (c *Container) attach(streams *AttachStreams, keys string, resize <-chan remotecommand.TerminalSize, startContainer bool, started chan bool) error { + if !streams.AttachOutput && !streams.AttachError && !streams.AttachInput { + return errors.Wrapf(define.ErrInvalidArg, "must provide at least one stream to attach to") + } + if startContainer && started == nil { + return errors.Wrapf(define.ErrInternal, "started chan not passed when startContainer set") + } + + detachKeys, err := processDetachKeys(keys) + if err != nil { + return err + } + + logrus.Debugf("Attaching to container %s", c.ID()) + + registerResizeFunc(resize, c.bundlePath()) + + socketPath := buildSocketPath(c.AttachSocketPath()) + + conn, err := net.DialUnix("unixpacket", nil, &net.UnixAddr{Name: socketPath, Net: "unixpacket"}) + if err != nil { + return errors.Wrapf(err, "failed to connect to container's attach socket: %v", socketPath) + } + defer func() { + if err := conn.Close(); err != nil { + logrus.Errorf("unable to close socket: %q", err) + } + }() + + // If starting was requested, start the container and notify when that's + // done. + if startContainer { + if err := c.start(); err != nil { + return err + } + started <- true + } + + receiveStdoutError, stdinDone := setupStdioChannels(streams, conn, detachKeys) + return readStdio(streams, receiveStdoutError, stdinDone) +} + +// Attach to the given container's exec session +// attachFd and startFd must be open file descriptors +// attachFd must be the output side of the fd. attachFd is used for two things: +// conmon will first send a nonse value across the pipe indicating it has set up its side of the console socket +// this ensures attachToExec gets all of the output of the called process +// conmon will then send the exit code of the exec process, or an error in the exec session +// startFd must be the input side of the fd. +// conmon will wait to start the exec session until the parent process has setup the console socket. +// Once attachToExec successfully attaches to the console socket, the child conmon process responsible for calling runtime exec +// will read from the output side of start fd, thus learning to start the child process. +// Thus, the order goes as follow: +// 1. conmon parent process sets up its console socket. sends on attachFd +// 2. attachToExec attaches to the console socket after reading on attachFd +// 3. child waits on startFd for attachToExec to attach to said console socket +// 4. attachToExec sends on startFd, signalling it has attached to the socket and child is ready to go +// 5. child receives on startFd, runs the runtime exec command +// attachToExec is responsible for closing startFd and attachFd +func (c *Container) attachToExec(streams *AttachStreams, keys string, resize <-chan remotecommand.TerminalSize, sessionID string, startFd, attachFd *os.File) error { + if !streams.AttachOutput && !streams.AttachError && !streams.AttachInput { + return errors.Wrapf(define.ErrInvalidArg, "must provide at least one stream to attach to") + } + if startFd == nil || attachFd == nil { + return errors.Wrapf(define.ErrInvalidArg, "start sync pipe and attach sync pipe must be defined for exec attach") + } + + defer errorhandling.CloseQuiet(startFd) + defer errorhandling.CloseQuiet(attachFd) + + detachKeys, err := processDetachKeys(keys) + if err != nil { + return err + } + + logrus.Debugf("Attaching to container %s exec session %s", c.ID(), sessionID) + + registerResizeFunc(resize, c.execBundlePath(sessionID)) + + // set up the socket path, such that it is the correct length and location for exec + socketPath := buildSocketPath(c.execAttachSocketPath(sessionID)) + + // 2: read from attachFd that the parent process has set up the console socket + if _, err := readConmonPipeData(attachFd, ""); err != nil { + return err + } + // 2: then attach + conn, err := net.DialUnix("unixpacket", nil, &net.UnixAddr{Name: socketPath, Net: "unixpacket"}) + if err != nil { + return errors.Wrapf(err, "failed to connect to container's attach socket: %v", socketPath) + } + defer func() { + if err := conn.Close(); err != nil { + logrus.Errorf("unable to close socket: %q", err) + } + }() + + // start listening on stdio of the process + receiveStdoutError, stdinDone := setupStdioChannels(streams, conn, detachKeys) + + // 4: send start message to child + if err := writeConmonPipeData(startFd); err != nil { + return err + } + + return readStdio(streams, receiveStdoutError, stdinDone) +} + +func processDetachKeys(keys string) ([]byte, error) { + // Check the validity of the provided keys first + if len(keys) == 0 { + keys = DefaultDetachKeys + } + detachKeys, err := term.ToBytes(keys) + if err != nil { + return nil, errors.Wrapf(err, "invalid detach keys") + } + return detachKeys, nil +} + +func registerResizeFunc(resize <-chan remotecommand.TerminalSize, bundlePath string) { + kubeutils.HandleResizing(resize, func(size remotecommand.TerminalSize) { + controlPath := filepath.Join(bundlePath, "ctl") + controlFile, err := os.OpenFile(controlPath, unix.O_WRONLY, 0) + if err != nil { + logrus.Debugf("Could not open ctl file: %v", err) + return + } + defer controlFile.Close() + + logrus.Debugf("Received a resize event: %+v", size) + if _, err = fmt.Fprintf(controlFile, "%d %d %d\n", 1, size.Height, size.Width); err != nil { + logrus.Warnf("Failed to write to control file to resize terminal: %v", err) + } + }) +} + +func buildSocketPath(socketPath string) string { + maxUnixLength := unixPathLength() + if maxUnixLength < len(socketPath) { + socketPath = socketPath[0:maxUnixLength] + } + + logrus.Debug("connecting to socket ", socketPath) + return socketPath +} + +func setupStdioChannels(streams *AttachStreams, conn *net.UnixConn, detachKeys []byte) (chan error, chan error) { + receiveStdoutError := make(chan error) + go func() { + receiveStdoutError <- redirectResponseToOutputStreams(streams.OutputStream, streams.ErrorStream, streams.AttachOutput, streams.AttachError, conn) + }() + + stdinDone := make(chan error) + go func() { + var err error + if streams.AttachInput { + _, err = utils.CopyDetachable(conn, streams.InputStream, detachKeys) + if connErr := conn.CloseWrite(); connErr != nil { + logrus.Errorf("unable to close conn: %q", connErr) + } + } + stdinDone <- err + }() + + return receiveStdoutError, stdinDone +} + +func redirectResponseToOutputStreams(outputStream, errorStream io.Writer, writeOutput, writeError bool, conn io.Reader) error { + var err error + buf := make([]byte, 8192+1) /* Sync with conmon STDIO_BUF_SIZE */ + for { + nr, er := conn.Read(buf) + if nr > 0 { + var dst io.Writer + var doWrite bool + switch buf[0] { + case AttachPipeStdout: + dst = outputStream + doWrite = writeOutput + case AttachPipeStderr: + dst = errorStream + doWrite = writeError + default: + logrus.Infof("Received unexpected attach type %+d", buf[0]) + } + if dst == nil { + return errors.New("output destination cannot be nil") + } + + if doWrite { + nw, ew := dst.Write(buf[1:nr]) + if ew != nil { + err = ew + break + } + if nr != nw+1 { + err = io.ErrShortWrite + break + } + } + } + if er == io.EOF { + break + } + if er != nil { + err = er + break + } + } + return err +} + +func readStdio(streams *AttachStreams, receiveStdoutError, stdinDone chan error) error { + var err error + select { + case err = <-receiveStdoutError: + return err + case err = <-stdinDone: + if err == define.ErrDetach { + return err + } + if streams.AttachOutput || streams.AttachError { + return <-receiveStdoutError + } + } + return nil +} diff --git a/libpod/container_attach_linux_cgo.go b/libpod/oci_attach_linux_cgo.go index d81243360..d81243360 100644 --- a/libpod/container_attach_linux_cgo.go +++ b/libpod/oci_attach_linux_cgo.go diff --git a/libpod/container_attach_linux_nocgo.go b/libpod/oci_attach_linux_nocgo.go index a514a555d..a514a555d 100644 --- a/libpod/container_attach_linux_nocgo.go +++ b/libpod/oci_attach_linux_nocgo.go diff --git a/libpod/container_attach_unsupported.go b/libpod/oci_attach_unsupported.go index c27ce0799..987d2c973 100644 --- a/libpod/container_attach_unsupported.go +++ b/libpod/oci_attach_unsupported.go @@ -3,6 +3,8 @@ package libpod import ( + "os" + "github.com/containers/libpod/libpod/define" "k8s.io/client-go/tools/remotecommand" ) @@ -10,3 +12,7 @@ import ( func (c *Container) attach(streams *AttachStreams, keys string, resize <-chan remotecommand.TerminalSize, startContainer bool, started chan bool) error { return define.ErrNotImplemented } + +func (c *Container) attachToExec(streams *AttachStreams, keys string, resize <-chan remotecommand.TerminalSize, sessionID string, startFd *os.File, attachFd *os.File) error { + return define.ErrNotImplemented +} diff --git a/libpod/oci_internal_linux.go b/libpod/oci_internal_linux.go new file mode 100644 index 000000000..0bcd021db --- /dev/null +++ b/libpod/oci_internal_linux.go @@ -0,0 +1,496 @@ +// +build linux + +package libpod + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/errorhandling" + "github.com/containers/libpod/pkg/lookup" + "github.com/containers/libpod/pkg/util" + "github.com/containers/libpod/utils" + "github.com/coreos/go-systemd/activation" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux" + "github.com/opencontainers/selinux/go-selinux/label" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +// createOCIContainer generates this container's main conmon instance and prepares it for starting +func (r *OCIRuntime) createOCIContainer(ctr *Container, restoreOptions *ContainerCheckpointOptions) (err error) { + var stderrBuf bytes.Buffer + + runtimeDir, err := util.GetRootlessRuntimeDir() + if err != nil { + return err + } + + parentSyncPipe, childSyncPipe, err := newPipe() + if err != nil { + return errors.Wrapf(err, "error creating socket pair") + } + defer errorhandling.CloseQuiet(parentSyncPipe) + + childStartPipe, parentStartPipe, err := newPipe() + if err != nil { + return errors.Wrapf(err, "error creating socket pair for start pipe") + } + + defer errorhandling.CloseQuiet(parentStartPipe) + + var ociLog string + if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON { + ociLog = filepath.Join(ctr.state.RunDir, "oci-log") + } + args := r.sharedConmonArgs(ctr, ctr.ID(), ctr.bundlePath(), filepath.Join(ctr.state.RunDir, "pidfile"), ctr.LogPath(), r.exitsDir, ociLog) + + if ctr.config.Spec.Process.Terminal { + args = append(args, "-t") + } else if ctr.config.Stdin { + args = append(args, "-i") + } + + if ctr.config.ConmonPidFile != "" { + args = append(args, "--conmon-pidfile", ctr.config.ConmonPidFile) + } + + if r.noPivot { + args = append(args, "--no-pivot") + } + + if len(ctr.config.ExitCommand) > 0 { + args = append(args, "--exit-command", ctr.config.ExitCommand[0]) + for _, arg := range ctr.config.ExitCommand[1:] { + args = append(args, []string{"--exit-command-arg", arg}...) + } + } + + if restoreOptions != nil { + args = append(args, "--restore", ctr.CheckpointPath()) + if restoreOptions.TCPEstablished { + args = append(args, "--runtime-opt", "--tcp-established") + } + } + + logrus.WithFields(logrus.Fields{ + "args": args, + }).Debugf("running conmon: %s", r.conmonPath) + + cmd := exec.Command(r.conmonPath, args...) + cmd.Dir = ctr.bundlePath() + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + // TODO this is probably a really bad idea for some uses + // Make this configurable + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if ctr.config.Spec.Process.Terminal { + cmd.Stderr = &stderrBuf + } + + // 0, 1 and 2 are stdin, stdout and stderr + conmonEnv, envFiles, err := r.configureConmonEnv(runtimeDir) + if err != nil { + return err + } + + cmd.Env = append(r.conmonEnv, fmt.Sprintf("_OCI_SYNCPIPE=%d", 3), fmt.Sprintf("_OCI_STARTPIPE=%d", 4)) + cmd.Env = append(cmd.Env, conmonEnv...) + cmd.ExtraFiles = append(cmd.ExtraFiles, childSyncPipe, childStartPipe) + cmd.ExtraFiles = append(cmd.ExtraFiles, envFiles...) + + if r.reservePorts && !ctr.config.NetMode.IsSlirp4netns() { + ports, err := bindPorts(ctr.config.PortMappings) + if err != nil { + return err + } + + // Leak the port we bound in the conmon process. These fd's won't be used + // by the container and conmon will keep the ports busy so that another + // process cannot use them. + cmd.ExtraFiles = append(cmd.ExtraFiles, ports...) + } + + if ctr.config.NetMode.IsSlirp4netns() { + ctr.rootlessSlirpSyncR, ctr.rootlessSlirpSyncW, err = os.Pipe() + if err != nil { + return errors.Wrapf(err, "failed to create rootless network sync pipe") + } + // Leak one end in conmon, the other one will be leaked into slirp4netns + cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessSlirpSyncW) + } + + err = startCommandGivenSelinux(cmd) + // regardless of whether we errored or not, we no longer need the children pipes + childSyncPipe.Close() + childStartPipe.Close() + if err != nil { + return err + } + if err := r.moveConmonToCgroupAndSignal(ctr, cmd, parentStartPipe, ctr.ID()); err != nil { + return err + } + /* Wait for initial setup and fork, and reap child */ + err = cmd.Wait() + if err != nil { + return err + } + + pid, err := readConmonPipeData(parentSyncPipe, ociLog) + if err != nil { + if err2 := r.deleteContainer(ctr); err2 != nil { + logrus.Errorf("Error removing container %s from runtime after creation failed", ctr.ID()) + } + return err + } + ctr.state.PID = pid + + conmonPID, err := readConmonPidFile(ctr.config.ConmonPidFile) + if err != nil { + logrus.Warnf("error reading conmon pid file for container %s: %s", ctr.ID(), err.Error()) + } 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 + } + + return nil +} + +// prepareProcessExec returns the path of the process.json used in runc exec -p +// caller is responsible to close the returned *os.File if needed. +func prepareProcessExec(c *Container, cmd, env []string, tty bool, cwd, user, sessionID string) (*os.File, error) { + f, err := ioutil.TempFile(c.execBundlePath(sessionID), "exec-process-") + if err != nil { + return nil, err + } + + pspec := c.config.Spec.Process + pspec.Args = cmd + // We need to default this to false else it will inherit terminal as true + // from the container. + pspec.Terminal = false + if tty { + pspec.Terminal = true + } + if len(env) > 0 { + pspec.Env = append(pspec.Env, env...) + } + + if cwd != "" { + pspec.Cwd = cwd + + } + // If user was set, look it up in the container to get a UID to use on + // the host + if user != "" { + execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, user, nil) + if err != nil { + return nil, err + } + sgids := make([]uint32, 0, len(execUser.Sgids)) + for _, sgid := range execUser.Sgids { + sgids = append(sgids, uint32(sgid)) + } + processUser := spec.User{ + UID: uint32(execUser.Uid), + GID: uint32(execUser.Gid), + AdditionalGids: sgids, + } + + pspec.User = processUser + } + + processJSON, err := json.Marshal(pspec) + if err != nil { + return nil, err + } + + if err := ioutil.WriteFile(f.Name(), processJSON, 0644); err != nil { + return nil, err + } + return f, nil +} + +// configureConmonEnv gets the environment values to add to conmon's exec struct +// TODO this may want to be less hardcoded/more configurable in the future +func (r *OCIRuntime) configureConmonEnv(runtimeDir string) ([]string, []*os.File, error) { + env := make([]string, 0, 6) + env = append(env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) + env = append(env, fmt.Sprintf("_CONTAINERS_USERNS_CONFIGURED=%s", os.Getenv("_CONTAINERS_USERNS_CONFIGURED"))) + env = append(env, fmt.Sprintf("_CONTAINERS_ROOTLESS_UID=%s", os.Getenv("_CONTAINERS_ROOTLESS_UID"))) + home, err := homeDir() + if err != nil { + return nil, nil, err + } + env = append(env, fmt.Sprintf("HOME=%s", home)) + + extraFiles := make([]*os.File, 0) + if notify, ok := os.LookupEnv("NOTIFY_SOCKET"); ok { + env = append(env, fmt.Sprintf("NOTIFY_SOCKET=%s", notify)) + } + if listenfds, ok := os.LookupEnv("LISTEN_FDS"); ok { + env = append(env, fmt.Sprintf("LISTEN_FDS=%s", listenfds), "LISTEN_PID=1") + fds := activation.Files(false) + extraFiles = append(extraFiles, fds...) + } + return env, extraFiles, nil +} + +// sharedConmonArgs takes common arguments for exec and create/restore and formats them for the conmon CLI +func (r *OCIRuntime) sharedConmonArgs(ctr *Container, cuuid, bundlePath, pidPath, logPath, exitDir, ociLogPath string) []string { + // set the conmon API version to be able to use the correct sync struct keys + args := []string{"--api-version", "1"} + if r.cgroupManager == SystemdCgroupsManager { + args = append(args, "-s") + } + args = append(args, "-c", ctr.ID()) + args = append(args, "-u", cuuid) + args = append(args, "-r", r.path) + args = append(args, "-b", bundlePath) + args = append(args, "-p", pidPath) + + var logDriver string + switch ctr.LogDriver() { + case JournaldLogging: + logDriver = JournaldLogging + case JSONLogging: + fallthrough + default: //nolint-stylecheck + // No case here should happen except JSONLogging, but keep this here in case the options are extended + logrus.Errorf("%s logging specified but not supported. Choosing k8s-file logging instead", ctr.LogDriver()) + fallthrough + case KubernetesLogging: + logDriver = fmt.Sprintf("%s:%s", KubernetesLogging, logPath) + } + + args = append(args, "-l", logDriver) + args = append(args, "--exit-dir", exitDir) + args = append(args, "--socket-dir-path", r.socketsDir) + if r.logSizeMax >= 0 { + args = append(args, "--log-size-max", fmt.Sprintf("%v", r.logSizeMax)) + } + + logLevel := logrus.GetLevel() + args = append(args, "--log-level", logLevel.String()) + + if logLevel == logrus.DebugLevel { + logrus.Debugf("%s messages will be logged to syslog", r.conmonPath) + args = append(args, "--syslog") + } + if ociLogPath != "" { + args = append(args, "--runtime-arg", "--log-format=json", "--runtime-arg", "--log", fmt.Sprintf("--runtime-arg=%s", ociLogPath)) + } + return args +} + +// startCommandGivenSelinux starts a container ensuring to set the labels of +// the process to make sure SELinux doesn't block conmon communication, if SELinux is enabled +func startCommandGivenSelinux(cmd *exec.Cmd) error { + if !selinux.GetEnabled() { + return cmd.Start() + } + // Set the label of the conmon process to be level :s0 + // This will allow the container processes to talk to fifo-files + // passed into the container by conmon + var ( + plabel string + con selinux.Context + err error + ) + plabel, err = selinux.CurrentLabel() + if err != nil { + return errors.Wrapf(err, "Failed to get current SELinux label") + } + + con, err = selinux.NewContext(plabel) + if err != nil { + return errors.Wrapf(err, "Failed to get new context from SELinux label") + } + + runtime.LockOSThread() + if con["level"] != "s0" && con["level"] != "" { + con["level"] = "s0" + if err = label.SetProcessLabel(con.Get()); err != nil { + runtime.UnlockOSThread() + return err + } + } + err = cmd.Start() + // Ignore error returned from SetProcessLabel("") call, + // can't recover. + if labelErr := label.SetProcessLabel(""); labelErr != nil { + logrus.Errorf("unable to set process label: %q", err) + } + runtime.UnlockOSThread() + return err +} + +// moveConmonToCgroupAndSignal gets a container's cgroupParent and moves the conmon process to that cgroup +// it then signals for conmon to start by sending nonse data down the start fd +func (r *OCIRuntime) moveConmonToCgroupAndSignal(ctr *Container, cmd *exec.Cmd, startFd *os.File, uuid string) error { + cgroupParent := ctr.CgroupParent() + if os.Geteuid() == 0 { + if r.cgroupManager == SystemdCgroupsManager { + unitName := createUnitName("libpod-conmon", ctr.ID()) + + realCgroupParent := cgroupParent + splitParent := strings.Split(cgroupParent, "/") + if strings.HasSuffix(cgroupParent, ".slice") && len(splitParent) > 1 { + realCgroupParent = splitParent[len(splitParent)-1] + } + + logrus.Infof("Running conmon under slice %s and unitName %s", realCgroupParent, unitName) + if err := utils.RunUnderSystemdScope(cmd.Process.Pid, realCgroupParent, unitName); err != nil { + logrus.Warnf("Failed to add conmon to systemd sandbox cgroup: %v", err) + } + } else { + cgroupPath := filepath.Join(ctr.config.CgroupParent, "conmon") + control, err := cgroups.New(cgroupPath, &spec.LinuxResources{}) + if err != nil { + logrus.Warnf("Failed to add conmon to cgroupfs sandbox cgroup: %v", err) + } else { + // we need to remove this defer and delete the cgroup once conmon exits + // maybe need a conmon monitor? + if err := control.AddPid(cmd.Process.Pid); err != nil { + logrus.Warnf("Failed to add conmon to cgroupfs sandbox cgroup: %v", err) + } + } + } + } + + /* We set the cgroup, now the child can start creating children */ + if err := writeConmonPipeData(startFd); err != nil { + return err + } + return nil +} + +// newPipe creates a unix socket pair for communication +func newPipe() (parent *os.File, child *os.File, err error) { + fds, err := unix.Socketpair(unix.AF_LOCAL, unix.SOCK_SEQPACKET|unix.SOCK_CLOEXEC, 0) + if err != nil { + return nil, nil, err + } + return os.NewFile(uintptr(fds[1]), "parent"), os.NewFile(uintptr(fds[0]), "child"), nil +} + +// readConmonPidFile attempts to read conmon's pid from its pid file +func readConmonPidFile(pidFile string) (int, error) { + // Let's try reading the Conmon pid at the same time. + if pidFile != "" { + contents, err := ioutil.ReadFile(pidFile) + if err != nil { + return -1, err + } + // Convert it to an int + conmonPID, err := strconv.Atoi(string(contents)) + if err != nil { + return -1, err + } + return conmonPID, nil + } + return 0, nil +} + +// readConmonPipeData attempts to read a syncInfo struct from the pipe +func readConmonPipeData(pipe *os.File, ociLog string) (int, error) { + // syncInfo is used to return data from monitor process to daemon + type syncInfo struct { + Data int `json:"data"` + Message string `json:"message,omitempty"` + } + + // Wait to get container pid from conmon + type syncStruct struct { + si *syncInfo + err error + } + ch := make(chan syncStruct) + go func() { + var si *syncInfo + rdr := bufio.NewReader(pipe) + b, err := rdr.ReadBytes('\n') + if err != nil { + ch <- syncStruct{err: err} + } + if err := json.Unmarshal(b, &si); err != nil { + ch <- syncStruct{err: err} + return + } + ch <- syncStruct{si: si} + }() + + data := -1 + select { + case ss := <-ch: + if ss.err != nil { + return -1, errors.Wrapf(ss.err, "error reading container (probably exited) json message") + } + logrus.Debugf("Received: %d", ss.si.Data) + if ss.si.Data < 0 { + if ociLog != "" { + ociLogData, err := ioutil.ReadFile(ociLog) + if err == nil { + var ociErr ociError + if err := json.Unmarshal(ociLogData, &ociErr); err == nil { + return ss.si.Data, getOCIRuntimeError(ociErr.Msg) + } + } + } + // If we failed to parse the JSON errors, then print the output as it is + if ss.si.Message != "" { + return ss.si.Data, getOCIRuntimeError(ss.si.Message) + } + return ss.si.Data, errors.Wrapf(define.ErrInternal, "container create failed") + } + data = ss.si.Data + case <-time.After(ContainerCreateTimeout): + return -1, errors.Wrapf(define.ErrInternal, "container creation timeout") + } + return data, nil +} + +func getOCIRuntimeError(runtimeMsg string) error { + if match, _ := regexp.MatchString(".*permission denied.*", runtimeMsg); match { + return errors.Wrapf(define.ErrOCIRuntimePermissionDenied, "%s", strings.Trim(runtimeMsg, "\n")) + } + if match, _ := regexp.MatchString(".*executable file not found in.*", runtimeMsg); match { + return errors.Wrapf(define.ErrOCIRuntimeNotFound, "%s", strings.Trim(runtimeMsg, "\n")) + } + return errors.Wrapf(define.ErrOCIRuntime, "%s", strings.Trim(runtimeMsg, "\n")) +} + +// writeConmonPipeData writes nonse data to a pipe +func writeConmonPipeData(pipe *os.File) error { + someData := []byte{0} + _, err := pipe.Write(someData) + return err +} + +// formatRuntimeOpts prepends opts passed to it with --runtime-opt for passing to conmon +func formatRuntimeOpts(opts ...string) []string { + args := make([]string, 0, len(opts)*2) + for _, o := range opts { + args = append(args, "--runtime-opt", o) + } + return args +} diff --git a/libpod/oci_linux.go b/libpod/oci_linux.go index 9ce836cb5..45365203e 100644 --- a/libpod/oci_linux.go +++ b/libpod/oci_linux.go @@ -3,78 +3,29 @@ package libpod import ( - "bufio" - "bytes" "fmt" - "io/ioutil" "os" "os/exec" "path/filepath" "runtime" - "strconv" "strings" "syscall" "time" "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/errorhandling" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/util" "github.com/containers/libpod/utils" pmount "github.com/containers/storage/pkg/mount" - "github.com/coreos/go-systemd/activation" - spec "github.com/opencontainers/runtime-spec/specs-go" - "github.com/opencontainers/selinux/go-selinux" - "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" + "k8s.io/client-go/tools/remotecommand" ) const unknownPackage = "Unknown" -func (r *OCIRuntime) moveConmonToCgroup(ctr *Container, cgroupParent string, cmd *exec.Cmd) error { - if os.Geteuid() == 0 { - if r.cgroupManager == SystemdCgroupsManager { - unitName := createUnitName("libpod-conmon", ctr.ID()) - - realCgroupParent := cgroupParent - splitParent := strings.Split(cgroupParent, "/") - if strings.HasSuffix(cgroupParent, ".slice") && len(splitParent) > 1 { - realCgroupParent = splitParent[len(splitParent)-1] - } - - logrus.Infof("Running conmon under slice %s and unitName %s", realCgroupParent, unitName) - if err := utils.RunUnderSystemdScope(cmd.Process.Pid, realCgroupParent, unitName); err != nil { - logrus.Warnf("Failed to add conmon to systemd sandbox cgroup: %v", err) - } - } else { - cgroupPath := filepath.Join(ctr.config.CgroupParent, "conmon") - control, err := cgroups.New(cgroupPath, &spec.LinuxResources{}) - if err != nil { - logrus.Warnf("Failed to add conmon to cgroupfs sandbox cgroup: %v", err) - } else { - // we need to remove this defer and delete the cgroup once conmon exits - // maybe need a conmon monitor? - if err := control.AddPid(cmd.Process.Pid); err != nil { - logrus.Warnf("Failed to add conmon to cgroupfs sandbox cgroup: %v", err) - } - } - } - } - return nil -} - -// newPipe creates a unix socket pair for communication -func newPipe() (parent *os.File, child *os.File, err error) { - fds, err := unix.Socketpair(unix.AF_LOCAL, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) - if err != nil { - return nil, nil, err - } - return os.NewFile(uintptr(fds[1]), "parent"), os.NewFile(uintptr(fds[0]), "child"), nil -} - // makeAccessible changes the path permission and each parent directory to have --x--x--x func makeAccessible(path string, uid, gid int) error { for ; path != "/"; path = filepath.Dir(path) { @@ -100,7 +51,7 @@ func makeAccessible(path string, uid, gid int) error { // CreateContainer creates a container in the OCI runtime // TODO terminal support for container // Presently just ignoring conmon opts related to it -func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restoreOptions *ContainerCheckpointOptions) (err error) { +func (r *OCIRuntime) createContainer(ctr *Container, restoreOptions *ContainerCheckpointOptions) (err error) { if len(ctr.config.IDMappings.UIDMap) != 0 || len(ctr.config.IDMappings.GIDMap) != 0 { for _, i := range []string{ctr.state.RunDir, ctr.runtime.config.TmpDir, ctr.config.StaticDir, ctr.state.Mountpoint, ctr.runtime.config.VolumePath} { if err := makeAccessible(i, ctr.RootUID(), ctr.RootGID()); err != nil { @@ -152,7 +103,7 @@ func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restor return errors.Wrapf(err, "cannot unmount %s", m.Mountpoint) } } - return r.createOCIContainer(ctr, cgroupParent, restoreOptions) + return r.createOCIContainer(ctr, restoreOptions) }() ch <- err }() @@ -160,7 +111,7 @@ func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restor return err } } - return r.createOCIContainer(ctr, cgroupParent, restoreOptions) + return r.createOCIContainer(ctr, restoreOptions) } func rpmVersion(path string) string { @@ -195,293 +146,178 @@ func (r *OCIRuntime) conmonPackage() string { return dpkgVersion(r.conmonPath) } -func (r *OCIRuntime) createOCIContainer(ctr *Container, cgroupParent string, restoreOptions *ContainerCheckpointOptions) (err error) { - var stderrBuf bytes.Buffer +// execContainer executes a command in a running container +// TODO: Add --detach support +// TODO: Convert to use conmon +// TODO: add --pid-file and use that to generate exec session tracking +func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty bool, cwd, user, sessionID string, streams *AttachStreams, preserveFDs int, resize chan remotecommand.TerminalSize, detachKeys string) (int, chan error, error) { + if len(cmd) == 0 { + return -1, nil, errors.Wrapf(define.ErrInvalidArg, "must provide a command to execute") + } - runtimeDir, err := util.GetRootlessRuntimeDir() - if err != nil { - return err + if sessionID == "" { + return -1, nil, errors.Wrapf(define.ErrEmptyID, "must provide a session ID for exec") } - parentPipe, childPipe, err := newPipe() + // create sync pipe to receive the pid + parentSyncPipe, childSyncPipe, err := newPipe() if err != nil { - return errors.Wrapf(err, "error creating socket pair") + return -1, nil, errors.Wrapf(err, "error creating socket pair") } + defer errorhandling.CloseQuiet(parentSyncPipe) + + // create start pipe to set the cgroup before running + // attachToExec is responsible for closing parentStartPipe childStartPipe, parentStartPipe, err := newPipe() if err != nil { - return errors.Wrapf(err, "error creating socket pair for start pipe") + return -1, nil, errors.Wrapf(err, "error creating socket pair") } - defer errorhandling.CloseQuiet(parentPipe) - defer errorhandling.CloseQuiet(parentStartPipe) + // We want to make sure we close the parent{Start,Attach}Pipes if we fail + // but also don't want to close them after attach to exec is called + attachToExecCalled := false - ociLog := filepath.Join(ctr.state.RunDir, "oci-log") - logLevel := logrus.GetLevel() + defer func() { + if !attachToExecCalled { + errorhandling.CloseQuiet(parentStartPipe) + } + }() - args := []string{} - if r.cgroupManager == SystemdCgroupsManager { - args = append(args, "-s") - } - args = append(args, "-c", ctr.ID()) - args = append(args, "-u", ctr.ID()) - args = append(args, "-n", ctr.Name()) - args = append(args, "-r", r.path) - args = append(args, "-b", ctr.bundlePath()) - args = append(args, "-p", filepath.Join(ctr.state.RunDir, "pidfile")) - args = append(args, "--exit-dir", r.exitsDir) - if logLevel != logrus.DebugLevel && r.supportsJSON { - args = append(args, "--runtime-arg", "--log-format=json", "--runtime-arg", "--log", fmt.Sprintf("--runtime-arg=%s", ociLog)) - } - if ctr.config.ConmonPidFile != "" { - args = append(args, "--conmon-pidfile", ctr.config.ConmonPidFile) + // create the attach pipe to allow attach socket to be created before + // $RUNTIME exec starts running. This is to make sure we can capture all output + // from the process through that socket, rather than half reading the log, half attaching to the socket + // attachToExec is responsible for closing parentAttachPipe + parentAttachPipe, childAttachPipe, err := newPipe() + if err != nil { + return -1, nil, errors.Wrapf(err, "error creating socket pair") } - if len(ctr.config.ExitCommand) > 0 { - args = append(args, "--exit-command", ctr.config.ExitCommand[0]) - for _, arg := range ctr.config.ExitCommand[1:] { - args = append(args, []string{"--exit-command-arg", arg}...) + + defer func() { + if !attachToExecCalled { + errorhandling.CloseQuiet(parentAttachPipe) } - } - args = append(args, "--socket-dir-path", r.socketsDir) - if ctr.config.Spec.Process.Terminal { - args = append(args, "-t") - } else if ctr.config.Stdin { - args = append(args, "-i") - } - if r.logSizeMax >= 0 { - args = append(args, "--log-size-max", fmt.Sprintf("%v", r.logSizeMax)) + }() + + childrenClosed := false + defer func() { + if !childrenClosed { + errorhandling.CloseQuiet(childSyncPipe) + errorhandling.CloseQuiet(childAttachPipe) + errorhandling.CloseQuiet(childStartPipe) + } + }() + + runtimeDir, err := util.GetRootlessRuntimeDir() + if err != nil { + return -1, nil, err } - logDriver := KubernetesLogging - if ctr.LogDriver() == JSONLogging { - logrus.Errorf("json-file logging specified but not supported. Choosing k8s-file logging instead") - } else if ctr.LogDriver() != "" { - logDriver = ctr.LogDriver() + processFile, err := prepareProcessExec(c, cmd, env, tty, cwd, user, sessionID) + if err != nil { + return -1, nil, err } - args = append(args, "-l", fmt.Sprintf("%s:%s", logDriver, ctr.LogPath())) - if r.noPivot { - args = append(args, "--no-pivot") + var ociLog string + if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON { + ociLog = c.execOCILog(sessionID) } + args := r.sharedConmonArgs(c, sessionID, c.execBundlePath(sessionID), c.execPidPath(sessionID), c.execLogPath(sessionID), c.execExitFileDir(sessionID), ociLog) - args = append(args, "--log-level", logLevel.String()) + if preserveFDs > 0 { + args = append(args, formatRuntimeOpts("--preserve-fds", string(preserveFDs))...) + } - if logLevel == logrus.DebugLevel { - logrus.Debugf("%s messages will be logged to syslog", r.conmonPath) - args = append(args, "--syslog") + for _, capability := range capAdd { + args = append(args, formatRuntimeOpts("--cap", capability)...) } - if restoreOptions != nil { - args = append(args, "--restore", ctr.CheckpointPath()) - if restoreOptions.TCPEstablished { - args = append(args, "--restore-arg", "--tcp-established") - } + if tty { + args = append(args, "-t") } + // Append container ID and command + args = append(args, "-e") + // TODO make this optional when we can detach + args = append(args, "--exec-attach") + args = append(args, "--exec-process-spec", processFile.Name()) + logrus.WithFields(logrus.Fields{ "args": args, }).Debugf("running conmon: %s", r.conmonPath) + execCmd := exec.Command(r.conmonPath, args...) - cmd := exec.Command(r.conmonPath, args...) - cmd.Dir = ctr.bundlePath() - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, + if streams.AttachInput { + execCmd.Stdin = streams.InputStream } - // TODO this is probably a really bad idea for some uses - // Make this configurable - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if ctr.config.Spec.Process.Terminal { - cmd.Stderr = &stderrBuf - } - - cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe, childStartPipe) - // 0, 1 and 2 are stdin, stdout and stderr - cmd.Env = append(r.conmonEnv, fmt.Sprintf("_OCI_SYNCPIPE=%d", 3)) - cmd.Env = append(cmd.Env, fmt.Sprintf("_OCI_STARTPIPE=%d", 4)) - cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir)) - cmd.Env = append(cmd.Env, fmt.Sprintf("_CONTAINERS_USERNS_CONFIGURED=%s", os.Getenv("_CONTAINERS_USERNS_CONFIGURED"))) - cmd.Env = append(cmd.Env, fmt.Sprintf("_CONTAINERS_ROOTLESS_UID=%s", os.Getenv("_CONTAINERS_ROOTLESS_UID"))) - home, err := homeDir() - if err != nil { - return err + if streams.AttachOutput { + execCmd.Stdout = streams.OutputStream + } + if streams.AttachError { + execCmd.Stderr = streams.ErrorStream } - cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", home)) - - if r.reservePorts && !ctr.config.NetMode.IsSlirp4netns() { - ports, err := bindPorts(ctr.config.PortMappings) - if err != nil { - return err - } - // Leak the port we bound in the conmon process. These fd's won't be used - // by the container and conmon will keep the ports busy so that another - // process cannot use them. - cmd.ExtraFiles = append(cmd.ExtraFiles, ports...) + conmonEnv, extraFiles, err := r.configureConmonEnv(runtimeDir) + if err != nil { + return -1, nil, err } - if ctr.config.NetMode.IsSlirp4netns() { - ctr.rootlessSlirpSyncR, ctr.rootlessSlirpSyncW, err = os.Pipe() - if err != nil { - return errors.Wrapf(err, "failed to create rootless network sync pipe") - } - // Leak one end in conmon, the other one will be leaked into slirp4netns - cmd.ExtraFiles = append(cmd.ExtraFiles, ctr.rootlessSlirpSyncW) - } - - if notify, ok := os.LookupEnv("NOTIFY_SOCKET"); ok { - cmd.Env = append(cmd.Env, fmt.Sprintf("NOTIFY_SOCKET=%s", notify)) - } - if listenfds, ok := os.LookupEnv("LISTEN_FDS"); ok { - cmd.Env = append(cmd.Env, fmt.Sprintf("LISTEN_FDS=%s", listenfds), "LISTEN_PID=1") - fds := activation.Files(false) - cmd.ExtraFiles = append(cmd.ExtraFiles, fds...) - } - if selinux.GetEnabled() { - // Set the label of the conmon process to be level :s0 - // This will allow the container processes to talk to fifo-files - // passed into the container by conmon - var ( - plabel string - con selinux.Context - ) - plabel, err = selinux.CurrentLabel() - if err != nil { - if err := childPipe.Close(); err != nil { - logrus.Errorf("failed to close child pipe: %q", err) - } - return errors.Wrapf(err, "Failed to get current SELinux label") - } + // we don't want to step on users fds they asked to preserve + // Since 0-2 are used for stdio, start the fds we pass in at preserveFDs+3 + execCmd.Env = append(r.conmonEnv, fmt.Sprintf("_OCI_SYNCPIPE=%d", preserveFDs+3), fmt.Sprintf("_OCI_STARTPIPE=%d", preserveFDs+4), fmt.Sprintf("_OCI_ATTACHPIPE=%d", preserveFDs+5)) + execCmd.Env = append(execCmd.Env, conmonEnv...) - con, err = selinux.NewContext(plabel) - if err != nil { - return errors.Wrapf(err, "Failed to get new context from SELinux label") - } + execCmd.ExtraFiles = append(execCmd.ExtraFiles, childSyncPipe, childStartPipe, childAttachPipe) + execCmd.ExtraFiles = append(execCmd.ExtraFiles, extraFiles...) + execCmd.Dir = c.execBundlePath(sessionID) + execCmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } - runtime.LockOSThread() - if con["level"] != "s0" && con["level"] != "" { - con["level"] = "s0" - if err = label.SetProcessLabel(con.Get()); err != nil { - runtime.UnlockOSThread() - return err - } + if preserveFDs > 0 { + for fd := 3; fd < 3+preserveFDs; fd++ { + execCmd.ExtraFiles = append(execCmd.ExtraFiles, os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd))) } - err = cmd.Start() - // Ignore error returned from SetProcessLabel("") call, - // can't recover. - if err := label.SetProcessLabel(""); err != nil { - _ = err - } - runtime.UnlockOSThread() - } else { - err = cmd.Start() } - if err != nil { - errorhandling.CloseQuiet(childPipe) - return err - } - defer func() { - _ = cmd.Wait() - }() - // We don't need childPipe on the parent side - if err := childPipe.Close(); err != nil { - return err - } - if err := childStartPipe.Close(); err != nil { - return err - } + err = startCommandGivenSelinux(execCmd) - // Move conmon to specified cgroup - if err := r.moveConmonToCgroup(ctr, cgroupParent, cmd); err != nil { - return err - } + // We don't need children pipes on the parent side + errorhandling.CloseQuiet(childSyncPipe) + errorhandling.CloseQuiet(childAttachPipe) + errorhandling.CloseQuiet(childStartPipe) + childrenClosed = true - /* We set the cgroup, now the child can start creating children */ - someData := []byte{0} - _, err = parentStartPipe.Write(someData) if err != nil { - return err + return -1, nil, errors.Wrapf(err, "cannot start container %s", c.ID()) } - - /* Wait for initial setup and fork, and reap child */ - err = cmd.Wait() - if err != nil { - return err + if err := r.moveConmonToCgroupAndSignal(c, execCmd, parentStartPipe, sessionID); err != nil { + return -1, nil, err } - defer func() { - if err != nil { - if err2 := r.deleteContainer(ctr); err2 != nil { - logrus.Errorf("Error removing container %s from runtime after creation failed", ctr.ID()) + if preserveFDs > 0 { + for fd := 3; fd < 3+preserveFDs; fd++ { + // These fds were passed down to the runtime. Close them + // and not interfere + if err := os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd)).Close(); err != nil { + logrus.Debugf("unable to close file fd-%d", fd) } } - }() - - // Wait to get container pid from conmon - type syncStruct struct { - si *syncInfo - err error } - ch := make(chan syncStruct) + + // TODO Only create if !detach + // Attach to the container before starting it + attachChan := make(chan error) go func() { - var si *syncInfo - rdr := bufio.NewReader(parentPipe) - b, err := rdr.ReadBytes('\n') - if err != nil { - ch <- syncStruct{err: err} - } - if err := json.Unmarshal(b, &si); err != nil { - ch <- syncStruct{err: err} - return - } - ch <- syncStruct{si: si} + // attachToExec is responsible for closing pipes + attachChan <- c.attachToExec(streams, detachKeys, resize, sessionID, parentStartPipe, parentAttachPipe) + close(attachChan) }() + attachToExecCalled = true - select { - case ss := <-ch: - if ss.err != nil { - return errors.Wrapf(ss.err, "error reading container (probably exited) json message") - } - logrus.Debugf("Received container pid: %d", ss.si.Pid) - if ss.si.Pid == -1 { - if r.supportsJSON { - data, err := ioutil.ReadFile(ociLog) - if err == nil { - var ociErr ociError - if err := json.Unmarshal(data, &ociErr); err == nil { - return errors.Wrapf(define.ErrOCIRuntime, "%s", strings.Trim(ociErr.Msg, "\n")) - } - } - } - // If we failed to parse the JSON errors, then print the output as it is - if ss.si.Message != "" { - return errors.Wrapf(define.ErrOCIRuntime, "%s", ss.si.Message) - } - return errors.Wrapf(define.ErrInternal, "container create failed") - } - ctr.state.PID = ss.si.Pid - // Let's try reading the Conmon pid at the same time. - if ctr.config.ConmonPidFile != "" { - contents, err := ioutil.ReadFile(ctr.config.ConmonPidFile) - if err != nil { - logrus.Warnf("Error reading Conmon pidfile for container %s: %v", ctr.ID(), err) - } else { - // Convert it to an int - conmonPID, err := strconv.Atoi(string(contents)) - if err != nil { - logrus.Warnf("Error decoding Conmon PID %q for container %s: %v", string(contents), ctr.ID(), err) - } else { - ctr.state.ConmonPID = conmonPID - logrus.Infof("Got Conmon PID as %d", conmonPID) - } - } - } - case <-time.After(ContainerCreateTimeout): - return errors.Wrapf(define.ErrInternal, "container creation timeout") - } - return nil + pid, err := readConmonPipeData(parentSyncPipe, ociLog) + + return pid, attachChan, err } // Wait for a container which has been sent a signal to stop diff --git a/libpod/oci_unsupported.go b/libpod/oci_unsupported.go index cfdf70bc6..4a65d4d1d 100644 --- a/libpod/oci_unsupported.go +++ b/libpod/oci_unsupported.go @@ -7,6 +7,7 @@ import ( "os/exec" "github.com/containers/libpod/libpod/define" + "k8s.io/client-go/tools/remotecommand" ) func (r *OCIRuntime) moveConmonToCgroup(ctr *Container, cgroupParent string, cmd *exec.Cmd) error { @@ -17,7 +18,7 @@ func newPipe() (parent *os.File, child *os.File, err error) { return nil, nil, define.ErrNotImplemented } -func (r *OCIRuntime) createContainer(ctr *Container, cgroupParent string, restoreOptions *ContainerCheckpointOptions) (err error) { +func (r *OCIRuntime) createContainer(ctr *Container, restoreOptions *ContainerCheckpointOptions) (err error) { return define.ErrNotImplemented } @@ -40,3 +41,7 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { func (r *OCIRuntime) stopContainer(ctr *Container, timeout uint) error { return define.ErrOSNotSupported } + +func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty bool, cwd, user, sessionID string, streams *AttachStreams, preserveFDs int, resize chan remotecommand.TerminalSize, detachKeys string) (int, chan error, error) { + return -1, nil, define.ErrOSNotSupported +} diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 9726b237f..47f1b091e 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -924,13 +924,85 @@ func (r *LocalRuntime) execPS(c *libpod.Container, args []string) ([]string, err }() cmd := append([]string{"ps"}, args...) - if err := c.Exec(false, false, []string{}, cmd, "", "", streams, 0); err != nil { + ec, err := c.Exec(false, false, []string{}, cmd, "", "", streams, 0, nil, "") + if err != nil { return nil, err + } else if ec != 0 { + return nil, errors.Errorf("Runtime failed with exit status: %d and output: %s", ec, strings.Join(psOutput, " ")) } return psOutput, nil } +// ExecContainer executes a command in the container +func (r *LocalRuntime) ExecContainer(ctx context.Context, cli *cliconfig.ExecValues) (int, error) { + var ( + ctr *Container + err error + cmd []string + ) + // default invalid command exit code + ec := 125 + + if cli.Latest { + if ctr, err = r.GetLatestContainer(); err != nil { + return ec, err + } + cmd = cli.InputArgs[0:] + } else { + if ctr, err = r.LookupContainer(cli.InputArgs[0]); err != nil { + return ec, err + } + cmd = cli.InputArgs[1:] + } + + if cli.PreserveFDs > 0 { + entries, err := ioutil.ReadDir("/proc/self/fd") + if err != nil { + return ec, errors.Wrapf(err, "unable to read /proc/self/fd") + } + + m := make(map[int]bool) + for _, e := range entries { + i, err := strconv.Atoi(e.Name()) + if err != nil { + return ec, errors.Wrapf(err, "cannot parse %s in /proc/self/fd", e.Name()) + } + m[i] = true + } + + for i := 3; i < 3+cli.PreserveFDs; i++ { + if _, found := m[i]; !found { + return ec, errors.New("invalid --preserve-fds=N specified. Not enough FDs available") + } + } + } + + // Validate given environment variables + env := map[string]string{} + if err := parse.ReadKVStrings(env, []string{}, cli.Env); err != nil { + return ec, errors.Wrapf(err, "unable to process environment variables") + } + + // Build env slice of key=value strings for Exec + envs := []string{} + for k, v := range env { + envs = append(envs, fmt.Sprintf("%s=%s", k, v)) + } + + streams := new(libpod.AttachStreams) + streams.OutputStream = os.Stdout + streams.ErrorStream = os.Stderr + if cli.Interactive { + streams.InputStream = os.Stdin + streams.AttachInput = true + } + streams.AttachOutput = true + streams.AttachError = true + + return ExecAttachCtr(ctx, ctr.Container, cli.Tty, cli.Privileged, envs, cmd, cli.User, cli.Workdir, streams, cli.PreserveFDs, cli.DetachKeys) +} + // Prune removes stopped containers func (r *LocalRuntime) Prune(ctx context.Context, maxWorkers int, force bool) ([]string, map[string]error, error) { var ( @@ -1129,59 +1201,3 @@ func (r *LocalRuntime) Commit(ctx context.Context, c *cliconfig.CommitValues, co } return newImage.ID(), nil } - -// Exec a command in a container -func (r *LocalRuntime) Exec(c *cliconfig.ExecValues, cmd []string) error { - var ctr *Container - var err error - - if c.Latest { - ctr, err = r.GetLatestContainer() - } else { - ctr, err = r.LookupContainer(c.InputArgs[0]) - } - if err != nil { - return errors.Wrapf(err, "unable to exec into %s", c.InputArgs[0]) - } - - if c.PreserveFDs > 0 { - entries, err := ioutil.ReadDir("/proc/self/fd") - if err != nil { - return errors.Wrapf(err, "unable to read /proc/self/fd") - } - m := make(map[int]bool) - for _, e := range entries { - i, err := strconv.Atoi(e.Name()) - if err != nil { - return errors.Wrapf(err, "cannot parse %s in /proc/self/fd", e.Name()) - } - m[i] = true - } - for i := 3; i < 3+c.PreserveFDs; i++ { - if _, found := m[i]; !found { - return errors.New("invalid --preserve-fds=N specified. Not enough FDs available") - } - } - } - - // ENVIRONMENT VARIABLES - env := map[string]string{} - - if err := parse.ReadKVStrings(env, []string{}, c.Env); err != nil { - return errors.Wrapf(err, "unable to process environment variables") - } - envs := []string{} - for k, v := range env { - envs = append(envs, fmt.Sprintf("%s=%s", k, v)) - } - - streams := new(libpod.AttachStreams) - streams.OutputStream = os.Stdout - streams.ErrorStream = os.Stderr - streams.InputStream = os.Stdin - streams.AttachOutput = true - streams.AttachError = true - streams.AttachInput = true - - return ctr.Exec(c.Tty, c.Privileged, envs, cmd, c.User, c.Workdir, streams, c.PreserveFDs) -} diff --git a/pkg/adapter/containers_remote.go b/pkg/adapter/containers_remote.go index fc23381a4..6b9fc8ee7 100644 --- a/pkg/adapter/containers_remote.go +++ b/pkg/adapter/containers_remote.go @@ -14,6 +14,7 @@ import ( "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/cmd/podman/shared/parse" iopodman "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" @@ -1034,7 +1035,42 @@ func (r *LocalRuntime) Commit(ctx context.Context, c *cliconfig.CommitValues, co return iid, nil } -// Exec executes a container in a running container -func (r *LocalRuntime) Exec(c *cliconfig.ExecValues, cmd []string) error { - return define.ErrNotImplemented +// ExecContainer executes a command in the container +func (r *LocalRuntime) ExecContainer(ctx context.Context, cli *cliconfig.ExecValues) (int, error) { + // default invalid command exit code + ec := 125 + // Validate given environment variables + env := map[string]string{} + if err := parse.ReadKVStrings(env, []string{}, cli.Env); err != nil { + return -1, errors.Wrapf(err, "Exec unable to process environment variables") + } + + // Build env slice of key=value strings for Exec + envs := []string{} + for k, v := range env { + envs = append(envs, fmt.Sprintf("%s=%s", k, v)) + } + + opts := iopodman.ExecOpts{ + Name: cli.InputArgs[0], + Tty: cli.Tty, + Privileged: cli.Privileged, + Cmd: cli.InputArgs[1:], + User: &cli.User, + Workdir: &cli.Workdir, + Env: &envs, + } + + receive, err := iopodman.ExecContainer().Send(r.Conn, varlink.Upgrade, opts) + if err != nil { + return ec, errors.Wrapf(err, "Exec failed to contact service for %s", cli.InputArgs) + } + + _, err = receive() + if err != nil { + return ec, errors.Wrapf(err, "Exec operation failed for %s", cli.InputArgs) + } + + // TODO return exit code from exec call + return 0, nil } diff --git a/pkg/adapter/terminal_linux.go b/pkg/adapter/terminal_linux.go index 9f6ddc2e6..de2600b75 100644 --- a/pkg/adapter/terminal_linux.go +++ b/pkg/adapter/terminal_linux.go @@ -13,10 +13,33 @@ import ( "k8s.io/client-go/tools/remotecommand" ) +// ExecAttachCtr execs and attaches to a container +func ExecAttachCtr(ctx context.Context, ctr *libpod.Container, tty, privileged bool, env, cmd []string, user, workDir string, streams *libpod.AttachStreams, preserveFDs int, detachKeys string) (int, error) { + resize := make(chan remotecommand.TerminalSize) + + haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd())) + + // Check if we are attached to a terminal. If we are, generate resize + // events, and set the terminal to raw mode + if haveTerminal && tty { + cancel, oldTermState, err := handleTerminalAttach(ctx, resize) + if err != nil { + return -1, err + } + defer cancel() + defer func() { + if err := restoreTerminal(oldTermState); err != nil { + logrus.Errorf("unable to restore terminal: %q", err) + } + }() + } + return ctr.Exec(tty, privileged, env, cmd, user, workDir, streams, preserveFDs, resize, detachKeys) +} + // StartAttachCtr starts and (if required) attaches to a container // if you change the signature of this function from os.File to io.Writer, it will trigger a downstream // error. we may need to just lint disable this one. -func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, stdin *os.File, detachKeys string, sigProxy bool, startContainer bool, recursive bool) error { +func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, stdin *os.File, detachKeys string, sigProxy bool, startContainer bool, recursive bool) error { //nolint-interfacer resize := make(chan remotecommand.TerminalSize) haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd())) @@ -24,28 +47,16 @@ func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, // Check if we are attached to a terminal. If we are, generate resize // events, and set the terminal to raw mode if haveTerminal && ctr.Spec().Process.Terminal { - logrus.Debugf("Handling terminal attach") - - subCtx, cancel := context.WithCancel(ctx) - defer cancel() - - resizeTty(subCtx, resize) - - oldTermState, err := term.SaveState(os.Stdin.Fd()) + cancel, oldTermState, err := handleTerminalAttach(ctx, resize) if err != nil { - return errors.Wrapf(err, "unable to save terminal state") - } - - logrus.SetFormatter(&RawTtyFormatter{}) - if _, err := term.SetRawTerminal(os.Stdin.Fd()); err != nil { return err } - defer func() { if err := restoreTerminal(oldTermState); err != nil { logrus.Errorf("unable to restore terminal: %q", err) } }() + defer cancel() } streams := new(libpod.AttachStreams) @@ -97,3 +108,25 @@ func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, return nil } + +func handleTerminalAttach(ctx context.Context, resize chan remotecommand.TerminalSize) (context.CancelFunc, *term.State, error) { + logrus.Debugf("Handling terminal attach") + + subCtx, cancel := context.WithCancel(ctx) + + resizeTty(subCtx, resize) + + oldTermState, err := term.SaveState(os.Stdin.Fd()) + if err != nil { + // allow caller to not have to do any cleaning up if we error here + cancel() + return nil, nil, errors.Wrapf(err, "unable to save terminal state") + } + + logrus.SetFormatter(&RawTtyFormatter{}) + if _, err := term.SetRawTerminal(os.Stdin.Fd()); err != nil { + return cancel, nil, err + } + + return cancel, oldTermState, nil +} diff --git a/pkg/util/utils_linux.go b/pkg/util/utils_linux.go index 318bd2b1b..288137ca5 100644 --- a/pkg/util/utils_linux.go +++ b/pkg/util/utils_linux.go @@ -39,8 +39,8 @@ func FindDeviceNodes() (map[string]string, error) { if !ok { return errors.Errorf("Could not convert stat output for use") } - major := uint64(sysstat.Rdev / 256) - minor := uint64(sysstat.Rdev % 256) + major := sysstat.Rdev / 256 + minor := sysstat.Rdev % 256 nodes[fmt.Sprintf("%d:%d", major, minor)] = path diff --git a/pkg/varlinkapi/attach.go b/pkg/varlinkapi/attach.go index afa88e6a3..97ba525a5 100644 --- a/pkg/varlinkapi/attach.go +++ b/pkg/varlinkapi/attach.go @@ -82,9 +82,10 @@ func (i *LibpodAPI) Attach(call iopodman.VarlinkCall, name string, detachKeys st if finalErr != define.ErrDetach && finalErr != nil { logrus.Error(finalErr) } - quitWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.Quit) - _, err = quitWriter.Write([]byte("HANG-UP")) - // TODO error handling is not quite right here yet + + if err = virtwriter.HangUp(writer); err != nil { + logrus.Errorf("Failed to HANG-UP attach to %s: %s", ctr.ID(), err.Error()) + } return call.Writer.Flush() } diff --git a/pkg/varlinkapi/containers.go b/pkg/varlinkapi/containers.go index 6f6909fac..19a8bfd2e 100644 --- a/pkg/varlinkapi/containers.go +++ b/pkg/varlinkapi/containers.go @@ -19,8 +19,11 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/varlinkapi/virtwriter" "github.com/containers/storage/pkg/archive" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/client-go/tools/remotecommand" ) // ListContainers ... @@ -756,3 +759,82 @@ func (i *LibpodAPI) Top(call iopodman.VarlinkCall, nameOrID string, descriptors } return call.ReplyTop(topInfo) } + +// ExecContainer is the varlink endpoint to execute a command in a container +func (i *LibpodAPI) ExecContainer(call iopodman.VarlinkCall, opts iopodman.ExecOpts) error { + if !call.WantsUpgrade() { + return call.ReplyErrorOccurred("client must use upgraded connection to exec") + } + + ctr, err := i.Runtime.LookupContainer(opts.Name) + if err != nil { + return call.ReplyContainerNotFound(opts.Name, err.Error()) + } + + state, err := ctr.State() + if err != nil { + return call.ReplyErrorOccurred( + fmt.Sprintf("exec failed to obtain container %s state: %s", ctr.ID(), err.Error())) + } + + if state != define.ContainerStateRunning { + return call.ReplyErrorOccurred( + fmt.Sprintf("exec requires a running container, %s is %s", ctr.ID(), state.String())) + } + + envs := []string{} + if opts.Env != nil { + envs = *opts.Env + } + + var user string + if opts.User != nil { + user = *opts.User + } + + var workDir string + if opts.Workdir != nil { + workDir = *opts.Workdir + } + + resizeChan := make(chan remotecommand.TerminalSize) + errChan := make(chan error) + + reader, writer, _, pipeWriter, streams := setupStreams(call) + + go func() { + fmt.Printf("ExecContainer Start Reader\n") + if err := virtwriter.Reader(reader, nil, nil, pipeWriter, resizeChan); err != nil { + fmt.Printf("ExecContainer Reader err %s, %s\n", err.Error(), errors.Cause(err).Error()) + errChan <- err + } + }() + + // Debugging... + time.Sleep(5 * time.Second) + + go func() { + fmt.Printf("ExecContainer Start ctr.Exec\n") + // TODO detach keys and resize + // TODO add handling for exit code + // TODO capture exit code and return to main thread + _, err := ctr.Exec(opts.Tty, opts.Privileged, envs, opts.Cmd, user, workDir, streams, 0, nil, "") + if err != nil { + fmt.Printf("ExecContainer Exec err %s, %s\n", err.Error(), errors.Cause(err).Error()) + errChan <- errors.Wrapf(err, "ExecContainer failed for container %s", ctr.ID()) + } + }() + + execErr := <-errChan + + if execErr != nil && errors.Cause(execErr) != io.EOF { + fmt.Printf("ExecContainer err: %s\n", execErr.Error()) + return call.ReplyErrorOccurred(execErr.Error()) + } + + if err = virtwriter.HangUp(writer); err != nil { + fmt.Printf("ExecContainer hangup err: %s\n", err.Error()) + logrus.Errorf("ExecContainer failed to HANG-UP on %s: %s", ctr.ID(), err.Error()) + } + return call.Writer.Flush() +} diff --git a/pkg/varlinkapi/virtwriter/virtwriter.go b/pkg/varlinkapi/virtwriter/virtwriter.go index 5e88914b2..0da2a91fc 100644 --- a/pkg/varlinkapi/virtwriter/virtwriter.go +++ b/pkg/varlinkapi/virtwriter/virtwriter.go @@ -4,8 +4,9 @@ import ( "bufio" "encoding/binary" "encoding/json" - "errors" "io" + + "github.com/pkg/errors" "k8s.io/client-go/tools/remotecommand" ) @@ -95,7 +96,7 @@ func Reader(r *bufio.Reader, output io.Writer, errput io.Writer, input io.Writer for { n, err := io.ReadFull(r, headerBytes) if err != nil { - return err + return errors.Wrapf(err, "Virtual Read failed, %d", n) } if n < 8 { return errors.New("short read and no full header read") @@ -151,3 +152,19 @@ func Reader(r *bufio.Reader, output io.Writer, errput io.Writer, input io.Writer } } } + +// HangUp sends message to peer to close connection +func HangUp(writer *bufio.Writer) (err error) { + n := 0 + msg := []byte("HANG-UP") + + writeQuit := NewVirtWriteCloser(writer, Quit) + if n, err = writeQuit.Write(msg); err != nil { + return + } + + if n != len(msg) { + return errors.Errorf("Failed to send complete %s message", string(msg)) + } + return +} diff --git a/test/e2e/exec_test.go b/test/e2e/exec_test.go index f42831ebb..76515cf3e 100644 --- a/test/e2e/exec_test.go +++ b/test/e2e/exec_test.go @@ -169,4 +169,24 @@ var _ = Describe("Podman exec", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(1)) }) + + It("podman exec cannot be invoked", func() { + setup := podmanTest.RunTopContainer("test1") + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + session := podmanTest.Podman([]string{"exec", "test1", "/etc"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(126)) + }) + + It("podman exec command not found", func() { + setup := podmanTest.RunTopContainer("test1") + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + session := podmanTest.Podman([]string{"exec", "test1", "notthere"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(127)) + }) }) diff --git a/vendor/github.com/containers/image/copy/copy.go b/vendor/github.com/containers/image/copy/copy.go index 3ed8a2b82..f1b029f97 100644 --- a/vendor/github.com/containers/image/copy/copy.go +++ b/vendor/github.com/containers/image/copy/copy.go @@ -597,15 +597,32 @@ func (c *copier) createProgressBar(pool *mpb.Progress, info types.BlobInfo, kind prefix = prefix[:maxPrefixLen] } - bar := pool.AddBar(info.Size, - mpb.BarClearOnComplete(), - mpb.PrependDecorators( - decor.Name(prefix), - ), - mpb.AppendDecorators( - decor.OnComplete(decor.CountersKibiByte("%.1f / %.1f"), " "+onComplete), - ), - ) + // Use a normal progress bar when we know the size (i.e., size > 0). + // Otherwise, use a spinner to indicate that something's happening. + var bar *mpb.Bar + if info.Size > 0 { + bar = pool.AddBar(info.Size, + mpb.BarClearOnComplete(), + mpb.PrependDecorators( + decor.Name(prefix), + ), + mpb.AppendDecorators( + decor.OnComplete(decor.CountersKibiByte("%.1f / %.1f"), " "+onComplete), + ), + ) + } else { + bar = pool.AddSpinner(info.Size, + mpb.SpinnerOnLeft, + mpb.BarClearOnComplete(), + mpb.SpinnerStyle([]string{".", "..", "...", "....", ""}), + mpb.PrependDecorators( + decor.Name(prefix), + ), + mpb.AppendDecorators( + decor.OnComplete(decor.Name(""), " "+onComplete), + ), + ) + } if c.progressOutput == ioutil.Discard { c.Printf("Copying %s %s\n", kind, info.Digest) } diff --git a/vendor/github.com/containers/image/docker/docker_image_src.go b/vendor/github.com/containers/image/docker/docker_image_src.go index c43e6e7ca..6951f31e9 100644 --- a/vendor/github.com/containers/image/docker/docker_image_src.go +++ b/vendor/github.com/containers/image/docker/docker_image_src.go @@ -138,8 +138,9 @@ func (s *dockerImageSource) GetManifest(ctx context.Context, instanceDigest *dig func (s *dockerImageSource) fetchManifest(ctx context.Context, tagOrDigest string) ([]byte, string, error) { path := fmt.Sprintf(manifestPath, reference.Path(s.ref.ref), tagOrDigest) - headers := make(map[string][]string) - headers["Accept"] = manifest.DefaultRequestedManifestMIMETypes + headers := map[string][]string{ + "Accept": manifest.DefaultRequestedManifestMIMETypes, + } res, err := s.c.makeRequest(ctx, "GET", path, headers, nil, v2Auth, nil) if err != nil { return nil, "", err @@ -381,11 +382,9 @@ func deleteImage(ctx context.Context, sys *types.SystemContext, ref dockerRefere return err } - // When retrieving the digest from a registry >= 2.3 use the following header: - // "Accept": "application/vnd.docker.distribution.manifest.v2+json" - headers := make(map[string][]string) - headers["Accept"] = []string{manifest.DockerV2Schema2MediaType} - + headers := map[string][]string{ + "Accept": manifest.DefaultRequestedManifestMIMETypes, + } refTail, err := ref.tagOrDigest() if err != nil { return err diff --git a/vendor/github.com/containers/image/docker/tarfile/src.go b/vendor/github.com/containers/image/docker/tarfile/src.go index 03735f8a4..dd5d78fe8 100644 --- a/vendor/github.com/containers/image/docker/tarfile/src.go +++ b/vendor/github.com/containers/image/docker/tarfile/src.go @@ -15,24 +15,24 @@ import ( "github.com/containers/image/manifest" "github.com/containers/image/pkg/compression" "github.com/containers/image/types" - "github.com/opencontainers/go-digest" + digest "github.com/opencontainers/go-digest" "github.com/pkg/errors" ) // Source is a partial implementation of types.ImageSource for reading from tarPath. type Source struct { tarPath string - removeTarPathOnClose bool // Remove temp file on close if true - cacheDataLock sync.Once // Atomic way to ensure that ensureCachedDataIsPresent is only invoked once + removeTarPathOnClose bool // Remove temp file on close if true // The following data is only available after ensureCachedDataIsPresent() succeeds - cacheDataResult error // The return value of ensureCachedDataIsPresent, since it should be as safe to cache as the side effects tarManifest *ManifestItem // nil if not available yet. configBytes []byte configDigest digest.Digest orderedDiffIDList []digest.Digest knownLayers map[digest.Digest]*layerInfo // Other state - generatedManifest []byte // Private cache for GetManifest(), nil if not set yet. + generatedManifest []byte // Private cache for GetManifest(), nil if not set yet. + cacheDataLock sync.Once // Private state for ensureCachedDataIsPresent to make it concurrency-safe + cacheDataResult error // Private state for ensureCachedDataIsPresent } type layerInfo struct { @@ -201,47 +201,50 @@ func (s *Source) readTarComponent(path string) ([]byte, error) { } // ensureCachedDataIsPresent loads data necessary for any of the public accessors. +// It is safe to call this from multi-threaded code. func (s *Source) ensureCachedDataIsPresent() error { s.cacheDataLock.Do(func() { - // Read and parse manifest.json - tarManifest, err := s.loadTarManifest() - if err != nil { - s.cacheDataResult = err - return - } + s.cacheDataResult = s.ensureCachedDataIsPresentPrivate() + }) + return s.cacheDataResult +} - // Check to make sure length is 1 - if len(tarManifest) != 1 { - s.cacheDataResult = errors.Errorf("Unexpected tar manifest.json: expected 1 item, got %d", len(tarManifest)) - return - } +// ensureCachedDataIsPresentPrivate is a private implementation detail of ensureCachedDataIsPresent. +// Call ensureCachedDataIsPresent instead. +func (s *Source) ensureCachedDataIsPresentPrivate() error { + // Read and parse manifest.json + tarManifest, err := s.loadTarManifest() + if err != nil { + return err + } - // Read and parse config. - configBytes, err := s.readTarComponent(tarManifest[0].Config) - if err != nil { - s.cacheDataResult = err - return - } - var parsedConfig manifest.Schema2Image // There's a lot of info there, but we only really care about layer DiffIDs. - if err := json.Unmarshal(configBytes, &parsedConfig); err != nil { - s.cacheDataResult = errors.Wrapf(err, "Error decoding tar config %s", tarManifest[0].Config) - return - } + // Check to make sure length is 1 + if len(tarManifest) != 1 { + return errors.Errorf("Unexpected tar manifest.json: expected 1 item, got %d", len(tarManifest)) + } - knownLayers, err := s.prepareLayerData(&tarManifest[0], &parsedConfig) - if err != nil { - s.cacheDataResult = err - return - } + // Read and parse config. + configBytes, err := s.readTarComponent(tarManifest[0].Config) + if err != nil { + return err + } + var parsedConfig manifest.Schema2Image // There's a lot of info there, but we only really care about layer DiffIDs. + if err := json.Unmarshal(configBytes, &parsedConfig); err != nil { + return errors.Wrapf(err, "Error decoding tar config %s", tarManifest[0].Config) + } - // Success; commit. - s.tarManifest = &tarManifest[0] - s.configBytes = configBytes - s.configDigest = digest.FromBytes(configBytes) - s.orderedDiffIDList = parsedConfig.RootFS.DiffIDs - s.knownLayers = knownLayers - }) - return s.cacheDataResult + knownLayers, err := s.prepareLayerData(&tarManifest[0], &parsedConfig) + if err != nil { + return err + } + + // Success; commit. + s.tarManifest = &tarManifest[0] + s.configBytes = configBytes + s.configDigest = digest.FromBytes(configBytes) + s.orderedDiffIDList = parsedConfig.RootFS.DiffIDs + s.knownLayers = knownLayers + return nil } // loadTarManifest loads and decodes the manifest.json. diff --git a/vendor/github.com/containers/image/manifest/docker_schema1.go b/vendor/github.com/containers/image/manifest/docker_schema1.go index 1ba65d5be..b76286b8f 100644 --- a/vendor/github.com/containers/image/manifest/docker_schema1.go +++ b/vendor/github.com/containers/image/manifest/docker_schema1.go @@ -226,6 +226,7 @@ func (m *Schema1) Inspect(_ func(types.BlobInfo) ([]byte, error)) (*types.ImageI } if s1.Config != nil { i.Labels = s1.Config.Labels + i.Env = s1.Config.Env } return i, nil } diff --git a/vendor/github.com/containers/image/manifest/docker_schema2.go b/vendor/github.com/containers/image/manifest/docker_schema2.go index 0671aed9f..76a80e5a6 100644 --- a/vendor/github.com/containers/image/manifest/docker_schema2.go +++ b/vendor/github.com/containers/image/manifest/docker_schema2.go @@ -241,6 +241,7 @@ func (m *Schema2) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*t } if s2.Config != nil { i.Labels = s2.Config.Labels + i.Env = s2.Config.Env } return i, nil } diff --git a/vendor/github.com/containers/image/manifest/oci.go b/vendor/github.com/containers/image/manifest/oci.go index 91705045b..dd65e0ba2 100644 --- a/vendor/github.com/containers/image/manifest/oci.go +++ b/vendor/github.com/containers/image/manifest/oci.go @@ -116,6 +116,7 @@ func (m *OCI1) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*type Architecture: v1.Architecture, Os: v1.OS, Layers: layerInfosToStrings(m.LayerInfos()), + Env: d1.Config.Env, } return i, nil } diff --git a/vendor/github.com/containers/image/ostree/ostree_dest.go b/vendor/github.com/containers/image/ostree/ostree_dest.go index d69f4fa33..06a905aed 100644 --- a/vendor/github.com/containers/image/ostree/ostree_dest.go +++ b/vendor/github.com/containers/image/ostree/ostree_dest.go @@ -1,4 +1,4 @@ -// +build !containers_image_ostree_stub +// +build containers_image_ostree package ostree @@ -218,7 +218,7 @@ func fixFiles(selinuxHnd *C.struct_selabel_handle, root string, dir string, user defer C.free(unsafe.Pointer(fullpathC)) res, err = C.lsetfilecon_raw(fullpathC, context) if int(res) < 0 { - return errors.Wrapf(err, "cannot setfilecon_raw %s", fullpath) + return errors.Wrapf(err, "cannot setfilecon_raw %s to %s", fullpath, C.GoString(context)) } } } diff --git a/vendor/github.com/containers/image/ostree/ostree_src.go b/vendor/github.com/containers/image/ostree/ostree_src.go index 35d852139..dc52ccb6e 100644 --- a/vendor/github.com/containers/image/ostree/ostree_src.go +++ b/vendor/github.com/containers/image/ostree/ostree_src.go @@ -1,4 +1,4 @@ -// +build !containers_image_ostree_stub +// +build containers_image_ostree package ostree diff --git a/vendor/github.com/containers/image/ostree/ostree_transport.go b/vendor/github.com/containers/image/ostree/ostree_transport.go index c9856530b..2e86623ac 100644 --- a/vendor/github.com/containers/image/ostree/ostree_transport.go +++ b/vendor/github.com/containers/image/ostree/ostree_transport.go @@ -1,4 +1,4 @@ -// +build !containers_image_ostree_stub +// +build containers_image_ostree package ostree diff --git a/vendor/github.com/containers/image/pkg/docker/config/config.go b/vendor/github.com/containers/image/pkg/docker/config/config.go index 57b548e26..2e6bb378f 100644 --- a/vendor/github.com/containers/image/pkg/docker/config/config.go +++ b/vendor/github.com/containers/image/pkg/docker/config/config.go @@ -56,6 +56,7 @@ func SetAuthentication(sys *types.SystemContext, registry, username, password st // If an entry is not found empty strings are returned for the username and password func GetAuthentication(sys *types.SystemContext, registry string) (string, string, error) { if sys != nil && sys.DockerAuthConfig != nil { + logrus.Debug("Returning credentials from DockerAuthConfig") return sys.DockerAuthConfig.Username, sys.DockerAuthConfig.Password, nil } @@ -76,12 +77,15 @@ func GetAuthentication(sys *types.SystemContext, registry string) (string, strin legacyFormat := path == dockerLegacyPath username, password, err := findAuthentication(registry, path, legacyFormat) if err != nil { + logrus.Debugf("Credentials not found") return "", "", err } if username != "" && password != "" { + logrus.Debugf("Returning credentials from %s", path) return username, password, nil } } + logrus.Debugf("Credentials not found") return "", "", nil } diff --git a/vendor/github.com/containers/image/pkg/sysregistriesv2/system_registries_v2.go b/vendor/github.com/containers/image/pkg/sysregistriesv2/system_registries_v2.go index 361e6fc60..0c13913ed 100644 --- a/vendor/github.com/containers/image/pkg/sysregistriesv2/system_registries_v2.go +++ b/vendor/github.com/containers/image/pkg/sysregistriesv2/system_registries_v2.go @@ -30,10 +30,10 @@ const builtinRegistriesConfPath = "/etc/containers/registries.conf" // Endpoint describes a remote location of a registry. type Endpoint struct { // The endpoint's remote location. - Location string `toml:"location"` + Location string `toml:"location,omitempty"` // If true, certs verification will be skipped and HTTP (non-TLS) // connections will be allowed. - Insecure bool `toml:"insecure"` + Insecure bool `toml:"insecure,omitempty"` } // rewriteReference will substitute the provided reference `prefix` to the @@ -56,22 +56,22 @@ func (e *Endpoint) rewriteReference(ref reference.Named, prefix string) (referen // Registry represents a registry. type Registry struct { + // Prefix is used for matching images, and to translate one namespace to + // another. If `Prefix="example.com/bar"`, `location="example.com/foo/bar"` + // and we pull from "example.com/bar/myimage:latest", the image will + // effectively be pulled from "example.com/foo/bar/myimage:latest". + // If no Prefix is specified, it defaults to the specified location. + Prefix string `toml:"prefix"` // A registry is an Endpoint too Endpoint // The registry's mirrors. - Mirrors []Endpoint `toml:"mirror"` + Mirrors []Endpoint `toml:"mirror,omitempty"` // If true, pulling from the registry will be blocked. - Blocked bool `toml:"blocked"` + Blocked bool `toml:"blocked,omitempty"` // If true, mirrors will only be used for digest pulls. Pulling images by // tag can potentially yield different images, depending on which endpoint // we pull from. Forcing digest-pulls for mirrors avoids that issue. - MirrorByDigestOnly bool `toml:"mirror-by-digest-only"` - // Prefix is used for matching images, and to translate one namespace to - // another. If `Prefix="example.com/bar"`, `location="example.com/foo/bar"` - // and we pull from "example.com/bar/myimage:latest", the image will - // effectively be pulled from "example.com/foo/bar/myimage:latest". - // If no Prefix is specified, it defaults to the specified location. - Prefix string `toml:"prefix"` + MirrorByDigestOnly bool `toml:"mirror-by-digest-only,omitempty"` } // PullSource consists of an Endpoint and a Reference. Note that the reference is diff --git a/vendor/github.com/containers/image/storage/storage_image.go b/vendor/github.com/containers/image/storage/storage_image.go index b39d2bcc0..946a85f7b 100644 --- a/vendor/github.com/containers/image/storage/storage_image.go +++ b/vendor/github.com/containers/image/storage/storage_image.go @@ -491,14 +491,21 @@ func (s *storageImageDestination) TryReusingBlob(ctx context.Context, blobinfo t // Does the blob correspond to a known DiffID which we already have available? // Because we must return the size, which is unknown for unavailable compressed blobs, the returned BlobInfo refers to the - // uncompressed layer, and that can happen only if canSubstitute. - if canSubstitute { + // uncompressed layer, and that can happen only if canSubstitute, or if the incoming manifest already specifies the size. + if canSubstitute || blobinfo.Size != -1 { if uncompressedDigest := cache.UncompressedDigest(blobinfo.Digest); uncompressedDigest != "" && uncompressedDigest != blobinfo.Digest { layers, err := s.imageRef.transport.store.LayersByUncompressedDigest(uncompressedDigest) if err != nil && errors.Cause(err) != storage.ErrLayerUnknown { return false, types.BlobInfo{}, errors.Wrapf(err, `Error looking for layers with digest %q`, uncompressedDigest) } if len(layers) > 0 { + if blobinfo.Size != -1 { + s.blobDiffIDs[blobinfo.Digest] = layers[0].UncompressedDigest + return true, blobinfo, nil + } + if !canSubstitute { + return false, types.BlobInfo{}, fmt.Errorf("Internal error: canSubstitute was expected to be true for blobInfo %v", blobinfo) + } s.blobDiffIDs[uncompressedDigest] = layers[0].UncompressedDigest return true, types.BlobInfo{ Digest: uncompressedDigest, @@ -627,7 +634,7 @@ func (s *storageImageDestination) Commit(ctx context.Context) error { if !ok { // Try to find the layer with contents matching that blobsum. layer := "" - layers, err2 := s.imageRef.transport.store.LayersByUncompressedDigest(blob.Digest) + layers, err2 := s.imageRef.transport.store.LayersByUncompressedDigest(diffID) if err2 == nil && len(layers) > 0 { layer = layers[0].ID } else { diff --git a/vendor/github.com/containers/image/transports/alltransports/ostree.go b/vendor/github.com/containers/image/transports/alltransports/ostree.go index 4a3b29a00..cc4d69fe8 100644 --- a/vendor/github.com/containers/image/transports/alltransports/ostree.go +++ b/vendor/github.com/containers/image/transports/alltransports/ostree.go @@ -1,4 +1,4 @@ -// +build !containers_image_ostree_stub,linux +// +build containers_image_ostree,linux package alltransports diff --git a/vendor/github.com/containers/image/transports/alltransports/ostree_stub.go b/vendor/github.com/containers/image/transports/alltransports/ostree_stub.go index 48fcaa58d..fb5b96e54 100644 --- a/vendor/github.com/containers/image/transports/alltransports/ostree_stub.go +++ b/vendor/github.com/containers/image/transports/alltransports/ostree_stub.go @@ -1,4 +1,4 @@ -// +build containers_image_ostree_stub !linux +// +build !containers_image_ostree !linux package alltransports diff --git a/vendor/github.com/containers/image/types/types.go b/vendor/github.com/containers/image/types/types.go index 789504348..08b4241e0 100644 --- a/vendor/github.com/containers/image/types/types.go +++ b/vendor/github.com/containers/image/types/types.go @@ -398,6 +398,7 @@ type ImageInspectInfo struct { Architecture string Os string Layers []string + Env []string } // DockerAuthConfig contains authorization information for connecting to a registry. diff --git a/vendor/github.com/containers/image/version/version.go b/vendor/github.com/containers/image/version/version.go index 62b2c8bc5..807daf7a2 100644 --- a/vendor/github.com/containers/image/version/version.go +++ b/vendor/github.com/containers/image/version/version.go @@ -8,7 +8,7 @@ const ( // VersionMinor is for functionality in a backwards-compatible manner VersionMinor = 0 // VersionPatch is for backwards-compatible bug fixes - VersionPatch = 0 + VersionPatch = 1 // VersionDev indicates development branch. Releases will be empty string. VersionDev = "" diff --git a/vendor/modules.txt b/vendor/modules.txt index 462ba1408..17b11fd58 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -62,7 +62,7 @@ github.com/containers/buildah/docker github.com/containers/buildah/pkg/blobcache github.com/containers/buildah/pkg/overlay github.com/containers/buildah/pkg/unshare -# github.com/containers/image v2.0.0+incompatible +# github.com/containers/image v2.0.1+incompatible github.com/containers/image/directory github.com/containers/image/docker github.com/containers/image/docker/archive |