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: :. 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, }, ) }