airshipctl/pkg/container/container_docker.go
Ruslan Aliev ec51a71181 Add progress bar and improve cmd output for image build command
This patch provides the ability to show progress bar using
container logs (ubuntu/debian based only).

Change-Id: I86eebe4d368d81c4685fb27ca31b86cbb3dea08d
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Relates-To: #278
2020-09-28 16:11:34 +00:00

318 lines
8.4 KiB
Go

/*
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: <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,
Tty: true,
}
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
}
// GetID returns ID of the container
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,
) 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
}
log.Debug("docker container is started")
return nil
}
// GetContainerLogs returns logs from the container as io.ReadCloser
func (c *DockerContainer) GetContainerLogs() (io.ReadCloser, error) {
return c.dockerClient.ContainerLogs(*c.ctx, c.id, types.ContainerLogsOptions{ShowStdout: true, Follow: 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,
},
)
}
// WaitUntilFinished waits unit container command is finished, return an error if failed
func (c *DockerContainer) WaitUntilFinished() error {
statusCh, errCh := c.dockerClient.ContainerWait(*c.ctx, c.id, container.WaitConditionNotRunning)
log.Debugf("waiting until command is finished...")
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
}