diff options
| -rw-r--r-- | libpod/container_exec.go | 69 | ||||
| -rw-r--r-- | libpod/define/inspect.go | 54 | ||||
| -rw-r--r-- | libpod/options.go | 2 | ||||
| -rw-r--r-- | libpod/runtime_ctr.go | 18 | ||||
| -rw-r--r-- | pkg/api/handlers/compat/exec.go | 107 | ||||
| -rw-r--r-- | pkg/api/handlers/types.go | 8 | ||||
| -rw-r--r-- | pkg/api/server/register_exec.go | 16 | ||||
| -rw-r--r-- | pkg/bindings/containers/exec.go | 71 | ||||
| -rw-r--r-- | pkg/bindings/test/containers_test.go | 2 | ||||
| -rw-r--r-- | pkg/bindings/test/exec_test.go | 77 | ||||
| -rw-r--r-- | pkg/bindings/test/pods_test.go | 4 | 
11 files changed, 360 insertions, 68 deletions
diff --git a/libpod/container_exec.go b/libpod/container_exec.go index 6b88e5205..5469462f8 100644 --- a/libpod/container_exec.go +++ b/libpod/container_exec.go @@ -94,67 +94,14 @@ func (e *ExecSession) ContainerID() string {  	return e.ContainerId  } -// InspectExecSession contains information about a given exec session. -type InspectExecSession struct { -	// CanRemove is legacy and used purely for compatibility reasons. -	// Will always be set to true, unless the exec session is running. -	CanRemove bool `json:"CanRemove"` -	// ContainerID is the ID of the container this exec session is attached -	// to. -	ContainerID string `json:"ContainerID"` -	// DetachKeys are the detach keys used by the exec session. -	// If set to "" the default keys are being used. -	// Will show "<none>" if no detach keys are set. -	DetachKeys string `json:"DetachKeys"` -	// ExitCode is the exit code of the exec session. Will be set to 0 if -	// the exec session has not yet exited. -	ExitCode int `json:"ExitCode"` -	// ID is the ID of the exec session. -	ID string `json:"ID"` -	// OpenStderr is whether the container's STDERR stream will be attached. -	// Always set to true if the exec session created a TTY. -	OpenStderr bool `json:"OpenStderr"` -	// OpenStdin is whether the container's STDIN stream will be attached -	// to. -	OpenStdin bool `json:"OpenStdin"` -	// OpenStdout is whether the container's STDOUT stream will be attached. -	// Always set to true if the exec session created a TTY. -	OpenStdout bool `json:"OpenStdout"` -	// Running is whether the exec session is running. -	Running bool `json:"Running"` -	// Pid is the PID of the exec session's process. -	// Will be set to 0 if the exec session is not running. -	Pid int `json:"Pid"` -	// ProcessConfig contains information about the exec session's process. -	ProcessConfig *InspectExecProcess `json:"ProcessConfig"` -} - -// InspectExecProcess contains information about the process in a given exec -// session. -type InspectExecProcess struct { -	// Arguments are the arguments to the entrypoint command of the exec -	// session. -	Arguments []string `json:"arguments"` -	// Entrypoint is the entrypoint for the exec session (the command that -	// will be executed in the container). -	Entrypoint string `json:"entrypoint"` -	// Privileged is whether the exec session will be started with elevated -	// privileges. -	Privileged bool `json:"privileged"` -	// Tty is whether the exec session created a terminal. -	Tty bool `json:"tty"` -	// User is the user the exec session was started as. -	User string `json:"user"` -} -  // Inspect inspects the given exec session and produces detailed output on its  // configuration and current state. -func (e *ExecSession) Inspect() (*InspectExecSession, error) { +func (e *ExecSession) Inspect() (*define.InspectExecSession, error) {  	if e.Config == nil {  		return nil, errors.Wrapf(define.ErrInternal, "given exec session does not have a configuration block")  	} -	output := new(InspectExecSession) +	output := new(define.InspectExecSession)  	output.CanRemove = e.State != define.ExecStateRunning  	output.ContainerID = e.ContainerId  	if e.Config.DetachKeys != nil { @@ -167,7 +114,7 @@ func (e *ExecSession) Inspect() (*InspectExecSession, error) {  	output.OpenStdout = e.Config.AttachStdout  	output.Running = e.State == define.ExecStateRunning  	output.Pid = e.PID -	output.ProcessConfig = new(InspectExecProcess) +	output.ProcessConfig = new(define.InspectExecProcess)  	if len(e.Config.Command) > 0 {  		output.ProcessConfig.Entrypoint = e.Config.Command[0]  		if len(e.Config.Command) > 1 { @@ -213,6 +160,11 @@ func (c *Container) ExecCreate(config *ExecConfig) (string, error) {  		return "", errors.Wrapf(define.ErrInvalidArg, "cannot specify streams to attach to when exec session has a pseudoterminal")  	} +	// Verify that we are in a good state to continue +	if !c.ensureState(define.ContainerStateRunning) { +		return "", errors.Wrapf(define.ErrCtrStateInvalid, "can only create exec sessions on running containers") +	} +  	// Generate an ID for our new exec session  	sessionID := stringid.GenerateNonCryptoID()  	found := true @@ -279,6 +231,11 @@ func (c *Container) ExecStartAndAttach(sessionID string, streams *AttachStreams)  		}  	} +	// Verify that we are in a good state to continue +	if !c.ensureState(define.ContainerStateRunning) { +		return errors.Wrapf(define.ErrCtrStateInvalid, "can only start exec sessions when their container is running") +	} +  	session, ok := c.state.ExecSessions[sessionID]  	if !ok {  		return errors.Wrapf(define.ErrNoSuchExecSession, "container %s has no exec session with ID %s", c.ID(), sessionID) diff --git a/libpod/define/inspect.go b/libpod/define/inspect.go new file mode 100644 index 000000000..b7cd13f82 --- /dev/null +++ b/libpod/define/inspect.go @@ -0,0 +1,54 @@ +package define + +// InspectExecSession contains information about a given exec session. +type InspectExecSession struct { +	// CanRemove is legacy and used purely for compatibility reasons. +	// Will always be set to true, unless the exec session is running. +	CanRemove bool `json:"CanRemove"` +	// ContainerID is the ID of the container this exec session is attached +	// to. +	ContainerID string `json:"ContainerID"` +	// DetachKeys are the detach keys used by the exec session. +	// If set to "" the default keys are being used. +	// Will show "<none>" if no detach keys are set. +	DetachKeys string `json:"DetachKeys"` +	// ExitCode is the exit code of the exec session. Will be set to 0 if +	// the exec session has not yet exited. +	ExitCode int `json:"ExitCode"` +	// ID is the ID of the exec session. +	ID string `json:"ID"` +	// OpenStderr is whether the container's STDERR stream will be attached. +	// Always set to true if the exec session created a TTY. +	OpenStderr bool `json:"OpenStderr"` +	// OpenStdin is whether the container's STDIN stream will be attached +	// to. +	OpenStdin bool `json:"OpenStdin"` +	// OpenStdout is whether the container's STDOUT stream will be attached. +	// Always set to true if the exec session created a TTY. +	OpenStdout bool `json:"OpenStdout"` +	// Running is whether the exec session is running. +	Running bool `json:"Running"` +	// Pid is the PID of the exec session's process. +	// Will be set to 0 if the exec session is not running. +	Pid int `json:"Pid"` +	// ProcessConfig contains information about the exec session's process. +	ProcessConfig *InspectExecProcess `json:"ProcessConfig"` +} + +// InspectExecProcess contains information about the process in a given exec +// session. +type InspectExecProcess struct { +	// Arguments are the arguments to the entrypoint command of the exec +	// session. +	Arguments []string `json:"arguments"` +	// Entrypoint is the entrypoint for the exec session (the command that +	// will be executed in the container). +	Entrypoint string `json:"entrypoint"` +	// Privileged is whether the exec session will be started with elevated +	// privileges. +	Privileged bool `json:"privileged"` +	// Tty is whether the exec session created a terminal. +	Tty bool `json:"tty"` +	// User is the user the exec session was started as. +	User string `json:"user"` +} diff --git a/libpod/options.go b/libpod/options.go index 9b61d7947..74f9c485e 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -21,6 +21,8 @@ import (  var (  	// NameRegex is a regular expression to validate container/pod names. +	// This must NOT be changed from outside of Libpod. It should be a +	// constant, but Go won't let us do that.  	NameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")  	// RegexError is thrown in presence of an invalid container/pod name.  	RegexError = errors.Wrapf(define.ErrInvalidArg, "names must match [a-zA-Z0-9][a-zA-Z0-9_.-]*") diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 0b18436ca..b99e6df84 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -830,6 +830,24 @@ func (r *Runtime) GetLatestContainer() (*Container, error) {  	return ctrs[lastCreatedIndex], nil  } +// GetExecSessionContainer gets the container that a given exec session ID is +// attached to. +func (r *Runtime) GetExecSessionContainer(id string) (*Container, error) { +	r.lock.RLock() +	defer r.lock.RUnlock() + +	if !r.valid { +		return nil, define.ErrRuntimeStopped +	} + +	ctrID, err := r.state.GetExecSession(id) +	if err != nil { +		return nil, err +	} + +	return r.state.Container(ctrID) +} +  // PruneContainers removes stopped and exited containers from localstorage.  A set of optional filters  // can be provided to be more granular.  func (r *Runtime) PruneContainers(filterFuncs []ContainerFilter) (map[string]int64, map[string]error, error) { diff --git a/pkg/api/handlers/compat/exec.go b/pkg/api/handlers/compat/exec.go new file mode 100644 index 000000000..ec1a8ac96 --- /dev/null +++ b/pkg/api/handlers/compat/exec.go @@ -0,0 +1,107 @@ +package compat + +import ( +	"encoding/json" +	"fmt" +	"net/http" +	"strings" + +	"github.com/containers/libpod/libpod" +	"github.com/containers/libpod/libpod/define" +	"github.com/containers/libpod/pkg/api/handlers" +	"github.com/containers/libpod/pkg/api/handlers/utils" +	"github.com/gorilla/mux" +	"github.com/pkg/errors" +	"github.com/sirupsen/logrus" +) + +// ExecCreateHandler creates an exec session for a given container. +func ExecCreateHandler(w http.ResponseWriter, r *http.Request) { +	runtime := r.Context().Value("runtime").(*libpod.Runtime) + +	input := new(handlers.ExecCreateConfig) +	if err := json.NewDecoder(r.Body).Decode(&input); err != nil { +		utils.InternalServerError(w, errors.Wrapf(err, "error decoding request body as JSON")) +		return +	} + +	ctrName := utils.GetName(r) +	ctr, err := runtime.LookupContainer(ctrName) +	if err != nil { +		utils.ContainerNotFound(w, ctrName, err) +		return +	} + +	libpodConfig := new(libpod.ExecConfig) +	libpodConfig.Command = input.Cmd +	libpodConfig.Terminal = input.Tty +	libpodConfig.AttachStdin = input.AttachStdin +	libpodConfig.AttachStderr = input.AttachStderr +	libpodConfig.AttachStdout = input.AttachStdout +	if input.DetachKeys != "" { +		libpodConfig.DetachKeys = &input.DetachKeys +	} +	libpodConfig.Environment = make(map[string]string) +	for _, envStr := range input.Env { +		split := strings.SplitN(envStr, "=", 2) +		if len(split) != 2 { +			utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, errors.Errorf("environment variable %q badly formed, must be key=value", envStr)) +			return +		} +		libpodConfig.Environment[split[0]] = split[1] +	} +	libpodConfig.WorkDir = input.WorkingDir +	libpodConfig.Privileged = input.Privileged +	libpodConfig.User = input.User + +	sessID, err := ctr.ExecCreate(libpodConfig) +	if err != nil { +		if errors.Cause(err) == define.ErrCtrStateInvalid { +			// Check if the container is paused. If so, return a 409 +			state, err := ctr.State() +			if err == nil { +				// Ignore the error != nil case. We're already +				// throwing an InternalServerError below. +				if state == define.ContainerStatePaused { +					utils.Error(w, "Container is paused", http.StatusConflict, errors.Errorf("cannot create exec session as container %s is paused", ctr.ID())) +					return +				} +			} +		} +		utils.InternalServerError(w, err) +		return +	} + +	resp := new(handlers.ExecCreateResponse) +	resp.ID = sessID + +	utils.WriteResponse(w, http.StatusCreated, resp) +} + +// ExecInspectHandler inspects a given exec session. +func ExecInspectHandler(w http.ResponseWriter, r *http.Request) { +	runtime := r.Context().Value("runtime").(*libpod.Runtime) + +	sessionID := mux.Vars(r)["id"] +	sessionCtr, err := runtime.GetExecSessionContainer(sessionID) +	if err != nil { +		utils.Error(w, fmt.Sprintf("No such exec session: %s", sessionID), http.StatusNotFound, err) +		return +	} + +	logrus.Debugf("Inspecting exec session %s of container %s", sessionID, sessionCtr.ID()) + +	session, err := sessionCtr.ExecSession(sessionID) +	if err != nil { +		utils.InternalServerError(w, errors.Wrapf(err, "error retrieving exec session %s from container %s", sessionID, sessionCtr.ID())) +		return +	} + +	inspectOut, err := session.Inspect() +	if err != nil { +		utils.InternalServerError(w, err) +		return +	} + +	utils.WriteResponse(w, http.StatusOK, inspectOut) +} diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 84ca0fbed..fe4198c37 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -172,6 +172,14 @@ type ImageTreeResponse struct {  	Layers []ImageLayer `json:"layers"`  } +type ExecCreateConfig struct { +	docker.ExecConfig +} + +type ExecCreateResponse struct { +	docker.IDResponse +} +  func EventToApiEvent(e *events.Event) *Event {  	return &Event{dockerEvents.Message{  		Type:   e.Type.String(), diff --git a/pkg/api/server/register_exec.go b/pkg/api/server/register_exec.go index d27d21a04..71fb50307 100644 --- a/pkg/api/server/register_exec.go +++ b/pkg/api/server/register_exec.go @@ -8,7 +8,7 @@ import (  )  func (s *APIServer) registerExecHandlers(r *mux.Router) error { -	// swagger:operation POST /containers/{name}/create compat createExec +	// swagger:operation POST /containers/{name}/exec compat createExec  	// ---  	// tags:  	//   - exec (compat) @@ -74,9 +74,9 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error {  	//	   description: container is paused  	//   500:  	//     $ref: "#/responses/InternalError" -	r.Handle(VersionedPath("/containers/{name}/create"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) +	r.Handle(VersionedPath("/containers/{name}/exec"), s.APIHandler(compat.ExecCreateHandler)).Methods(http.MethodPost)  	// Added non version path to URI to support docker non versioned paths -	r.Handle("/containers/{name}/create", s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) +	r.Handle("/containers/{name}/exec", s.APIHandler(compat.ExecCreateHandler)).Methods(http.MethodPost)  	// swagger:operation POST /exec/{id}/start compat startExec  	// ---  	// tags: @@ -169,15 +169,15 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error {  	//     $ref: "#/responses/NoSuchExecInstance"  	//   500:  	//     $ref: "#/responses/InternalError" -	r.Handle(VersionedPath("/exec/{id}/json"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodGet) +	r.Handle(VersionedPath("/exec/{id}/json"), s.APIHandler(compat.ExecInspectHandler)).Methods(http.MethodGet)  	// Added non version path to URI to support docker non versioned paths -	r.Handle("/exec/{id}/json", s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodGet) +	r.Handle("/exec/{id}/json", s.APIHandler(compat.ExecInspectHandler)).Methods(http.MethodGet)  	/*  		libpod api follows  	*/ -	// swagger:operation POST /libpod/containers/{name}/create libpod libpodCreateExec +	// swagger:operation POST /libpod/containers/{name}/exec libpod libpodCreateExec  	// ---  	// tags:  	//   - exec @@ -243,7 +243,7 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error {  	//	   description: container is paused  	//   500:  	//     $ref: "#/responses/InternalError" -	r.Handle(VersionedPath("/libpod/containers/{name}/create"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) +	r.Handle(VersionedPath("/libpod/containers/{name}/exec"), s.APIHandler(compat.ExecCreateHandler)).Methods(http.MethodPost)  	// swagger:operation POST /libpod/exec/{id}/start libpod libpodStartExec  	// ---  	// tags: @@ -332,6 +332,6 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error {  	//     $ref: "#/responses/NoSuchExecInstance"  	//   500:  	//     $ref: "#/responses/InternalError" -	r.Handle(VersionedPath("/libpod/exec/{id}/json"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodGet) +	r.Handle(VersionedPath("/libpod/exec/{id}/json"), s.APIHandler(compat.ExecInspectHandler)).Methods(http.MethodGet)  	return nil  } diff --git a/pkg/bindings/containers/exec.go b/pkg/bindings/containers/exec.go new file mode 100644 index 000000000..48f9ed697 --- /dev/null +++ b/pkg/bindings/containers/exec.go @@ -0,0 +1,71 @@ +package containers + +import ( +	"context" +	"net/http" +	"strings" + +	"github.com/containers/libpod/libpod/define" +	"github.com/containers/libpod/pkg/api/handlers" +	"github.com/containers/libpod/pkg/bindings" +	jsoniter "github.com/json-iterator/go" +	"github.com/pkg/errors" +	"github.com/sirupsen/logrus" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +// ExecCreate creates a new exec session in an existing container. +// The exec session will not be started; that is done with ExecStart. +// Returns ID of new exec session, or an error if one occurred. +func ExecCreate(ctx context.Context, nameOrID string, config *handlers.ExecCreateConfig) (string, error) { +	conn, err := bindings.GetClient(ctx) +	if err != nil { +		return "", err +	} + +	if config == nil { +		return "", errors.Errorf("must provide a configuration for exec session") +	} + +	requestJSON, err := json.Marshal(config) +	if err != nil { +		return "", errors.Wrapf(err, "error marshalling exec config to JSON") +	} +	jsonReader := strings.NewReader(string(requestJSON)) + +	resp, err := conn.DoRequest(jsonReader, http.MethodPost, "/containers/%s/exec", nil, nameOrID) +	if err != nil { +		return "", err +	} + +	respStruct := new(handlers.ExecCreateResponse) +	if err := resp.Process(respStruct); err != nil { +		return "", err +	} + +	return respStruct.ID, nil +} + +// ExecInspect inspects an existing exec session, returning detailed information +// about it. +func ExecInspect(ctx context.Context, sessionID string) (*define.InspectExecSession, error) { +	conn, err := bindings.GetClient(ctx) +	if err != nil { +		return nil, err +	} + +	logrus.Debugf("Inspecting session ID %q", sessionID) + +	resp, err := conn.DoRequest(nil, http.MethodGet, "/exec/%s/json", nil, sessionID) +	if err != nil { +		return nil, err +	} + +	respStruct := new(define.InspectExecSession) +	if err := resp.Process(respStruct); err != nil { +		return nil, err +	} + +	return respStruct, nil +} diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index f5465c803..55c739865 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -1,12 +1,12 @@  package test_bindings  import ( -	"github.com/containers/libpod/libpod/define"  	"net/http"  	"strconv"  	"strings"  	"time" +	"github.com/containers/libpod/libpod/define"  	"github.com/containers/libpod/pkg/bindings"  	"github.com/containers/libpod/pkg/bindings/containers"  	"github.com/containers/libpod/pkg/specgen" diff --git a/pkg/bindings/test/exec_test.go b/pkg/bindings/test/exec_test.go new file mode 100644 index 000000000..1ef2197b6 --- /dev/null +++ b/pkg/bindings/test/exec_test.go @@ -0,0 +1,77 @@ +package test_bindings + +import ( +	"time" + +	"github.com/containers/libpod/pkg/api/handlers" +	"github.com/containers/libpod/pkg/bindings" +	"github.com/containers/libpod/pkg/bindings/containers" +	. "github.com/onsi/ginkgo" +	. "github.com/onsi/gomega" +	"github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman containers exec", func() { +	var ( +		bt *bindingTest +		s  *gexec.Session +	) + +	BeforeEach(func() { +		bt = newBindingTest() +		bt.RestoreImagesFromCache() +		s = bt.startAPIService() +		time.Sleep(1 * time.Second) +		err := bt.NewConnection() +		Expect(err).To(BeNil()) +	}) + +	AfterEach(func() { +		s.Kill() +		bt.cleanup() +	}) + +	It("Podman exec create makes an exec session", func() { +		name := "testCtr" +		cid, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) +		Expect(err).To(BeNil()) + +		execConfig := new(handlers.ExecCreateConfig) +		execConfig.Cmd = []string{"echo", "hello world"} + +		sessionID, err := containers.ExecCreate(bt.conn, name, execConfig) +		Expect(err).To(BeNil()) +		Expect(sessionID).To(Not(Equal(""))) + +		inspectOut, err := containers.ExecInspect(bt.conn, sessionID) +		Expect(err).To(BeNil()) +		Expect(inspectOut.ContainerID).To(Equal(cid)) +		Expect(inspectOut.ProcessConfig.Entrypoint).To(Equal("echo")) +		Expect(len(inspectOut.ProcessConfig.Arguments)).To(Equal(1)) +		Expect(inspectOut.ProcessConfig.Arguments[0]).To(Equal("hello world")) +	}) + +	It("Podman exec create with bad command fails", func() { +		name := "testCtr" +		_, err := bt.RunTopContainer(&name, &bindings.PFalse, nil) +		Expect(err).To(BeNil()) + +		execConfig := new(handlers.ExecCreateConfig) + +		_, err = containers.ExecCreate(bt.conn, name, execConfig) +		Expect(err).To(Not(BeNil())) +	}) + +	It("Podman exec create with invalid container fails", func() { +		execConfig := new(handlers.ExecCreateConfig) +		execConfig.Cmd = []string{"echo", "hello world"} + +		_, err := containers.ExecCreate(bt.conn, "doesnotexist", execConfig) +		Expect(err).To(Not(BeNil())) +	}) + +	It("Podman exec inspect on invalid session fails", func() { +		_, err := containers.ExecInspect(bt.conn, "0000000000000000000000000000000000000000000000000000000000000000") +		Expect(err).To(Not(BeNil())) +	}) +}) diff --git a/pkg/bindings/test/pods_test.go b/pkg/bindings/test/pods_test.go index e94048a9c..a1d6ee184 100644 --- a/pkg/bindings/test/pods_test.go +++ b/pkg/bindings/test/pods_test.go @@ -79,9 +79,7 @@ var _ = Describe("Podman pods", func() {  	// The test validates the list pod endpoint with passing filters as the params.  	It("List pods with filters", func() { -		var ( -			newpod2 string = "newpod2" -		) +		newpod2 := "newpod2"  		bt.Podcreate(&newpod2)  		_, err = bt.RunTopContainer(nil, &bindings.PTrue, &newpod)  		Expect(err).To(BeNil())  | 
