github.imxd.top/openshift/source-to-image@v1.2.0/pkg/docker/docker.go (about) 1 package docker 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "encoding/base64" 7 "encoding/json" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "math/rand" 12 "net/http" 13 "os" 14 "path" 15 "path/filepath" 16 "runtime" 17 "strings" 18 "syscall" 19 "time" 20 21 dockertypes "github.com/docker/docker/api/types" 22 dockercontainer "github.com/docker/docker/api/types/container" 23 dockernetwork "github.com/docker/docker/api/types/network" 24 dockerapi "github.com/docker/docker/client" 25 dockermessage "github.com/docker/docker/pkg/jsonmessage" 26 dockerstdcopy "github.com/docker/docker/pkg/stdcopy" 27 "github.com/docker/go-connections/tlsconfig" 28 "golang.org/x/net/context" 29 30 "github.com/openshift/source-to-image/pkg/api" 31 "github.com/openshift/source-to-image/pkg/api/constants" 32 s2ierr "github.com/openshift/source-to-image/pkg/errors" 33 s2itar "github.com/openshift/source-to-image/pkg/tar" 34 "github.com/openshift/source-to-image/pkg/util" 35 "github.com/openshift/source-to-image/pkg/util/fs" 36 "github.com/openshift/source-to-image/pkg/util/interrupt" 37 ) 38 39 const ( 40 // DefaultDestination is the destination where the artifacts will be placed 41 // if DestinationLabel was not specified. 42 DefaultDestination = "/tmp" 43 // DefaultTag is the image tag, being applied if none is specified. 44 DefaultTag = "latest" 45 46 // DefaultDockerTimeout specifies a timeout for Docker API calls. When this 47 // timeout is reached, certain Docker API calls might error out. 48 DefaultDockerTimeout = 2 * time.Minute 49 50 // DefaultShmSize is the default shared memory size to use (in bytes) if not specified. 51 DefaultShmSize = int64(1024 * 1024 * 64) 52 // DefaultPullRetryDelay is the default pull image retry interval 53 DefaultPullRetryDelay = 5 * time.Second 54 // DefaultPullRetryCount is the default pull image retry times 55 DefaultPullRetryCount = 6 56 ) 57 58 var ( 59 // RetriableErrors is a set of strings that indicate that an retriable error occurred. 60 RetriableErrors = []string{ 61 "ping attempt failed with error", 62 "is already in progress", 63 "connection reset by peer", 64 "transport closed before response was received", 65 "connection refused", 66 } 67 ) 68 69 // containerNamePrefix prefixes the name of containers launched by S2I. We 70 // cannot reuse the prefix "k8s" because we don't want the containers to be 71 // managed by a kubelet. 72 const containerNamePrefix = "s2i" 73 74 // containerName creates names for Docker containers launched by S2I. It is 75 // meant to resemble Kubernetes' pkg/kubelet/dockertools.BuildDockerName. 76 func containerName(image string) string { 77 //Initialize seed 78 rand.Seed(time.Now().UnixNano()) 79 uid := fmt.Sprintf("%08x", rand.Uint32()) 80 // Replace invalid characters for container name with underscores. 81 image = strings.Map(func(r rune) rune { 82 if ('0' <= r && r <= '9') || ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') { 83 return r 84 } 85 return '_' 86 }, image) 87 return fmt.Sprintf("%s_%s_%s", containerNamePrefix, image, uid) 88 } 89 90 // Docker is the interface between STI and the docker engine-api. 91 // It contains higher level operations called from the STI 92 // build or usage commands 93 type Docker interface { 94 IsImageInLocalRegistry(name string) (bool, error) 95 IsImageOnBuild(string) bool 96 GetOnBuild(string) ([]string, error) 97 RemoveContainer(id string) error 98 GetScriptsURL(name string) (string, error) 99 GetAssembleInputFiles(string) (string, error) 100 GetAssembleRuntimeUser(string) (string, error) 101 RunContainer(opts RunContainerOptions) error 102 GetImageID(name string) (string, error) 103 GetImageWorkdir(name string) (string, error) 104 CommitContainer(opts CommitContainerOptions) (string, error) 105 RemoveImage(name string) error 106 CheckImage(name string) (*api.Image, error) 107 PullImage(name string) (*api.Image, error) 108 CheckAndPullImage(name string) (*api.Image, error) 109 BuildImage(opts BuildImageOptions) error 110 GetImageUser(name string) (string, error) 111 GetImageEntrypoint(name string) ([]string, error) 112 GetLabels(name string) (map[string]string, error) 113 UploadToContainer(fs fs.FileSystem, srcPath, destPath, container string) error 114 UploadToContainerWithTarWriter(fs fs.FileSystem, srcPath, destPath, container string, makeTarWriter func(io.Writer) s2itar.Writer) error 115 DownloadFromContainer(containerPath string, w io.Writer, container string) error 116 Version() (dockertypes.Version, error) 117 CheckReachable() error 118 } 119 120 // Client contains all methods used when interacting directly with docker engine-api 121 type Client interface { 122 ContainerAttach(ctx context.Context, container string, options dockertypes.ContainerAttachOptions) (dockertypes.HijackedResponse, error) 123 ContainerCommit(ctx context.Context, container string, options dockertypes.ContainerCommitOptions) (dockertypes.IDResponse, error) 124 ContainerCreate(ctx context.Context, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, networkingConfig *dockernetwork.NetworkingConfig, containerName string) (dockercontainer.ContainerCreateCreatedBody, error) 125 ContainerInspect(ctx context.Context, container string) (dockertypes.ContainerJSON, error) 126 ContainerRemove(ctx context.Context, container string, options dockertypes.ContainerRemoveOptions) error 127 ContainerStart(ctx context.Context, container string, options dockertypes.ContainerStartOptions) error 128 ContainerKill(ctx context.Context, container, signal string) error 129 ContainerWait(ctx context.Context, container string, condition dockercontainer.WaitCondition) (<-chan dockercontainer.ContainerWaitOKBody, <-chan error) 130 CopyToContainer(ctx context.Context, container, path string, content io.Reader, opts dockertypes.CopyToContainerOptions) error 131 CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, dockertypes.ContainerPathStat, error) 132 ImageBuild(ctx context.Context, buildContext io.Reader, options dockertypes.ImageBuildOptions) (dockertypes.ImageBuildResponse, error) 133 ImageInspectWithRaw(ctx context.Context, image string) (dockertypes.ImageInspect, []byte, error) 134 ImagePull(ctx context.Context, ref string, options dockertypes.ImagePullOptions) (io.ReadCloser, error) 135 ImageRemove(ctx context.Context, image string, options dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error) 136 ServerVersion(ctx context.Context) (dockertypes.Version, error) 137 } 138 139 type stiDocker struct { 140 client Client 141 pullAuth dockertypes.AuthConfig 142 } 143 144 // InspectImage returns the image information and its raw representation. 145 func (d stiDocker) InspectImage(name string) (*dockertypes.ImageInspect, error) { 146 ctx, cancel := getDefaultContext() 147 defer cancel() 148 resp, _, err := d.client.ImageInspectWithRaw(ctx, name) 149 if err != nil { 150 return nil, err 151 } 152 return &resp, nil 153 } 154 155 // PostExecutor is an interface which provides a PostExecute function 156 type PostExecutor interface { 157 PostExecute(containerID, destination string) error 158 } 159 160 // PullResult is the result returned by the PullImage function 161 type PullResult struct { 162 OnBuild bool 163 Image *api.Image 164 } 165 166 // RunContainerOptions are options passed in to the RunContainer method 167 type RunContainerOptions struct { 168 Image string 169 PullImage bool 170 PullAuth api.AuthConfig 171 ExternalScripts bool 172 ScriptsURL string 173 Destination string 174 Env []string 175 AddHost []string 176 // Entrypoint will be used to override the default entrypoint 177 // for the image if it has one. If the image has no entrypoint, 178 // this value is ignored. 179 Entrypoint []string 180 Stdin io.ReadCloser 181 Stdout io.WriteCloser 182 Stderr io.WriteCloser 183 OnStart func(containerID string) error 184 PostExec PostExecutor 185 TargetImage bool 186 NetworkMode string 187 User string 188 CGroupLimits *api.CGroupLimits 189 CapDrop []string 190 Binds []string 191 Command string 192 CommandOverrides func(originalCmd string) string 193 // CommandExplicit provides a full control on the CMD directive. 194 // It won't modified in any way and will be passed to the docker as-is. 195 // Use this option when you want to use arbitrary command as CMD directive. 196 // In this case you can't use Command because 1) it's just a string 197 // 2) it will be modified by prepending base dir and cleaned by the path.Join(). 198 // You also can't use CommandOverrides because 1) it's a string 199 // 2) it only gets applied when Command equals to "assemble" or "usage" script 200 // AND script is inside of the tar archive. 201 CommandExplicit []string 202 // SecurityOpt is passed through as security options to the underlying container. 203 SecurityOpt []string 204 } 205 206 // asDockerConfig converts a RunContainerOptions into a Config understood by the 207 // docker client 208 func (rco RunContainerOptions) asDockerConfig() dockercontainer.Config { 209 return dockercontainer.Config{ 210 Image: getImageName(rco.Image), 211 User: rco.User, 212 Env: rco.Env, 213 Entrypoint: rco.Entrypoint, 214 OpenStdin: rco.Stdin != nil, 215 StdinOnce: rco.Stdin != nil, 216 AttachStdout: rco.Stdout != nil, 217 } 218 } 219 220 // asDockerHostConfig converts a RunContainerOptions into a HostConfig 221 // understood by the docker client 222 func (rco RunContainerOptions) asDockerHostConfig() dockercontainer.HostConfig { 223 hostConfig := dockercontainer.HostConfig{ 224 CapDrop: rco.CapDrop, 225 PublishAllPorts: rco.TargetImage, 226 NetworkMode: dockercontainer.NetworkMode(rco.NetworkMode), 227 Binds: rco.Binds, 228 ExtraHosts: rco.AddHost, 229 SecurityOpt: rco.SecurityOpt, 230 } 231 if rco.CGroupLimits != nil { 232 hostConfig.Resources.Memory = rco.CGroupLimits.MemoryLimitBytes 233 hostConfig.Resources.MemorySwap = rco.CGroupLimits.MemorySwap 234 hostConfig.Resources.CgroupParent = rco.CGroupLimits.Parent 235 } 236 return hostConfig 237 } 238 239 // asDockerCreateContainerOptions converts a RunContainerOptions into a 240 // ContainerCreateConfig understood by the docker client 241 func (rco RunContainerOptions) asDockerCreateContainerOptions() dockertypes.ContainerCreateConfig { 242 config := rco.asDockerConfig() 243 hostConfig := rco.asDockerHostConfig() 244 return dockertypes.ContainerCreateConfig{ 245 Name: containerName(rco.Image), 246 Config: &config, 247 HostConfig: &hostConfig, 248 } 249 } 250 251 // asDockerAttachToContainerOptions converts a RunContainerOptions into a 252 // ContainerAttachOptions understood by the docker client 253 func (rco RunContainerOptions) asDockerAttachToContainerOptions() dockertypes.ContainerAttachOptions { 254 return dockertypes.ContainerAttachOptions{ 255 Stdin: rco.Stdin != nil, 256 Stdout: rco.Stdout != nil, 257 Stderr: rco.Stderr != nil, 258 Stream: rco.Stdout != nil, 259 } 260 } 261 262 // CommitContainerOptions are options passed in to the CommitContainer method 263 type CommitContainerOptions struct { 264 ContainerID string 265 Repository string 266 User string 267 Command []string 268 Env []string 269 Entrypoint []string 270 Labels map[string]string 271 } 272 273 // BuildImageOptions are options passed in to the BuildImage method 274 type BuildImageOptions struct { 275 Name string 276 Stdin io.Reader 277 Stdout io.WriteCloser 278 CGroupLimits *api.CGroupLimits 279 } 280 281 // NewEngineAPIClient creates a new Docker engine API client 282 func NewEngineAPIClient(config *api.DockerConfig) (*dockerapi.Client, error) { 283 var httpClient *http.Client 284 285 if config.UseTLS || config.TLSVerify { 286 tlscOptions := tlsconfig.Options{ 287 InsecureSkipVerify: !config.TLSVerify, 288 } 289 290 if _, err := os.Stat(config.CAFile); !os.IsNotExist(err) { 291 tlscOptions.CAFile = config.CAFile 292 } 293 if _, err := os.Stat(config.CertFile); !os.IsNotExist(err) { 294 tlscOptions.CertFile = config.CertFile 295 } 296 if _, err := os.Stat(config.KeyFile); !os.IsNotExist(err) { 297 tlscOptions.KeyFile = config.KeyFile 298 } 299 300 tlsc, err := tlsconfig.Client(tlscOptions) 301 if err != nil { 302 return nil, err 303 } 304 305 httpClient = &http.Client{ 306 Transport: &http.Transport{ 307 TLSClientConfig: tlsc, 308 }, 309 } 310 } 311 return dockerapi.NewClient(config.Endpoint, os.Getenv("DOCKER_API_VERSION"), httpClient, nil) 312 } 313 314 // New creates a new implementation of the STI Docker interface 315 func New(client Client, auth api.AuthConfig) Docker { 316 return &stiDocker{ 317 client: client, 318 pullAuth: dockertypes.AuthConfig{ 319 Username: auth.Username, 320 Password: auth.Password, 321 Email: auth.Email, 322 ServerAddress: auth.ServerAddress, 323 }, 324 } 325 } 326 327 func getDefaultContext() (context.Context, context.CancelFunc) { 328 // the intention is: all docker API calls with the exception of known long- 329 // running calls (ContainerWait, ImagePull, ImageBuild, ImageCommit) must complete within a 330 // certain timeout otherwise we bail. 331 return context.WithTimeout(context.Background(), DefaultDockerTimeout) 332 } 333 334 // GetImageWorkdir returns the WORKDIR property for the given image name. 335 // When the WORKDIR is not set or empty, return "/" instead. 336 func (d *stiDocker) GetImageWorkdir(name string) (string, error) { 337 resp, err := d.InspectImage(name) 338 if err != nil { 339 return "", err 340 } 341 workdir := resp.Config.WorkingDir 342 if len(workdir) == 0 { 343 // This is a default destination used by UploadToContainer when the WORKDIR 344 // is not set or it is empty. To show user where the injections will end up, 345 // we set this to "/". 346 workdir = "/" 347 } 348 return workdir, nil 349 } 350 351 // GetImageEntrypoint returns the ENTRYPOINT property for the given image name. 352 func (d *stiDocker) GetImageEntrypoint(name string) ([]string, error) { 353 image, err := d.InspectImage(name) 354 if err != nil { 355 return nil, err 356 } 357 return image.Config.Entrypoint, nil 358 } 359 360 // UploadToContainer uploads artifacts to the container. 361 func (d *stiDocker) UploadToContainer(fs fs.FileSystem, src, dest, container string) error { 362 makeWorldWritable := func(writer io.Writer) s2itar.Writer { 363 return s2itar.ChmodAdapter{Writer: tar.NewWriter(writer), NewFileMode: 0666, NewExecFileMode: 0666, NewDirMode: 0777} 364 } 365 366 return d.UploadToContainerWithTarWriter(fs, src, dest, container, makeWorldWritable) 367 } 368 369 // UploadToContainerWithTarWriter uploads artifacts to the container. 370 // If the source is a directory, then all files and sub-folders are copied into 371 // the destination (which has to be directory as well). 372 // If the source is a single file, then the file copied into destination (which 373 // has to be full path to a file inside the container). 374 func (d *stiDocker) UploadToContainerWithTarWriter(fs fs.FileSystem, src, dest, container string, makeTarWriter func(io.Writer) s2itar.Writer) error { 375 destPath := filepath.Dir(dest) 376 r, w := io.Pipe() 377 go func() { 378 tarWriter := makeTarWriter(w) 379 tarWriter = s2itar.RenameAdapter{Writer: tarWriter, Old: filepath.Base(src), New: filepath.Base(dest)} 380 381 err := s2itar.New(fs).CreateTarStreamToTarWriter(src, true, tarWriter, nil) 382 if err == nil { 383 err = tarWriter.Close() 384 } 385 386 w.CloseWithError(err) 387 }() 388 log.V(3).Infof("Uploading %q to %q ...", src, destPath) 389 ctx, cancel := getDefaultContext() 390 defer cancel() 391 err := d.client.CopyToContainer(ctx, container, destPath, r, dockertypes.CopyToContainerOptions{}) 392 if err != nil { 393 log.V(0).Infof("error: Uploading to container failed: %v", err) 394 } 395 return err 396 } 397 398 // DownloadFromContainer downloads file (or directory) from the container. 399 func (d *stiDocker) DownloadFromContainer(containerPath string, w io.Writer, container string) error { 400 ctx, cancel := getDefaultContext() 401 defer cancel() 402 readCloser, _, err := d.client.CopyFromContainer(ctx, container, containerPath) 403 if err != nil { 404 return err 405 } 406 defer readCloser.Close() 407 _, err = io.Copy(w, readCloser) 408 return err 409 } 410 411 // IsImageInLocalRegistry determines whether the supplied image is in the local registry. 412 func (d *stiDocker) IsImageInLocalRegistry(name string) (bool, error) { 413 name = getImageName(name) 414 resp, err := d.InspectImage(name) 415 if resp != nil { 416 return true, nil 417 } 418 if err != nil && !dockerapi.IsErrNotFound(err) { 419 return false, s2ierr.NewInspectImageError(name, err) 420 } 421 return false, nil 422 } 423 424 // GetImageUser finds and retrieves the user associated with 425 // an image if one has been specified 426 func (d *stiDocker) GetImageUser(name string) (string, error) { 427 name = getImageName(name) 428 resp, err := d.InspectImage(name) 429 if err != nil { 430 log.V(4).Infof("error inspecting image %s: %v", name, err) 431 return "", s2ierr.NewInspectImageError(name, err) 432 } 433 user := resp.Config.User 434 return user, nil 435 } 436 437 // Version returns information of the docker client and server host 438 func (d *stiDocker) Version() (dockertypes.Version, error) { 439 ctx, cancel := getDefaultContext() 440 defer cancel() 441 return d.client.ServerVersion(ctx) 442 } 443 444 // IsImageOnBuild provides information about whether the Docker image has 445 // OnBuild instruction recorded in the Image Config. 446 func (d *stiDocker) IsImageOnBuild(name string) bool { 447 onbuild, err := d.GetOnBuild(name) 448 return err == nil && len(onbuild) > 0 449 } 450 451 // GetOnBuild returns the set of ONBUILD Dockerfile commands to execute 452 // for the given image 453 func (d *stiDocker) GetOnBuild(name string) ([]string, error) { 454 name = getImageName(name) 455 resp, err := d.InspectImage(name) 456 if err != nil { 457 log.V(4).Infof("error inspecting image %s: %v", name, err) 458 return nil, s2ierr.NewInspectImageError(name, err) 459 } 460 return resp.Config.OnBuild, nil 461 } 462 463 // CheckAndPullImage pulls an image into the local registry if not present 464 // and returns the image metadata 465 func (d *stiDocker) CheckAndPullImage(name string) (*api.Image, error) { 466 name = getImageName(name) 467 displayName := name 468 469 if !log.Is(3) { 470 // For less verbose log levels (less than 3), shorten long iamge names like: 471 // "centos/php-56-centos7@sha256:51c3e2b08bd9fadefccd6ec42288680d6d7f861bdbfbd2d8d24960621e4e27f5" 472 // to include just enough characters to differentiate the build from others in the docker repository: 473 // "centos/php-56-centos7@sha256:51c3e2b08bd..." 474 // 18 characters is somewhat arbitrary, but should be enough to avoid a name collision. 475 split := strings.Split(name, "@") 476 if len(split) > 1 && len(split[1]) > 18 { 477 displayName = split[0] + "@" + split[1][:18] + "..." 478 } 479 } 480 481 image, err := d.CheckImage(name) 482 if err != nil && !strings.Contains(err.(s2ierr.Error).Details.Error(), "No such image") { 483 return nil, err 484 } 485 if image == nil { 486 log.V(1).Infof("Image %q not available locally, pulling ...", displayName) 487 return d.PullImage(name) 488 } 489 490 log.V(3).Infof("Using locally available image %q", displayName) 491 return image, nil 492 } 493 494 // CheckImage checks image from the local registry. 495 func (d *stiDocker) CheckImage(name string) (*api.Image, error) { 496 name = getImageName(name) 497 inspect, err := d.InspectImage(name) 498 if err != nil { 499 log.V(4).Infof("error inspecting image %s: %v", name, err) 500 return nil, s2ierr.NewInspectImageError(name, err) 501 } 502 if inspect != nil { 503 image := &api.Image{} 504 updateImageWithInspect(image, inspect) 505 return image, nil 506 } 507 508 return nil, nil 509 } 510 511 func base64EncodeAuth(auth dockertypes.AuthConfig) (string, error) { 512 var buf bytes.Buffer 513 if err := json.NewEncoder(&buf).Encode(auth); err != nil { 514 return "", err 515 } 516 return base64.URLEncoding.EncodeToString(buf.Bytes()), nil 517 } 518 519 // PullImage pulls an image into the local registry 520 func (d *stiDocker) PullImage(name string) (*api.Image, error) { 521 name = getImageName(name) 522 523 // RegistryAuth is the base64 encoded credentials for the registry 524 base64Auth, err := base64EncodeAuth(d.pullAuth) 525 if err != nil { 526 return nil, s2ierr.NewPullImageError(name, err) 527 } 528 var retriableError = false 529 530 for retries := 0; retries <= DefaultPullRetryCount; retries++ { 531 err = util.TimeoutAfter(DefaultDockerTimeout, fmt.Sprintf("pulling image %q", name), func(timer *time.Timer) error { 532 resp, pullErr := d.client.ImagePull(context.Background(), name, dockertypes.ImagePullOptions{RegistryAuth: base64Auth}) 533 if pullErr != nil { 534 return pullErr 535 } 536 defer resp.Close() 537 538 decoder := json.NewDecoder(resp) 539 for { 540 if !timer.Stop() { 541 return &util.TimeoutError{} 542 } 543 timer.Reset(DefaultDockerTimeout) 544 545 var msg dockermessage.JSONMessage 546 pullErr = decoder.Decode(&msg) 547 if pullErr == io.EOF { 548 return nil 549 } 550 if pullErr != nil { 551 return pullErr 552 } 553 554 if msg.Error != nil { 555 return msg.Error 556 } 557 if msg.Progress != nil { 558 log.V(4).Infof("pulling image %s: %s", name, msg.Progress.String()) 559 } 560 } 561 }) 562 if err == nil { 563 break 564 } 565 log.V(0).Infof("pulling image error : %v", err) 566 errMsg := fmt.Sprintf("%s", err) 567 for _, errorString := range RetriableErrors { 568 if strings.Contains(errMsg, errorString) { 569 retriableError = true 570 break 571 } 572 } 573 574 if !retriableError { 575 return nil, s2ierr.NewPullImageError(name, err) 576 } 577 578 log.V(0).Infof("retrying in %s ...", DefaultPullRetryDelay) 579 time.Sleep(DefaultPullRetryDelay) 580 } 581 582 inspectResp, err := d.InspectImage(name) 583 if err != nil { 584 return nil, s2ierr.NewPullImageError(name, err) 585 } 586 if inspectResp != nil { 587 image := &api.Image{} 588 updateImageWithInspect(image, inspectResp) 589 return image, nil 590 } 591 return nil, nil 592 } 593 594 func updateImageWithInspect(image *api.Image, inspect *dockertypes.ImageInspect) { 595 image.ID = inspect.ID 596 if inspect.Config != nil { 597 image.Config = &api.ContainerConfig{ 598 Labels: inspect.Config.Labels, 599 Env: inspect.Config.Env, 600 } 601 } 602 if inspect.ContainerConfig != nil { 603 image.ContainerConfig = &api.ContainerConfig{ 604 Labels: inspect.ContainerConfig.Labels, 605 Env: inspect.ContainerConfig.Env, 606 } 607 } 608 } 609 610 // RemoveContainer removes a container and its associated volumes. 611 func (d *stiDocker) RemoveContainer(id string) error { 612 ctx, cancel := getDefaultContext() 613 defer cancel() 614 opts := dockertypes.ContainerRemoveOptions{ 615 RemoveVolumes: true, 616 } 617 return d.client.ContainerRemove(ctx, id, opts) 618 } 619 620 // KillContainer kills a container. 621 func (d *stiDocker) KillContainer(id string) error { 622 ctx, cancel := getDefaultContext() 623 defer cancel() 624 return d.client.ContainerKill(ctx, id, "SIGKILL") 625 } 626 627 // GetLabels retrieves the labels of the given image. 628 func (d *stiDocker) GetLabels(name string) (map[string]string, error) { 629 name = getImageName(name) 630 resp, err := d.InspectImage(name) 631 if err != nil { 632 log.V(4).Infof("error inspecting image %s: %v", name, err) 633 return nil, s2ierr.NewInspectImageError(name, err) 634 } 635 return resp.Config.Labels, nil 636 } 637 638 // getImageName checks the image name and adds DefaultTag if none is specified 639 func getImageName(name string) string { 640 _, tag, id := parseRepositoryTag(name) 641 if len(tag) == 0 && len(id) == 0 { 642 //_, tag, _ := parseRepositoryTag(name) 643 //if len(tag) == 0 { 644 return strings.Join([]string{name, DefaultTag}, ":") 645 } 646 647 return name 648 } 649 650 // getLabel gets label's value from the image metadata 651 func getLabel(image *api.Image, name string) string { 652 if value, ok := image.Config.Labels[name]; ok { 653 return value 654 } 655 return "" 656 } 657 658 // getVariable gets environment variable's value from the image metadata 659 func getVariable(image *api.Image, name string) string { 660 envName := name + "=" 661 for _, v := range image.Config.Env { 662 if strings.HasPrefix(v, envName) { 663 return strings.TrimSpace(v[len(envName):]) 664 } 665 } 666 667 return "" 668 } 669 670 // GetScriptsURL finds a scripts-url label on the given image. 671 func (d *stiDocker) GetScriptsURL(image string) (string, error) { 672 imageMetadata, err := d.CheckAndPullImage(image) 673 if err != nil { 674 return "", err 675 } 676 677 return getScriptsURL(imageMetadata), nil 678 } 679 680 // GetAssembleInputFiles finds a io.openshift.s2i.assemble-input-files label on the given image. 681 func (d *stiDocker) GetAssembleInputFiles(image string) (string, error) { 682 imageMetadata, err := d.CheckAndPullImage(image) 683 if err != nil { 684 return "", err 685 } 686 687 label := getLabel(imageMetadata, constants.AssembleInputFilesLabel) 688 if len(label) == 0 { 689 log.V(0).Infof("warning: Image %q does not contain a value for the %s label", image, constants.AssembleInputFilesLabel) 690 } else { 691 log.V(3).Infof("Image %q contains %s set to %q", image, constants.AssembleInputFilesLabel, label) 692 } 693 return label, nil 694 } 695 696 // GetAssembleRuntimeUser finds a io.openshift.s2i.assemble-runtime-user label on the given image. 697 func (d *stiDocker) GetAssembleRuntimeUser(image string) (string, error) { 698 imageMetadata, err := d.CheckAndPullImage(image) 699 if err != nil { 700 return "", err 701 } 702 return getLabel(imageMetadata, constants.AssembleRuntimeUserLabel), nil 703 } 704 705 // getScriptsURL finds a scripts url label in the image metadata 706 func getScriptsURL(image *api.Image) string { 707 if image == nil { 708 return "" 709 } 710 scriptsURL := getLabel(image, constants.ScriptsURLLabel) 711 712 // For backward compatibility, support the old label schema 713 if len(scriptsURL) == 0 { 714 scriptsURL = getLabel(image, constants.DeprecatedScriptsURLLabel) 715 if len(scriptsURL) > 0 { 716 log.V(0).Infof("warning: Image %s uses deprecated label '%s', please migrate it to %s instead!", 717 image.ID, constants.DeprecatedScriptsURLLabel, constants.ScriptsURLLabel) 718 } 719 } 720 if len(scriptsURL) == 0 { 721 scriptsURL = getVariable(image, constants.ScriptsURLEnvironment) 722 if len(scriptsURL) != 0 { 723 log.V(0).Infof("warning: Image %s uses deprecated environment variable %s, please migrate it to %s label instead!", 724 image.ID, constants.ScriptsURLEnvironment, constants.ScriptsURLLabel) 725 } 726 } 727 if len(scriptsURL) == 0 { 728 log.V(0).Infof("warning: Image %s does not contain a value for the %s label", image.ID, constants.ScriptsURLLabel) 729 } else { 730 log.V(2).Infof("Image %s contains %s set to %q", image.ID, constants.ScriptsURLLabel, scriptsURL) 731 } 732 733 return scriptsURL 734 } 735 736 // getDestination finds a destination label in the image metadata 737 func getDestination(image *api.Image) string { 738 if val := getLabel(image, constants.DestinationLabel); len(val) != 0 { 739 return val 740 } 741 // For backward compatibility, support the old label schema 742 if val := getLabel(image, constants.DeprecatedDestinationLabel); len(val) != 0 { 743 log.V(0).Infof("warning: Image %s uses deprecated label '%s', please migrate it to %s instead!", 744 image.ID, constants.DeprecatedDestinationLabel, constants.DestinationLabel) 745 return val 746 } 747 if val := getVariable(image, constants.LocationEnvironment); len(val) != 0 { 748 log.V(0).Infof("warning: Image %s uses deprecated environment variable %s, please migrate it to %s label instead!", 749 image.ID, constants.LocationEnvironment, constants.DestinationLabel) 750 return val 751 } 752 753 // default directory if none is specified 754 return DefaultDestination 755 } 756 757 func constructCommand(opts RunContainerOptions, imageMetadata *api.Image, tarDestination string) []string { 758 // base directory for all S2I commands 759 commandBaseDir := determineCommandBaseDir(opts, imageMetadata, tarDestination) 760 761 // NOTE: We use path.Join instead of filepath.Join to avoid converting the 762 // path to UNC (Windows) format as we always run this inside container. 763 binaryToRun := path.Join(commandBaseDir, opts.Command) 764 765 // when calling assemble script with Stdin parameter set (the tar file) 766 // we need to first untar the whole archive and only then call the assemble script 767 if opts.Stdin != nil && (opts.Command == constants.Assemble || opts.Command == constants.Usage) { 768 untarAndRun := fmt.Sprintf("tar -C %s -xf - && %s", tarDestination, binaryToRun) 769 770 resultedCommand := untarAndRun 771 if opts.CommandOverrides != nil { 772 resultedCommand = opts.CommandOverrides(untarAndRun) 773 } 774 return []string{"/bin/sh", "-c", resultedCommand} 775 } 776 777 return []string{binaryToRun} 778 } 779 780 func determineTarDestinationDir(opts RunContainerOptions, imageMetadata *api.Image) string { 781 if len(opts.Destination) != 0 { 782 return opts.Destination 783 } 784 return getDestination(imageMetadata) 785 } 786 787 func determineCommandBaseDir(opts RunContainerOptions, imageMetadata *api.Image, tarDestination string) string { 788 if opts.ExternalScripts { 789 // for external scripts we must always append 'scripts' because this is 790 // the default subdirectory inside tar for them 791 // NOTE: We use path.Join instead of filepath.Join to avoid converting the 792 // path to UNC (Windows) format as we always run this inside container. 793 log.V(2).Infof("Both scripts and untarred source will be placed in '%s'", tarDestination) 794 return path.Join(tarDestination, "scripts") 795 } 796 797 // for internal scripts we can have separate path for scripts and untar operation destination 798 scriptsURL := opts.ScriptsURL 799 if len(scriptsURL) == 0 { 800 scriptsURL = getScriptsURL(imageMetadata) 801 } 802 803 commandBaseDir := strings.TrimPrefix(scriptsURL, "image://") 804 log.V(2).Infof("Base directory for S2I scripts is '%s'. Untarring destination is '%s'.", 805 commandBaseDir, tarDestination) 806 807 return commandBaseDir 808 } 809 810 // dumpContainerInfo dumps information about a running container (port/IP/etc). 811 func dumpContainerInfo(container dockercontainer.ContainerCreateCreatedBody, d *stiDocker, image string) { 812 ctx, cancel := getDefaultContext() 813 defer cancel() 814 815 containerJSON, err := d.client.ContainerInspect(ctx, container.ID) 816 if err != nil { 817 return 818 } 819 820 liveports := "\n\nPort Bindings: " 821 for port, bindings := range containerJSON.NetworkSettings.NetworkSettingsBase.Ports { 822 liveports = liveports + "\n Container Port: " + string(port) 823 liveports = liveports + "\n Public Host / Port Mappings:" 824 for _, binding := range bindings { 825 liveports = liveports + "\n IP: " + binding.HostIP + " Port: " + binding.HostPort 826 } 827 } 828 liveports = liveports + "\n" 829 log.V(0).Infof("\n\n\n\n\nThe image %s has been started in container %s as a result of the --run=true option. The container's stdout/stderr will be redirected to this command's log output to help you validate its behavior. You can also inspect the container with docker commands if you like. If the container is set up to stay running, you will have to Ctrl-C to exit this command, which should also stop the container %s. This particular invocation attempts to run with the port mappings %+v \n\n\n\n\n", image, container.ID, container.ID, liveports) 830 } 831 832 // redirectResponseToOutputStream handles incoming streamed data from a 833 // container on a "hijacked" connection. If tty is true, expect multiplexed 834 // streams. Rules: 1) if you ask for streamed data from a container, you have 835 // to read it, otherwise sooner or later the container will block writing it. 836 // 2) if you're receiving multiplexed data, you have to actively read both 837 // streams in parallel, otherwise in the case of non-interleaved data, you, and 838 // then the container, will block. 839 func (d *stiDocker) redirectResponseToOutputStream(tty bool, outputStream, errorStream io.Writer, resp io.Reader) error { 840 if outputStream == nil { 841 outputStream = ioutil.Discard 842 } 843 if errorStream == nil { 844 errorStream = ioutil.Discard 845 } 846 var err error 847 if tty { 848 _, err = io.Copy(outputStream, resp) 849 } else { 850 _, err = dockerstdcopy.StdCopy(outputStream, errorStream, resp) 851 } 852 return err 853 } 854 855 // holdHijackedConnection pumps data up to the container's stdin, and runs a 856 // goroutine to pump data down from the container's stdout and stderr. it holds 857 // open the HijackedResponse until all of this is done. Caller's responsibility 858 // to close resp, as well as outputStream and errorStream if appropriate. 859 func (d *stiDocker) holdHijackedConnection(tty bool, opts *RunContainerOptions, resp dockertypes.HijackedResponse) error { 860 receiveStdout := make(chan error, 1) 861 if opts.Stdout != nil || opts.Stderr != nil { 862 go func() { 863 err := d.redirectResponseToOutputStream(tty, opts.Stdout, opts.Stderr, resp.Reader) 864 if opts.Stdout != nil { 865 opts.Stdout.Close() 866 opts.Stdout = nil 867 } 868 if opts.Stderr != nil { 869 opts.Stderr.Close() 870 opts.Stderr = nil 871 } 872 receiveStdout <- err 873 }() 874 } else { 875 receiveStdout <- nil 876 } 877 878 if opts.Stdin != nil { 879 _, err := io.Copy(resp.Conn, opts.Stdin) 880 opts.Stdin.Close() 881 opts.Stdin = nil 882 if err != nil { 883 <-receiveStdout 884 return err 885 } 886 } 887 err := resp.CloseWrite() 888 if err != nil { 889 <-receiveStdout 890 return err 891 } 892 893 // Hang around until the streaming is over - either when the server closes 894 // the connection, or someone locally closes resp. 895 return <-receiveStdout 896 } 897 898 // RunContainer creates and starts a container using the image specified in opts 899 // with the ability to stream input and/or output. Any non-nil 900 // opts.Std{in,out,err} will be closed upon return. 901 func (d *stiDocker) RunContainer(opts RunContainerOptions) error { 902 // Guarantee that Std{in,out,err} are closed upon return, including under 903 // error circumstances. In normal circumstances, holdHijackedConnection 904 // should do this for us. 905 defer func() { 906 if opts.Stdin != nil { 907 opts.Stdin.Close() 908 } 909 if opts.Stdout != nil { 910 opts.Stdout.Close() 911 } 912 if opts.Stderr != nil { 913 opts.Stderr.Close() 914 } 915 }() 916 917 createOpts := opts.asDockerCreateContainerOptions() 918 919 // get info about the specified image 920 image := createOpts.Config.Image 921 inspect, err := d.InspectImage(image) 922 imageMetadata := &api.Image{} 923 if err == nil { 924 updateImageWithInspect(imageMetadata, inspect) 925 if opts.PullImage { 926 _, err = d.CheckAndPullImage(image) 927 } 928 } 929 if err != nil { 930 log.V(0).Infof("error: Unable to get image metadata for %s: %v", image, err) 931 return err 932 } 933 934 entrypoint, err := d.GetImageEntrypoint(image) 935 if err != nil { 936 return fmt.Errorf("could not get entrypoint of %q image: %v", image, err) 937 } 938 939 // If the image has an entrypoint already defined, 940 // it will be overridden either by DefaultEntrypoint, 941 // or by the value in opts.Entrypoint. 942 // If the image does not have an entrypoint, but 943 // opts.Entrypoint is supplied, opts.Entrypoint will 944 // be respected. 945 if len(entrypoint) != 0 && len(opts.Entrypoint) == 0 { 946 opts.Entrypoint = DefaultEntrypoint 947 } 948 949 // tarDestination will be passed as location to PostExecute function 950 // and will be used as the prefix for the CMD (scripts/run) 951 var tarDestination string 952 953 var cmd []string 954 if !opts.TargetImage { 955 if len(opts.CommandExplicit) != 0 { 956 cmd = opts.CommandExplicit 957 } else { 958 tarDestination = determineTarDestinationDir(opts, imageMetadata) 959 cmd = constructCommand(opts, imageMetadata, tarDestination) 960 } 961 log.V(5).Infof("Setting %q command for container ...", strings.Join(cmd, " ")) 962 } 963 createOpts.Config.Cmd = cmd 964 965 if createOpts.HostConfig != nil && createOpts.HostConfig.ShmSize <= 0 { 966 createOpts.HostConfig.ShmSize = DefaultShmSize 967 } 968 969 // Create a new container. 970 log.V(2).Infof("Creating container with options {Name:%q Config:%+v HostConfig:%+v} ...", createOpts.Name, *util.SafeForLoggingContainerConfig(createOpts.Config), createOpts.HostConfig) 971 ctx, cancel := getDefaultContext() 972 defer cancel() 973 container, err := d.client.ContainerCreate(ctx, createOpts.Config, createOpts.HostConfig, createOpts.NetworkingConfig, createOpts.Name) 974 if err != nil { 975 return err 976 } 977 978 // Container was created, so we defer its removal, and also remove it if we get a SIGINT/SIGTERM/SIGQUIT/SIGHUP. 979 removeContainer := func() { 980 log.V(4).Infof("Removing container %q ...", container.ID) 981 982 killErr := d.KillContainer(container.ID) 983 984 if removeErr := d.RemoveContainer(container.ID); removeErr != nil { 985 if killErr != nil { 986 log.V(0).Infof("warning: Failed to kill container %q: %v", container.ID, killErr) 987 } 988 log.V(0).Infof("warning: Failed to remove container %q: %v", container.ID, removeErr) 989 } else { 990 log.V(4).Infof("Removed container %q", container.ID) 991 } 992 } 993 dumpStack := func(signal os.Signal) { 994 if signal == syscall.SIGQUIT { 995 buf := make([]byte, 1<<16) 996 runtime.Stack(buf, true) 997 fmt.Printf("%s", buf) 998 } 999 os.Exit(2) 1000 } 1001 return interrupt.New(dumpStack, removeContainer).Run(func() error { 1002 log.V(2).Infof("Attaching to container %q ...", container.ID) 1003 ctx, cancel := getDefaultContext() 1004 defer cancel() 1005 resp, err := d.client.ContainerAttach(ctx, container.ID, opts.asDockerAttachToContainerOptions()) 1006 if err != nil { 1007 log.V(0).Infof("error: Unable to attach to container %q: %v", container.ID, err) 1008 return err 1009 } 1010 defer resp.Close() 1011 1012 // Start the container 1013 log.V(2).Infof("Starting container %q ...", container.ID) 1014 ctx, cancel = getDefaultContext() 1015 defer cancel() 1016 err = d.client.ContainerStart(ctx, container.ID, dockertypes.ContainerStartOptions{}) 1017 if err != nil { 1018 return err 1019 } 1020 1021 // Run OnStart hook if defined. OnStart might block, so we run it in a 1022 // new goroutine, and wait for it to be done later on. 1023 onStartDone := make(chan error, 1) 1024 if opts.OnStart != nil { 1025 go func() { 1026 onStartDone <- opts.OnStart(container.ID) 1027 }() 1028 } 1029 1030 if opts.TargetImage { 1031 // When TargetImage is true, we're dealing with an invocation of `s2i build ... --run` 1032 // so this will, e.g., run a web server and block until the user interrupts it (or 1033 // the container exits normally). dump port/etc information for the user. 1034 dumpContainerInfo(container, d, image) 1035 } 1036 1037 err = d.holdHijackedConnection(false, &opts, resp) 1038 if err != nil { 1039 return err 1040 } 1041 1042 // Return an error if the exit code of the container is 1043 // non-zero. 1044 log.V(4).Infof("Waiting for container %q to stop ...", container.ID) 1045 waitC, errC := d.client.ContainerWait(context.Background(), container.ID, dockercontainer.WaitConditionNotRunning) 1046 select { 1047 case result := <-waitC: 1048 if result.StatusCode != 0 { 1049 var output string 1050 jsonOutput, _ := d.client.ContainerInspect(ctx, container.ID) 1051 if err == nil && jsonOutput.ContainerJSONBase != nil && jsonOutput.ContainerJSONBase.State != nil { 1052 state := jsonOutput.ContainerJSONBase.State 1053 output = fmt.Sprintf("Status: %s, Error: %s, OOMKilled: %v, Dead: %v", state.Status, state.Error, state.OOMKilled, state.Dead) 1054 } 1055 return s2ierr.NewContainerError(container.ID, int(result.StatusCode), output) 1056 } 1057 case err := <-errC: 1058 return fmt.Errorf("waiting for container %q to stop: %v", container.ID, err) 1059 } 1060 1061 // OnStart must be done before we move on. 1062 if opts.OnStart != nil { 1063 if err = <-onStartDone; err != nil { 1064 return err 1065 } 1066 } 1067 // Run PostExec hook if defined. 1068 if opts.PostExec != nil { 1069 log.V(2).Infof("Invoking PostExecute function") 1070 if err = opts.PostExec.PostExecute(container.ID, tarDestination); err != nil { 1071 return err 1072 } 1073 } 1074 return nil 1075 }) 1076 } 1077 1078 // GetImageID retrieves the ID of the image identified by name 1079 func (d *stiDocker) GetImageID(name string) (string, error) { 1080 name = getImageName(name) 1081 image, err := d.InspectImage(name) 1082 if err != nil { 1083 return "", err 1084 } 1085 return image.ID, nil 1086 } 1087 1088 // CommitContainer commits a container to an image with a specific tag. 1089 // The new image ID is returned 1090 func (d *stiDocker) CommitContainer(opts CommitContainerOptions) (string, error) { 1091 dockerOpts := dockertypes.ContainerCommitOptions{ 1092 Reference: opts.Repository, 1093 } 1094 if opts.Command != nil || opts.Entrypoint != nil { 1095 config := dockercontainer.Config{ 1096 Cmd: opts.Command, 1097 Entrypoint: opts.Entrypoint, 1098 Env: opts.Env, 1099 Labels: opts.Labels, 1100 User: opts.User, 1101 } 1102 dockerOpts.Config = &config 1103 log.V(2).Infof("Committing container with dockerOpts: %+v, config: %+v", dockerOpts, *util.SafeForLoggingContainerConfig(&config)) 1104 } 1105 1106 resp, err := d.client.ContainerCommit(context.Background(), opts.ContainerID, dockerOpts) 1107 if err == nil { 1108 return resp.ID, nil 1109 } 1110 return "", err 1111 } 1112 1113 // RemoveImage removes the image with specified ID 1114 func (d *stiDocker) RemoveImage(imageID string) error { 1115 ctx, cancel := getDefaultContext() 1116 defer cancel() 1117 _, err := d.client.ImageRemove(ctx, imageID, dockertypes.ImageRemoveOptions{}) 1118 return err 1119 } 1120 1121 // BuildImage builds the image according to specified options 1122 func (d *stiDocker) BuildImage(opts BuildImageOptions) error { 1123 dockerOpts := dockertypes.ImageBuildOptions{ 1124 Tags: []string{opts.Name}, 1125 NoCache: true, 1126 SuppressOutput: false, 1127 Remove: true, 1128 ForceRemove: true, 1129 } 1130 if opts.CGroupLimits != nil { 1131 dockerOpts.Memory = opts.CGroupLimits.MemoryLimitBytes 1132 dockerOpts.MemorySwap = opts.CGroupLimits.MemorySwap 1133 dockerOpts.CgroupParent = opts.CGroupLimits.Parent 1134 } 1135 log.V(2).Infof("Building container using config: %+v", dockerOpts) 1136 resp, err := d.client.ImageBuild(context.Background(), opts.Stdin, dockerOpts) 1137 if err != nil { 1138 return err 1139 } 1140 defer resp.Body.Close() 1141 // since can't pass in output stream to engine-api, need to copy contents of 1142 // the output stream they create into our output stream 1143 _, err = io.Copy(opts.Stdout, resp.Body) 1144 if opts.Stdout != nil { 1145 opts.Stdout.Close() 1146 } 1147 return err 1148 }