github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/bom/executor/docker_executor.go (about)

     1  /*
     2   * Copyright (c) 2021 CodeNotary, Inc. All Rights Reserved.
     3   * This software is released under GPL3.
     4   * The full license information can be found under:
     5   * https://www.gnu.org/licenses/gpl-3.0.en.html
     6   *
     7   */
     8  
     9  package executor
    10  
    11  import (
    12  	"archive/tar"
    13  	"bytes"
    14  	"context"
    15  	"fmt"
    16  	"io"
    17  	"io/ioutil"
    18  	"time"
    19  
    20  	"github.com/docker/docker/api/types"
    21  	"github.com/docker/docker/api/types/container"
    22  	docker "github.com/docker/docker/client"
    23  	"github.com/docker/docker/errdefs"
    24  	"github.com/docker/docker/pkg/stdcopy"
    25  )
    26  
    27  // timeout for kill ing the container if it doesn't properly shuts down
    28  var timeout = 5 * time.Second
    29  
    30  type DockerExecutor struct {
    31  	ctx    context.Context
    32  	client *docker.Client
    33  	contID string
    34  }
    35  
    36  // NewDockerExecutor starts a new container and returns an executor for the container
    37  func NewDockerExecutor(image string) (Executor, error) {
    38  	ctx := context.Background()
    39  
    40  	dockerClient, _ := docker.NewClientWithOpts(docker.FromEnv, docker.WithAPIVersionNegotiation())
    41  	cont, err := dockerClient.ContainerCreate(
    42  		ctx,
    43  		&container.Config{
    44  			Image:       image,
    45  			Entrypoint:  []string{"/bin/sh"},
    46  			AttachStdin: true,
    47  			Tty:         true,
    48  			OpenStdin:   true,
    49  		},
    50  		nil, nil, nil, "")
    51  	if err != nil {
    52  		return nil, fmt.Errorf("cannot create container: %w", err)
    53  	}
    54  
    55  	err = dockerClient.ContainerStart(ctx, cont.ID, types.ContainerStartOptions{})
    56  	if err != nil {
    57  		if errdefs.IsInvalidParameter(err) {
    58  			return nil, fmt.Errorf("cannot use container without shell. Images 'from scratch' are not supported")
    59  		}
    60  		return nil, err
    61  	}
    62  
    63  	return DockerExecutor{
    64  		ctx:    ctx,
    65  		client: dockerClient,
    66  		contID: cont.ID,
    67  	}, nil
    68  }
    69  
    70  // Exec executes a command inside the container
    71  func (e DockerExecutor) Exec(cmd []string) ([]byte, []byte, int, error) {
    72  	var stdOut, stdErr bytes.Buffer
    73  	exec, err := e.client.ContainerExecCreate(e.ctx, e.contID, types.ExecConfig{
    74  		Cmd:          cmd,
    75  		AttachStdout: true,
    76  		AttachStderr: true,
    77  	})
    78  	if err != nil {
    79  		return nil, nil, 0, err
    80  	}
    81  
    82  	hijacked, err := e.client.ContainerExecAttach(e.ctx, exec.ID, types.ExecStartCheck{})
    83  	if err != nil {
    84  		return nil, nil, 0, err
    85  	}
    86  	defer hijacked.Close()
    87  
    88  	done := make(chan error)
    89  
    90  	go func() {
    91  		_, err := stdcopy.StdCopy(&stdOut, &stdErr, hijacked.Reader)
    92  		done <- err
    93  	}()
    94  
    95  	select {
    96  	case err = <-done:
    97  		if err != nil {
    98  			return nil, nil, 0, err
    99  		}
   100  		break
   101  	case <-e.ctx.Done():
   102  		return nil, nil, 0, e.ctx.Err()
   103  	}
   104  
   105  	// check exit code
   106  	res, err := e.client.ContainerExecInspect(e.ctx, exec.ID)
   107  	if err != nil {
   108  		return nil, nil, 0, err
   109  	}
   110  
   111  	return stdOut.Bytes(), stdErr.Bytes(), res.ExitCode, nil
   112  }
   113  
   114  // Close stops previously started container by sending "exit" to shell - it is much faster than
   115  // stopping with container API
   116  func (e DockerExecutor) Close() error {
   117  	hijacked, err := e.client.ContainerAttach(e.ctx, e.contID, types.ContainerAttachOptions{
   118  		Stdin:  true,
   119  		Stream: true,
   120  	})
   121  	if err != nil {
   122  		// force stop
   123  		fmt.Printf("cannot attach to container: %s", err)
   124  		return e.client.ContainerStop(e.ctx, e.contID, &timeout)
   125  	}
   126  	_, err = hijacked.Conn.Write([]byte("exit\n"))
   127  	if err != nil {
   128  		// force stop
   129  		fmt.Printf("cannot send command to container: %s", err)
   130  		return e.client.ContainerStop(e.ctx, e.contID, &timeout)
   131  	}
   132  	hijacked.Conn.Close()
   133  	doneCh, errCh := e.client.ContainerWait(e.ctx, e.contID, container.WaitConditionNotRunning)
   134  	select {
   135  	case err = <-errCh:
   136  		return err
   137  	case <-doneCh:
   138  		return nil
   139  	}
   140  }
   141  
   142  // Read reads the file from container and returns its content
   143  func (e DockerExecutor) ReadFile(path string) ([]byte, error) {
   144  	reader, _, err := e.client.CopyFromContainer(e.ctx, e.contID, path)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	defer reader.Close()
   149  
   150  	tr := tar.NewReader(reader)
   151  	_, err = tr.Next()
   152  	if err != nil {
   153  		return nil, fmt.Errorf("error processing file from container: %w", err)
   154  	}
   155  
   156  	return ioutil.ReadAll(tr)
   157  }
   158  
   159  // ReadDir reads the files from container directory and returns the content as TAR stream
   160  func (e DockerExecutor) ReadDir(path string) (io.ReadCloser, error) {
   161  	reader, _, err := e.client.CopyFromContainer(e.ctx, e.contID, path)
   162  	return reader, err
   163  }