github.com/Datadog/cnab-go@v0.3.3-beta1.0.20191007143216-bba4b7e723d0/driver/docker/docker.go (about)

     1  package docker
     2  
     3  import (
     4  	"archive/tar"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	unix_path "path"
    11  
    12  	"github.com/deislabs/cnab-go/driver"
    13  	"github.com/docker/cli/cli/command"
    14  	cliflags "github.com/docker/cli/cli/flags"
    15  	"github.com/docker/distribution/reference"
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/strslice"
    19  	"github.com/docker/docker/client"
    20  	"github.com/docker/docker/pkg/jsonmessage"
    21  	"github.com/docker/docker/pkg/stdcopy"
    22  	"github.com/docker/docker/registry"
    23  )
    24  
    25  // Driver is capable of running Docker invocation images using Docker itself.
    26  type Driver struct {
    27  	config map[string]string
    28  	// If true, this will not actually run Docker
    29  	Simulate                   bool
    30  	dockerCli                  command.Cli
    31  	dockerConfigurationOptions []ConfigurationOption
    32  	containerOut               io.Writer
    33  	containerErr               io.Writer
    34  }
    35  
    36  // Run executes the Docker driver
    37  func (d *Driver) Run(op *driver.Operation) (driver.OperationResult, error) {
    38  	return d.exec(op)
    39  }
    40  
    41  // Handles indicates that the Docker driver supports "docker" and "oci"
    42  func (d *Driver) Handles(dt string) bool {
    43  	return dt == driver.ImageTypeDocker || dt == driver.ImageTypeOCI
    44  }
    45  
    46  // AddConfigurationOptions adds configuration callbacks to the driver
    47  func (d *Driver) AddConfigurationOptions(opts ...ConfigurationOption) {
    48  	d.dockerConfigurationOptions = append(d.dockerConfigurationOptions, opts...)
    49  }
    50  
    51  // Config returns the Docker driver configuration options
    52  func (d *Driver) Config() map[string]string {
    53  	return map[string]string{
    54  		"VERBOSE":             "Increase verbosity. true, false are supported values",
    55  		"PULL_ALWAYS":         "Always pull image, even if locally available (0|1)",
    56  		"DOCKER_DRIVER_QUIET": "Make the Docker driver quiet (only print container stdout/stderr)",
    57  		"OUTPUTS_MOUNT_PATH":  "Absolute path to where Docker driver can create temporary directories to bundle outputs. Defaults to temp dir.",
    58  		"CLEANUP_CONTAINERS":  "If true, the docker container will be destroyed when it finishes running. If false, it will not be destroyed. The supported values are true and false. Defaults to true.",
    59  	}
    60  }
    61  
    62  // SetConfig sets Docker driver configuration
    63  func (d *Driver) SetConfig(settings map[string]string) {
    64  	// Set default and provide feedback on acceptable input values.
    65  	value, ok := settings["CLEANUP_CONTAINERS"]
    66  	if !ok {
    67  		settings["CLEANUP_CONTAINERS"] = "true"
    68  	} else if value != "true" && value != "false" {
    69  		fmt.Printf("CLEANUP_CONTAINERS environment variable has unexpected value %q. Supported values are 'true', 'false', or unset.", value)
    70  	}
    71  
    72  	d.config = settings
    73  }
    74  
    75  // SetDockerCli makes the driver use an already initialized cli
    76  func (d *Driver) SetDockerCli(dockerCli command.Cli) {
    77  	d.dockerCli = dockerCli
    78  }
    79  
    80  // SetContainerOut sets the container output stream
    81  func (d *Driver) SetContainerOut(w io.Writer) {
    82  	d.containerOut = w
    83  }
    84  
    85  // SetContainerErr sets the container error stream
    86  func (d *Driver) SetContainerErr(w io.Writer) {
    87  	d.containerErr = w
    88  }
    89  
    90  func pullImage(ctx context.Context, cli command.Cli, image string) error {
    91  	ref, err := reference.ParseNormalizedNamed(image)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	// Resolve the Repository name from fqn to RepositoryInfo
    97  	repoInfo, err := registry.ParseRepositoryInfo(ref)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
   102  	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	options := types.ImagePullOptions{
   107  		RegistryAuth: encodedAuth,
   108  	}
   109  	responseBody, err := cli.Client().ImagePull(ctx, image, options)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	defer responseBody.Close()
   114  
   115  	// passing isTerm = false here because of https://github.com/Nvveen/Gotty/pull/1
   116  	return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.Out(), cli.Out().FD(), false, nil)
   117  }
   118  
   119  func (d *Driver) initializeDockerCli() (command.Cli, error) {
   120  	if d.dockerCli != nil {
   121  		return d.dockerCli, nil
   122  	}
   123  	cli, err := command.NewDockerCli()
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	if d.config["DOCKER_DRIVER_QUIET"] == "1" {
   128  		cli.Apply(command.WithCombinedStreams(ioutil.Discard))
   129  	}
   130  	if err := cli.Initialize(cliflags.NewClientOptions()); err != nil {
   131  		return nil, err
   132  	}
   133  	d.dockerCli = cli
   134  	return cli, nil
   135  }
   136  
   137  func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) {
   138  	ctx := context.Background()
   139  
   140  	cli, err := d.initializeDockerCli()
   141  	if err != nil {
   142  		return driver.OperationResult{}, err
   143  	}
   144  
   145  	if d.Simulate {
   146  		return driver.OperationResult{}, nil
   147  	}
   148  	if d.config["PULL_ALWAYS"] == "1" {
   149  		if err := pullImage(ctx, cli, op.Image.Image); err != nil {
   150  			return driver.OperationResult{}, err
   151  		}
   152  	}
   153  	var env []string
   154  	for k, v := range op.Environment {
   155  		env = append(env, fmt.Sprintf("%s=%v", k, v))
   156  	}
   157  
   158  	cfg := &container.Config{
   159  		Image:        op.Image.Image,
   160  		Env:          env,
   161  		Entrypoint:   strslice.StrSlice{"/cnab/app/run"},
   162  		AttachStderr: true,
   163  		AttachStdout: true,
   164  	}
   165  
   166  	hostCfg := &container.HostConfig{}
   167  	for _, opt := range d.dockerConfigurationOptions {
   168  		if err := opt(cfg, hostCfg); err != nil {
   169  			return driver.OperationResult{}, err
   170  		}
   171  	}
   172  
   173  	resp, err := cli.Client().ContainerCreate(ctx, cfg, hostCfg, nil, "")
   174  	switch {
   175  	case client.IsErrNotFound(err):
   176  		fmt.Fprintf(cli.Err(), "Unable to find image '%s' locally\n", op.Image.Image)
   177  		if err := pullImage(ctx, cli, op.Image.Image); err != nil {
   178  			return driver.OperationResult{}, err
   179  		}
   180  		if resp, err = cli.Client().ContainerCreate(ctx, cfg, hostCfg, nil, ""); err != nil {
   181  			return driver.OperationResult{}, fmt.Errorf("cannot create container: %v", err)
   182  		}
   183  	case err != nil:
   184  		return driver.OperationResult{}, fmt.Errorf("cannot create container: %v", err)
   185  	}
   186  
   187  	if d.config["CLEANUP_CONTAINERS"] == "true" {
   188  		defer cli.Client().ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{})
   189  	}
   190  
   191  	tarContent, err := generateTar(op.Files)
   192  	if err != nil {
   193  		return driver.OperationResult{}, fmt.Errorf("error staging files: %s", err)
   194  	}
   195  	options := types.CopyToContainerOptions{
   196  		AllowOverwriteDirWithFile: false,
   197  	}
   198  	// This copies the tar to the root of the container. The tar has been assembled using the
   199  	// path from the given file, starting at the /.
   200  	err = cli.Client().CopyToContainer(ctx, resp.ID, "/", tarContent, options)
   201  	if err != nil {
   202  		return driver.OperationResult{}, fmt.Errorf("error copying to / in container: %s", err)
   203  	}
   204  
   205  	attach, err := cli.Client().ContainerAttach(ctx, resp.ID, types.ContainerAttachOptions{
   206  		Stream: true,
   207  		Stdout: true,
   208  		Stderr: true,
   209  		Logs:   true,
   210  	})
   211  	if err != nil {
   212  		return driver.OperationResult{}, fmt.Errorf("unable to retrieve logs: %v", err)
   213  	}
   214  	var (
   215  		stdout io.Writer = os.Stdout
   216  		stderr io.Writer = os.Stderr
   217  	)
   218  	if d.containerOut != nil {
   219  		stdout = d.containerOut
   220  	}
   221  	if d.containerErr != nil {
   222  		stderr = d.containerErr
   223  	}
   224  	go func() {
   225  		defer attach.Close()
   226  		for {
   227  			_, err := stdcopy.StdCopy(stdout, stderr, attach.Reader)
   228  			if err != nil {
   229  				break
   230  			}
   231  		}
   232  	}()
   233  
   234  	statusc, errc := cli.Client().ContainerWait(ctx, resp.ID, container.WaitConditionNextExit)
   235  	if err = cli.Client().ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
   236  		return driver.OperationResult{}, fmt.Errorf("cannot start container: %v", err)
   237  	}
   238  	select {
   239  	case err := <-errc:
   240  		if err != nil {
   241  			opResult, fetchErr := d.fetchOutputs(ctx, resp.ID, op)
   242  			return opResult, containerError("error in container", err, fetchErr)
   243  		}
   244  	case s := <-statusc:
   245  		if s.StatusCode == 0 {
   246  			return d.fetchOutputs(ctx, resp.ID, op)
   247  		}
   248  		if s.Error != nil {
   249  			opResult, fetchErr := d.fetchOutputs(ctx, resp.ID, op)
   250  			return opResult, containerError(fmt.Sprintf("container exit code: %d, message", s.StatusCode), err, fetchErr)
   251  		}
   252  		opResult, fetchErr := d.fetchOutputs(ctx, resp.ID, op)
   253  		return opResult, containerError(fmt.Sprintf("container exit code: %d, message", s.StatusCode), err, fetchErr)
   254  	}
   255  	opResult, fetchErr := d.fetchOutputs(ctx, resp.ID, op)
   256  	if fetchErr != nil {
   257  		return opResult, fmt.Errorf("fetching outputs failed: %s", fetchErr)
   258  	}
   259  	return opResult, err
   260  }
   261  
   262  func containerError(containerMessage string, containerErr, fetchErr error) error {
   263  	if fetchErr != nil {
   264  		return fmt.Errorf("%s: %v. fetching outputs failed: %s", containerMessage, containerErr, fetchErr)
   265  	}
   266  
   267  	return fmt.Errorf("%s: %v", containerMessage, containerErr)
   268  }
   269  
   270  // fetchOutputs takes a context and a container ID; it copies the /cnab/app/outputs directory from that container.
   271  // The goal is to collect all the files in the directory (recursively) and put them in a flat map of path to contents.
   272  // This map will be inside the OperationResult. When fetchOutputs returns an error, it may also return partial results.
   273  func (d *Driver) fetchOutputs(ctx context.Context, container string, op *driver.Operation) (driver.OperationResult, error) {
   274  	opResult := driver.OperationResult{
   275  		Outputs: map[string]string{},
   276  	}
   277  	// The /cnab/app/outputs directory probably only exists if outputs are created. In the
   278  	// case there are no outputs defined on the operation, there probably are none to copy
   279  	// and we should return early.
   280  	if len(op.Outputs) == 0 {
   281  		return opResult, nil
   282  	}
   283  	ioReader, _, err := d.dockerCli.Client().CopyFromContainer(ctx, container, "/cnab/app/outputs")
   284  	if err != nil {
   285  		return opResult, fmt.Errorf("error copying outputs from container: %s", err)
   286  	}
   287  
   288  	tarReader := tar.NewReader(ioReader)
   289  	header, err := tarReader.Next()
   290  
   291  	// io.EOF pops us out of loop on successful run.
   292  	for err == nil {
   293  		// skip directories because we're gathering file contents
   294  		if header.FileInfo().IsDir() {
   295  			header, err = tarReader.Next()
   296  			continue
   297  		}
   298  
   299  		var contents []byte
   300  		// CopyFromContainer strips prefix above outputs directory.
   301  		pathInContainer := unix_path.Join("/cnab", "app", header.Name)
   302  
   303  		contents, err = ioutil.ReadAll(tarReader)
   304  		if err != nil {
   305  			return opResult, fmt.Errorf("error while reading %q from outputs tar: %s", pathInContainer, err)
   306  		}
   307  		opResult.Outputs[pathInContainer] = string(contents)
   308  		header, err = tarReader.Next()
   309  	}
   310  
   311  	if err != io.EOF {
   312  		return opResult, err
   313  	}
   314  
   315  	// if an applicable output is expected but does not exist and it has a
   316  	// non-empty default value, create an entry in the map with the
   317  	// default value as its contents
   318  	for name, output := range op.Bundle.Outputs {
   319  		filepath := unix_path.Join("/cnab", "app", "outputs", name)
   320  		if !existsInOutputsMap(opResult.Outputs, filepath) && output.AppliesTo(op.Action) {
   321  			if outputDefinition, exists := op.Bundle.Definitions[output.Definition]; exists {
   322  				outputDefault := outputDefinition.Default
   323  				if outputDefault != nil {
   324  					contents := fmt.Sprintf("%v", outputDefault)
   325  					opResult.Outputs[filepath] = contents
   326  				} else {
   327  					return opResult, fmt.Errorf("required output %s is missing and has no default", name)
   328  				}
   329  			}
   330  		}
   331  	}
   332  
   333  	return opResult, nil
   334  }
   335  
   336  func existsInOutputsMap(outputsMap map[string]string, path string) bool {
   337  	for outputPath := range outputsMap {
   338  		if outputPath == path {
   339  			return true
   340  		}
   341  	}
   342  	return false
   343  }
   344  
   345  func generateTar(files map[string]string) (io.Reader, error) {
   346  	r, w := io.Pipe()
   347  	tw := tar.NewWriter(w)
   348  	for path := range files {
   349  		if !unix_path.IsAbs(path) {
   350  			return nil, fmt.Errorf("destination path %s should be an absolute unix path", path)
   351  		}
   352  	}
   353  	go func() {
   354  		for path, content := range files {
   355  			hdr := &tar.Header{
   356  				Name: path,
   357  				Mode: 0644,
   358  				Size: int64(len(content)),
   359  			}
   360  			tw.WriteHeader(hdr)
   361  			tw.Write([]byte(content))
   362  		}
   363  		w.Close()
   364  	}()
   365  	return r, nil
   366  }
   367  
   368  // ConfigurationOption is an option used to customize docker driver container and host config
   369  type ConfigurationOption func(*container.Config, *container.HostConfig) error