airshipctl/pkg/container/container_docker.go

301 lines
7.7 KiB
Go

package container
import (
"context"
"fmt"
"io"
"io/ioutil"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)
// DockerClient interface that represents abstract Docker client object
// Interface used as a wrapper for upstream docker client object
type DockerClient interface {
// ImageInspectWithRaw returns the image information and its raw
// representation.
ImageInspectWithRaw(
context.Context,
string,
) (types.ImageInspect, []byte, error)
// ImageList returns a list of images in the docker host.
ImageList(
context.Context,
types.ImageListOptions,
) ([]types.ImageSummary, error)
// ImagePull requests the docker host to pull an image from a remote registry.
ImagePull(
context.Context,
string,
types.ImagePullOptions,
) (io.ReadCloser, error)
// ContainerCreate creates a new container based in the given configuration.
ContainerCreate(
context.Context,
*container.Config,
*container.HostConfig,
*network.NetworkingConfig,
string,
) (container.ContainerCreateCreatedBody, error)
// ContainerAttach attaches a connection to a container in the server.
ContainerAttach(
context.Context,
string,
types.ContainerAttachOptions,
) (types.HijackedResponse, error)
//ContainerStart sends a request to the docker daemon to start a container.
ContainerStart(context.Context, string, types.ContainerStartOptions) error
// ContainerWait waits until the specified container is in a certain state
// indicated by the given condition, either "not-running" (default),
// "next-exit", or "removed".
ContainerWait(
context.Context,
string,
container.WaitCondition,
) (<-chan container.ContainerWaitOKBody, <-chan error)
// ContainerLogs returns the logs generated by a container in an
// io.ReadCloser.
ContainerLogs(
context.Context,
string,
types.ContainerLogsOptions,
) (io.ReadCloser, error)
// ContainerRemove kills and removes a container from the docker host.
ContainerRemove(
context.Context,
string,
types.ContainerRemoveOptions,
) error
}
// DockerContainer docker container object wrapper
type DockerContainer struct {
tag string
imageURL string
id string
dockerClient DockerClient
ctx *context.Context
}
// NewDockerClient returns instance of DockerClient.
// Function essentially returns new Docker API client with default values
func NewDockerClient(ctx *context.Context) (DockerClient, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
cli.NegotiateAPIVersion(*ctx)
return cli, nil
}
// NewDockerContainer returns instance of DockerContainer object wrapper.
// Function gets container image url, pointer to execution context and
// DockerClient instance.
//
// url format: <image_path>:<tag>. If tag is not specified "latest" is used
// as default value
func NewDockerContainer(ctx *context.Context, url string, cli DockerClient) (*DockerContainer, error) {
t := "latest"
nameTag := strings.Split(url, ":")
if len(nameTag) == 2 {
t = nameTag[1]
}
cnt := &DockerContainer{
tag: t,
imageURL: url,
id: "",
dockerClient: cli,
ctx: ctx,
}
if err := cnt.ImagePull(); err != nil {
return nil, err
}
return cnt, nil
}
// getCmd identifies container command. Accepts list of strings each element
// represents command part (e.g "sample cmd --key" should be transformed to
// []string{"sample", "command", "--key"})
//
// If input parameter is NOT empty list method returns input parameter
// immediately
//
// If input parameter is empty list method identifies container image and
// tries to extract Cmd option from this image description (i.e. tries to
// identify default command specified in Dockerfile)
func (c *DockerContainer) getCmd(cmd []string) ([]string, error) {
if len(cmd) > 0 {
return cmd, nil
}
id, err := c.getImageId(c.imageURL)
if err != nil {
return nil, err
}
insp, _, err := c.dockerClient.ImageInspectWithRaw(*c.ctx, id)
if err != nil {
return nil, err
}
config := *insp.Config
return config.Cmd, nil
}
// getConfig creates configuration structures for Docker API client.
func (c *DockerContainer) getConfig(
cmd []string,
volumeMounts []string,
envVars []string,
) (container.Config, container.HostConfig) {
cCfg := container.Config{
Image: c.imageURL,
Cmd: cmd,
AttachStdin: true,
OpenStdin: true,
Env: envVars,
}
hCfg := container.HostConfig{
Binds: volumeMounts,
}
return cCfg, hCfg
}
// getImageId return ID of container image specified by URL. Method executes
// ImageList function supplied with "reference" filter
func (c *DockerContainer) getImageId(url string) (string, error) {
kv := filters.KeyValuePair{
Key: "reference",
Value: url,
}
filter := filters.NewArgs(kv)
opts := types.ImageListOptions{
All: false,
Filters: filter,
}
img, err := c.dockerClient.ImageList(*c.ctx, opts)
if err != nil {
return "", err
}
if len(img) == 0 {
return "", ErrEmptyImageList{}
}
return img[0].ID, nil
}
func (c *DockerContainer) GetId() string {
return c.id
}
// ImagePull downloads image for container
func (c *DockerContainer) ImagePull() error {
// TODO (D. Ukov) add logic for searching among local images
// to avoid image download on each execution
resp, err := c.dockerClient.ImagePull(*c.ctx, c.imageURL, types.ImagePullOptions{})
if err != nil {
return err
}
// Wait for image is downloaded
if _, err := ioutil.ReadAll(resp); err != nil {
return err
}
return nil
}
// RunCommand executes specified command in Docker container. Method handles
// container STDIN and volume binds
func (c *DockerContainer) RunCommand(
cmd []string,
containerInput io.Reader,
volumeMounts []string,
envVars []string,
// TODO (D. Ukov) add debug logic
debug bool,
) error {
realCmd, err := c.getCmd(cmd)
if err != nil {
return err
}
containerConfig, hostConfig := c.getConfig(realCmd, volumeMounts, envVars)
resp, err := c.dockerClient.ContainerCreate(
*c.ctx,
&containerConfig,
&hostConfig,
nil,
"",
)
if err != nil {
return err
}
c.id = resp.ID
if containerInput != nil {
conn, attachErr := c.dockerClient.ContainerAttach(*c.ctx, c.id, types.ContainerAttachOptions{
Stream: true,
Stdin: true,
})
if attachErr != nil {
return attachErr
}
if _, err = io.Copy(conn.Conn, containerInput); err != nil {
return err
}
}
if err = c.dockerClient.ContainerStart(*c.ctx, c.id, types.ContainerStartOptions{}); err != nil {
return err
}
statusCh, errCh := c.dockerClient.ContainerWait(*c.ctx, c.id, container.WaitConditionNotRunning)
select {
case err = <-errCh:
if err != nil {
return err
}
case retCode := <-statusCh:
if retCode.StatusCode != 0 {
logsCmd := fmt.Sprintf("docker logs %s", c.id)
return ErrRunContainerCommand{Cmd: logsCmd}
}
}
return nil
}
// RunCommandOutput executes specified command in Docker container and
// returns command output as ReadCloser object. RunCommand debug option is
// set to false explicitly
func (c *DockerContainer) RunCommandOutput(
cmd []string,
containerInput io.Reader,
volumeMounts []string,
envVars []string,
) (io.ReadCloser, error) {
if err := c.RunCommand(cmd, containerInput, volumeMounts, envVars, false); err != nil {
return nil, err
}
return c.dockerClient.ContainerLogs(*c.ctx, c.id, types.ContainerLogsOptions{ShowStdout: true})
}
// RmContainer kills and removes a container from the docker host.
func (c *DockerContainer) RmContainer() error {
return c.dockerClient.ContainerRemove(
*c.ctx,
c.id,
types.ContainerRemoveOptions{
Force: true,
},
)
}