e57b3ce4c0
Change-Id: I92c82eecdb35de22cc4da2326632b8af3aadd4cd
325 lines
8.4 KiB
Go
325 lines
8.4 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"
|
|
|
|
"opendev.org/airship/airshipctl/pkg/log"
|
|
)
|
|
|
|
// 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 {
|
|
// skip image download if already downloaded
|
|
// ImageInspectWithRaw returns err when image not found local and
|
|
// in this case it will proceed for ImagePull.
|
|
_, _, err := c.dockerClient.ImageInspectWithRaw(*c.ctx, c.imageURL)
|
|
if err == nil {
|
|
log.Debug("Image Already exists, skip download")
|
|
return nil
|
|
}
|
|
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,
|
|
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
|
|
}
|
|
|
|
if debug {
|
|
log.Debug("start reading container logs")
|
|
var reader io.ReadCloser
|
|
reader, err = c.dockerClient.ContainerLogs(*c.ctx, c.id, types.ContainerLogsOptions{ShowStdout: true, Follow: true})
|
|
if err != nil {
|
|
log.Debugf("failed to read container logs %s", err)
|
|
reader = ioutil.NopCloser(strings.NewReader(""))
|
|
}
|
|
|
|
_, err = io.Copy(log.Writer(), reader)
|
|
if err != nil {
|
|
log.Debugf("failed to write container logs to log output %s", err)
|
|
}
|
|
log.Debug("got EOF from container logs")
|
|
}
|
|
|
|
statusCh, errCh := c.dockerClient.ContainerWait(*c.ctx, c.id, container.WaitConditionNotRunning)
|
|
log.Debugf("waiting until command '%s' is finished...", realCmd)
|
|
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,
|
|
},
|
|
)
|
|
}
|