d1abe6e1ea
Unit test should follow black-box approach. Change-Id: I98c46c613a73b539f79d8dfe99cd73592792536d Signed-off-by: Ruslan Aliev <raliev@mirantis.com> Closes: #561
374 lines
9.8 KiB
Go
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
|
|
}
|