From 0c40b62c77d8f7dba8e73ac3ced0de536ec220d5 Mon Sep 17 00:00:00 2001
From: Matthew Heon <matthew.heon@pm.me>
Date: Fri, 20 Mar 2020 15:23:35 -0400
Subject: [PATCH 1/2] Implement APIv2 Exec Create and Inspect Endpoints

Start and Resize require further implementation work.

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
---
 libpod/container_exec.go        |  64 +++-----------------
 libpod/define/inspect.go        |  54 +++++++++++++++++
 libpod/runtime_ctr.go           |  18 ++++++
 pkg/api/handlers/compat/exec.go | 104 ++++++++++++++++++++++++++++++++
 pkg/api/handlers/types.go       |   8 +++
 pkg/api/server/register_exec.go |  16 ++---
 6 files changed, 200 insertions(+), 64 deletions(-)
 create mode 100644 libpod/define/inspect.go
 create mode 100644 pkg/api/handlers/compat/exec.go

diff --git a/libpod/container_exec.go b/libpod/container_exec.go
index 7ed7a3343a..795eb74537 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
diff --git a/libpod/define/inspect.go b/libpod/define/inspect.go
new file mode 100644
index 0000000000..b7cd13f826
--- /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/runtime_ctr.go b/libpod/runtime_ctr.go
index 0b18436ca1..b99e6df840 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 0000000000..1fa50d23be
--- /dev/null
+++ b/pkg/api/handlers/compat/exec.go
@@ -0,0 +1,104 @@
+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"
+)
+
+// 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
+	}
+
+	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 c6b70251bf..6fa776d0f3 100644
--- a/pkg/api/handlers/types.go
+++ b/pkg/api/handlers/types.go
@@ -177,6 +177,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 d27d21a040..71fb50307e 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
 }

From e42cbdd1b2f6788a814d6aa1838111989cd424ad Mon Sep 17 00:00:00 2001
From: Matthew Heon <matthew.heon@pm.me>
Date: Mon, 23 Mar 2020 16:18:17 -0400
Subject: [PATCH 2/2] Add bindings for Container Exec Create + Inspect

Also adds some basic tests for these two. More tests are needed
but will have to wait for state to be finished.

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
---
 libpod/container_exec.go             |  5 ++
 libpod/options.go                    |  2 +
 pkg/api/handlers/compat/exec.go      |  3 ++
 pkg/bindings/containers/exec.go      | 71 +++++++++++++++++++++++++
 pkg/bindings/test/containers_test.go |  2 +-
 pkg/bindings/test/exec_test.go       | 77 ++++++++++++++++++++++++++++
 pkg/bindings/test/pods_test.go       |  4 +-
 7 files changed, 160 insertions(+), 4 deletions(-)
 create mode 100644 pkg/bindings/containers/exec.go
 create mode 100644 pkg/bindings/test/exec_test.go

diff --git a/libpod/container_exec.go b/libpod/container_exec.go
index 795eb74537..912c2c2268 100644
--- a/libpod/container_exec.go
+++ b/libpod/container_exec.go
@@ -231,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/options.go b/libpod/options.go
index 9b61d7947f..74f9c485e2 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/pkg/api/handlers/compat/exec.go b/pkg/api/handlers/compat/exec.go
index 1fa50d23be..ec1a8ac967 100644
--- a/pkg/api/handlers/compat/exec.go
+++ b/pkg/api/handlers/compat/exec.go
@@ -12,6 +12,7 @@ import (
 	"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.
@@ -88,6 +89,8 @@ func ExecInspectHandler(w http.ResponseWriter, r *http.Request) {
 		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()))
diff --git a/pkg/bindings/containers/exec.go b/pkg/bindings/containers/exec.go
new file mode 100644
index 0000000000..48f9ed697d
--- /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 f5465c803a..55c739865d 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 0000000000..1ef2197b6a
--- /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 e94048a9cc..a1d6ee1840 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())