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