airshipctl/pkg/container/container_docker.go
Ruslan Aliev d1abe6e1ea Decouple implementations and unit tests in container module
Unit test should follow black-box approach.

Change-Id: I98c46c613a73b539f79d8dfe99cd73592792536d
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Closes: #561
2021-07-16 21:02:03 -05:00

374 lines
9.8 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/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"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,
*specs.Platform,
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
// ContainerInspect returns the container state
ContainerInspect(
ctx context.Context,
containerID string,
) (types.ContainerJSON, 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(opts RunCommandOptions) (container.Config, container.HostConfig, error) {
cmd, err := c.GetCmd(opts.Cmd)
if err != nil {
return container.Config{}, container.HostConfig{}, err
}
mounts := make([]mount.Mount, 0)
for _, mnt := range opts.Mounts {
mounts = append(mounts, mount.Mount{
Type: mount.Type(mnt.Type),
Source: mnt.Src,
Target: mnt.Dst,
ReadOnly: mnt.ReadOnly,
})
}
cCfg := container.Config{
Image: c.ImageURL,
Cmd: cmd,
AttachStdin: true,
StdinOnce: true,
OpenStdin: true,
AttachStderr: true,
AttachStdout: true,
Env: opts.EnvVars,
}
hCfg := container.HostConfig{
Binds: opts.Binds,
Mounts: mounts,
Privileged: opts.Privileged,
}
if opts.HostNetwork {
hCfg.NetworkMode = "host"
}
return cCfg, hCfg, nil
}
// 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(opts RunCommandOptions) (err error) {
containerConfig, hostConfig, err := c.getConfig(opts)
if err != nil {
return err
}
resp, err := c.DockerClient.ContainerCreate(
c.Ctx,
&containerConfig,
&hostConfig,
nil,
nil,
"",
)
if err != nil {
return err
}
c.ID = resp.ID
if opts.Input != nil {
conn, attachErr := c.DockerClient.ContainerAttach(c.Ctx, c.ID, types.ContainerAttachOptions{
Stream: true,
Stdin: true,
})
if attachErr != nil {
return attachErr
}
defer conn.Close()
// This code is simplified version of docker cli code
cErr := make(chan error, 1)
// Write to stdin asynchronously
go func() {
_, copyErr := io.Copy(conn.Conn, opts.Input)
cErr <- copyErr
}()
if err = c.DockerClient.ContainerStart(c.Ctx, c.ID, types.ContainerStartOptions{}); err != nil {
<-cErr
return err
}
// lock until error is returned from the write channel
return <-cErr
}
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(opts GetLogOptions) (io.ReadCloser, error) {
return c.DockerClient.ContainerLogs(c.Ctx, c.ID, types.ContainerLogsOptions{
ShowStderr: opts.Stderr,
Follow: opts.Follow,
ShowStdout: opts.Stdout,
})
}
// 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,
},
)
}
// InspectContainer inspect the running container
func (c *DockerContainer) InspectContainer() (State, error) {
json, err := c.DockerClient.ContainerInspect(context.Background(), c.ID)
if err != nil {
log.Debug("Failed to inspect container status")
return State{}, err
}
state := State{
ExitCode: json.ContainerJSONBase.State.ExitCode,
Status: Status(json.ContainerJSONBase.State.Status),
}
return state, err
}
// 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
}