971c81acdb
This commit allows to specify options to get container logs, such as stderr, stdout and if logs should be followed. Also extends RunCommandOptions with ability to add mounts in addtion to binds Relates-To: #458 Change-Id: I83507f2f7ca6ea596f52f5d3e9f868467458b6a3
357 lines
9.4 KiB
Go
357 lines
9.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/mount"
|
|
"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
|
|
// 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 := []mount.Mount{}
|
|
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.HostNewtork {
|
|
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,
|
|
"",
|
|
)
|
|
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()
|
|
|
|
if _, err = io.Copy(conn.Conn, opts.Input); 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(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
|
|
}
|