/* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ 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, }, ) }