github.com/nak3/source-to-image@v1.1.10-0.20180319140719-2ed55639898d/pkg/build/strategies/sti/postexecutorstep.go (about) 1 package sti 2 3 import ( 4 "archive/tar" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path" 11 "path/filepath" 12 "strings" 13 "time" 14 15 "github.com/openshift/source-to-image/pkg/api" 16 dockerpkg "github.com/openshift/source-to-image/pkg/docker" 17 s2ierr "github.com/openshift/source-to-image/pkg/errors" 18 s2itar "github.com/openshift/source-to-image/pkg/tar" 19 "github.com/openshift/source-to-image/pkg/util" 20 "github.com/openshift/source-to-image/pkg/util/fs" 21 utilstatus "github.com/openshift/source-to-image/pkg/util/status" 22 ) 23 24 const maximumLabelSize = 10240 25 26 type postExecutorStepContext struct { 27 // id of the previous image that we're holding because after committing the image, we'll lose it. 28 // Used only when build is incremental and RemovePreviousImage setting is enabled. 29 // See also: storePreviousImageStep and removePreviousImageStep 30 previousImageID string 31 32 // Container id that will be committed. 33 // See also: commitImageStep 34 containerID string 35 36 // Path to a directory in the image where scripts (for example, "run") will be placed. 37 // This location will be used for generation of the CMD directive. 38 // See also: commitImageStep 39 destination string 40 41 // Image id created by committing the container. 42 // See also: commitImageStep and reportAboutSuccessStep 43 imageID string 44 45 // Labels that will be passed to a callback. 46 // These labels are added to the image during commit. 47 // See also: commitImageStep and STI.Build() 48 labels map[string]string 49 } 50 51 type postExecutorStep interface { 52 execute(*postExecutorStepContext) error 53 } 54 55 type storePreviousImageStep struct { 56 builder *STI 57 docker dockerpkg.Docker 58 } 59 60 func (step *storePreviousImageStep) execute(ctx *postExecutorStepContext) error { 61 if step.builder.incremental && step.builder.config.RemovePreviousImage { 62 glog.V(3).Info("Executing step: store previous image") 63 ctx.previousImageID = step.getPreviousImage() 64 return nil 65 } 66 67 glog.V(3).Info("Skipping step: store previous image") 68 return nil 69 } 70 71 func (step *storePreviousImageStep) getPreviousImage() string { 72 previousImageID, err := step.docker.GetImageID(step.builder.config.Tag) 73 if err != nil { 74 glog.V(0).Infof("error: Error retrieving previous image's (%v) metadata: %v", step.builder.config.Tag, err) 75 return "" 76 } 77 return previousImageID 78 } 79 80 type removePreviousImageStep struct { 81 builder *STI 82 docker dockerpkg.Docker 83 } 84 85 func (step *removePreviousImageStep) execute(ctx *postExecutorStepContext) error { 86 if step.builder.incremental && step.builder.config.RemovePreviousImage { 87 glog.V(3).Info("Executing step: remove previous image") 88 step.removePreviousImage(ctx.previousImageID) 89 return nil 90 } 91 92 glog.V(3).Info("Skipping step: remove previous image") 93 return nil 94 } 95 96 func (step *removePreviousImageStep) removePreviousImage(previousImageID string) { 97 if previousImageID == "" { 98 return 99 } 100 101 glog.V(1).Infof("Removing previously-tagged image %s", previousImageID) 102 if err := step.docker.RemoveImage(previousImageID); err != nil { 103 glog.V(0).Infof("error: Unable to remove previous image: %v", err) 104 } 105 } 106 107 type commitImageStep struct { 108 image string 109 builder *STI 110 docker dockerpkg.Docker 111 fs fs.FileSystem 112 tar s2itar.Tar 113 } 114 115 func (step *commitImageStep) execute(ctx *postExecutorStepContext) error { 116 glog.V(3).Infof("Executing step: commit image") 117 118 user, err := step.docker.GetImageUser(step.image) 119 if err != nil { 120 return fmt.Errorf("could not get user of %q image: %v", step.image, err) 121 } 122 123 cmd := createCommandForExecutingRunScript(step.builder.scriptsURL, ctx.destination) 124 125 if err = checkAndGetNewLabels(step.builder, step.docker, step.tar, ctx.containerID); err != nil { 126 return fmt.Errorf("could not check for new labels for %q image: %v", step.image, err) 127 } 128 129 ctx.labels = createLabelsForResultingImage(step.builder, step.docker, step.image) 130 131 if err = checkLabelSize(ctx.labels); err != nil { 132 return fmt.Errorf("label validation failed for %q image: %v", step.image, err) 133 } 134 135 // Set the image entrypoint back to its original value on commit, the running 136 // container has "env" as its entrypoint and we don't want to commit that. 137 entrypoint, err := step.docker.GetImageEntrypoint(step.image) 138 if err != nil { 139 return fmt.Errorf("could not get entrypoint of %q image: %v", step.image, err) 140 } 141 // If the image has no explicit entrypoint, set it to an empty array 142 // so we don't default to leaving the entrypoint as "env" upon commit. 143 if entrypoint == nil { 144 entrypoint = []string{} 145 } 146 startTime := time.Now() 147 ctx.imageID, err = commitContainer( 148 step.docker, 149 ctx.containerID, 150 cmd, 151 user, 152 step.builder.config.Tag, 153 step.builder.env, 154 entrypoint, 155 ctx.labels, 156 ) 157 step.builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(step.builder.result.BuildInfo.Stages, api.StageCommit, api.StepCommitContainer, startTime, time.Now()) 158 if err != nil { 159 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 160 utilstatus.ReasonCommitContainerFailed, 161 utilstatus.ReasonMessageCommitContainerFailed, 162 ) 163 return err 164 } 165 166 return nil 167 } 168 169 type downloadFilesFromBuilderImageStep struct { 170 builder *STI 171 docker dockerpkg.Docker 172 fs fs.FileSystem 173 tar s2itar.Tar 174 } 175 176 func (step *downloadFilesFromBuilderImageStep) execute(ctx *postExecutorStepContext) error { 177 glog.V(3).Info("Executing step: download files from the builder image") 178 179 artifactsDir := filepath.Join(step.builder.config.WorkingDir, api.RuntimeArtifactsDir) 180 if err := step.fs.Mkdir(artifactsDir); err != nil { 181 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 182 utilstatus.ReasonFSOperationFailed, 183 utilstatus.ReasonMessageFSOperationFailed, 184 ) 185 return fmt.Errorf("could not create directory %q: %v", artifactsDir, err) 186 } 187 188 for _, artifact := range step.builder.config.RuntimeArtifacts { 189 if err := step.downloadAndExtractFile(artifact.Source, artifactsDir, ctx.containerID); err != nil { 190 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 191 utilstatus.ReasonRuntimeArtifactsFetchFailed, 192 utilstatus.ReasonMessageRuntimeArtifactsFetchFailed, 193 ) 194 return err 195 } 196 197 // for mapping like "/tmp/foo.txt -> app" we should create "app" and move "foo.txt" to that directory 198 dstSubDir := path.Clean(artifact.Destination) 199 if dstSubDir != "." && dstSubDir != "/" { 200 dstDir := filepath.Join(artifactsDir, dstSubDir) 201 glog.V(5).Infof("Creating directory %q", dstDir) 202 if err := step.fs.MkdirAll(dstDir); err != nil { 203 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 204 utilstatus.ReasonFSOperationFailed, 205 utilstatus.ReasonMessageFSOperationFailed, 206 ) 207 return fmt.Errorf("could not create directory %q: %v", dstDir, err) 208 } 209 210 currentFile := filepath.Base(artifact.Source) 211 oldFile := filepath.Join(artifactsDir, currentFile) 212 newFile := filepath.Join(artifactsDir, dstSubDir, currentFile) 213 glog.V(5).Infof("Renaming %q to %q", oldFile, newFile) 214 if err := step.fs.Rename(oldFile, newFile); err != nil { 215 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 216 utilstatus.ReasonFSOperationFailed, 217 utilstatus.ReasonMessageFSOperationFailed, 218 ) 219 return fmt.Errorf("could not rename %q -> %q: %v", oldFile, newFile, err) 220 } 221 } 222 } 223 224 return nil 225 } 226 227 func (step *downloadFilesFromBuilderImageStep) downloadAndExtractFile(artifactPath, artifactsDir, containerID string) error { 228 if res, err := downloadAndExtractFileFromContainer(step.docker, step.tar, artifactPath, artifactsDir, containerID); err != nil { 229 step.builder.result.BuildInfo.FailureReason = res 230 return err 231 } 232 return nil 233 } 234 235 type startRuntimeImageAndUploadFilesStep struct { 236 builder *STI 237 docker dockerpkg.Docker 238 fs fs.FileSystem 239 } 240 241 func (step *startRuntimeImageAndUploadFilesStep) execute(ctx *postExecutorStepContext) error { 242 glog.V(3).Info("Executing step: start runtime image and upload files") 243 244 fd, err := ioutil.TempFile("", "s2i-upload-done") 245 if err != nil { 246 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 247 utilstatus.ReasonGenericS2IBuildFailed, 248 utilstatus.ReasonMessageGenericS2iBuildFailed, 249 ) 250 return err 251 } 252 fd.Close() 253 lastFilePath := fd.Name() 254 defer func() { 255 os.Remove(lastFilePath) 256 }() 257 258 lastFileDstPath := "/tmp/" + filepath.Base(lastFilePath) 259 260 outReader, outWriter := io.Pipe() 261 errReader, errWriter := io.Pipe() 262 263 artifactsDir := filepath.Join(step.builder.config.WorkingDir, api.RuntimeArtifactsDir) 264 265 // We copy scripts to a directory with artifacts to upload files in one shot 266 for _, script := range []string{api.AssembleRuntime, api.Run} { 267 // scripts must be inside of "scripts" subdir, see createCommandForExecutingRunScript() 268 destinationDir := filepath.Join(artifactsDir, "scripts") 269 err = step.copyScriptIfNeeded(script, destinationDir) 270 if err != nil { 271 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 272 utilstatus.ReasonGenericS2IBuildFailed, 273 utilstatus.ReasonMessageGenericS2iBuildFailed, 274 ) 275 return err 276 } 277 } 278 279 image := step.builder.config.RuntimeImage 280 workDir, err := step.docker.GetImageWorkdir(image) 281 if err != nil { 282 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 283 utilstatus.ReasonGenericS2IBuildFailed, 284 utilstatus.ReasonMessageGenericS2iBuildFailed, 285 ) 286 return fmt.Errorf("could not get working dir of %q image: %v", image, err) 287 } 288 289 commandBaseDir := filepath.Join(workDir, "scripts") 290 useExternalAssembleScript := step.builder.externalScripts[api.AssembleRuntime] 291 if !useExternalAssembleScript { 292 // script already inside of the image 293 var scriptsURL string 294 scriptsURL, err = step.docker.GetScriptsURL(image) 295 if err != nil { 296 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 297 utilstatus.ReasonGenericS2IBuildFailed, 298 utilstatus.ReasonMessageGenericS2iBuildFailed, 299 ) 300 return err 301 } 302 if len(scriptsURL) == 0 { 303 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 304 utilstatus.ReasonGenericS2IBuildFailed, 305 utilstatus.ReasonMessageGenericS2iBuildFailed, 306 ) 307 return fmt.Errorf("could not determine scripts URL for image %q", image) 308 } 309 commandBaseDir = strings.TrimPrefix(scriptsURL, "image://") 310 } 311 312 cmd := fmt.Sprintf( 313 "while [ ! -f %q ]; do sleep 0.5; done; %s/%s; exit $?", 314 lastFileDstPath, 315 commandBaseDir, 316 api.AssembleRuntime, 317 ) 318 319 opts := dockerpkg.RunContainerOptions{ 320 Image: image, 321 PullImage: false, // The PullImage is false because we've already pulled the image 322 CommandExplicit: []string{"/bin/sh", "-c", cmd}, 323 Stdout: outWriter, 324 Stderr: errWriter, 325 NetworkMode: string(step.builder.config.DockerNetworkMode), 326 CGroupLimits: step.builder.config.CGroupLimits, 327 CapDrop: step.builder.config.DropCapabilities, 328 PostExec: step.builder.postExecutor, 329 Env: step.builder.env, 330 } 331 332 opts.OnStart = func(containerID string) error { 333 setStandardPerms := func(writer io.Writer) s2itar.Writer { 334 return s2itar.ChmodAdapter{Writer: tar.NewWriter(writer), NewFileMode: 0644, NewExecFileMode: 0755, NewDirMode: 0755} 335 } 336 337 glog.V(5).Infof("Uploading directory %q -> %q", artifactsDir, workDir) 338 onStartErr := step.docker.UploadToContainerWithTarWriter(step.fs, artifactsDir, workDir, containerID, setStandardPerms) 339 if onStartErr != nil { 340 return fmt.Errorf("could not upload directory (%q -> %q) into container %s: %v", artifactsDir, workDir, containerID, err) 341 } 342 343 glog.V(5).Infof("Uploading file %q -> %q", lastFilePath, lastFileDstPath) 344 onStartErr = step.docker.UploadToContainerWithTarWriter(step.fs, lastFilePath, lastFileDstPath, containerID, setStandardPerms) 345 if onStartErr != nil { 346 return fmt.Errorf("could not upload file (%q -> %q) into container %s: %v", lastFilePath, lastFileDstPath, containerID, err) 347 } 348 349 return onStartErr 350 } 351 352 dockerpkg.StreamContainerIO(outReader, nil, func(s string) { glog.V(0).Info(s) }) 353 354 errOutput := "" 355 c := dockerpkg.StreamContainerIO(errReader, &errOutput, func(s string) { glog.Info(s) }) 356 357 // switch to the next stage of post executors steps 358 step.builder.postExecutorStage++ 359 360 err = step.docker.RunContainer(opts) 361 if e, ok := err.(s2ierr.ContainerError); ok { 362 // Must wait for StreamContainerIO goroutine above to exit before reading errOutput. 363 <-c 364 err = s2ierr.NewContainerError(image, e.ErrorCode, errOutput+e.Output) 365 } 366 367 return err 368 } 369 370 func (step *startRuntimeImageAndUploadFilesStep) copyScriptIfNeeded(script, destinationDir string) error { 371 useExternalScript := step.builder.externalScripts[script] 372 if useExternalScript { 373 src := filepath.Join(step.builder.config.WorkingDir, api.UploadScripts, script) 374 dst := filepath.Join(destinationDir, script) 375 glog.V(5).Infof("Copying file %q -> %q", src, dst) 376 if err := step.fs.MkdirAll(destinationDir); err != nil { 377 return fmt.Errorf("could not create directory %q: %v", destinationDir, err) 378 } 379 if err := step.fs.Copy(src, dst); err != nil { 380 return fmt.Errorf("could not copy file (%q -> %q): %v", src, dst, err) 381 } 382 } 383 return nil 384 } 385 386 type reportSuccessStep struct { 387 builder *STI 388 } 389 390 func (step *reportSuccessStep) execute(ctx *postExecutorStepContext) error { 391 glog.V(3).Info("Executing step: report success") 392 393 step.builder.result.Success = true 394 step.builder.result.ImageID = ctx.imageID 395 396 glog.V(3).Infof("Successfully built %s", firstNonEmpty(step.builder.config.Tag, ctx.imageID)) 397 398 return nil 399 } 400 401 // shared methods 402 403 func commitContainer(docker dockerpkg.Docker, containerID, cmd, user, tag string, env, entrypoint []string, labels map[string]string) (string, error) { 404 opts := dockerpkg.CommitContainerOptions{ 405 Command: []string{cmd}, 406 Env: env, 407 Entrypoint: entrypoint, 408 ContainerID: containerID, 409 Repository: tag, 410 User: user, 411 Labels: labels, 412 } 413 414 imageID, err := docker.CommitContainer(opts) 415 if err != nil { 416 return "", s2ierr.NewCommitError(tag, err) 417 } 418 419 return imageID, nil 420 } 421 422 func createLabelsForResultingImage(builder *STI, docker dockerpkg.Docker, baseImage string) map[string]string { 423 generatedLabels := util.GenerateOutputImageLabels(builder.sourceInfo, builder.config) 424 425 existingLabels, err := docker.GetLabels(baseImage) 426 if err != nil { 427 glog.V(0).Infof("error: Unable to read existing labels from the base image %s", baseImage) 428 } 429 430 configLabels := builder.config.Labels 431 newLabels := builder.newLabels 432 433 return mergeLabels(existingLabels, generatedLabels, configLabels, newLabels) 434 } 435 436 func mergeLabels(labels ...map[string]string) map[string]string { 437 mergedLabels := map[string]string{} 438 439 for _, labelMap := range labels { 440 for k, v := range labelMap { 441 mergedLabels[k] = v 442 } 443 } 444 return mergedLabels 445 } 446 447 func createCommandForExecutingRunScript(scriptsURL map[string]string, location string) string { 448 cmd := scriptsURL[api.Run] 449 if strings.HasPrefix(cmd, "image://") { 450 // scripts from inside of the image, we need to strip the image part 451 // NOTE: We use path.Join instead of filepath.Join to avoid converting the 452 // path to UNC (Windows) format as we always run this inside container. 453 cmd = strings.TrimPrefix(cmd, "image://") 454 } else { 455 // external scripts, in which case we're taking the directory to which they 456 // were extracted and append scripts dir and name 457 cmd = path.Join(location, "scripts", api.Run) 458 } 459 return cmd 460 } 461 462 func downloadAndExtractFileFromContainer(docker dockerpkg.Docker, tar s2itar.Tar, sourcePath, destinationPath, containerID string) (api.FailureReason, error) { 463 glog.V(5).Infof("Downloading file %q", sourcePath) 464 465 fd, err := ioutil.TempFile(destinationPath, "s2i-runtime-artifact") 466 if err != nil { 467 res := utilstatus.NewFailureReason( 468 utilstatus.ReasonFSOperationFailed, 469 utilstatus.ReasonMessageFSOperationFailed, 470 ) 471 return res, fmt.Errorf("could not create temporary file for runtime artifact: %v", err) 472 } 473 defer func() { 474 fd.Close() 475 os.Remove(fd.Name()) 476 }() 477 478 if err := docker.DownloadFromContainer(sourcePath, fd, containerID); err != nil { 479 res := utilstatus.NewFailureReason( 480 utilstatus.ReasonGenericS2IBuildFailed, 481 utilstatus.ReasonMessageGenericS2iBuildFailed, 482 ) 483 return res, fmt.Errorf("could not download file (%q -> %q) from container %s: %v", sourcePath, fd.Name(), containerID, err) 484 } 485 486 // after writing to the file descriptor we need to rewind pointer to the beginning of the file before next reading 487 if _, err := fd.Seek(0, io.SeekStart); err != nil { 488 res := utilstatus.NewFailureReason( 489 utilstatus.ReasonGenericS2IBuildFailed, 490 utilstatus.ReasonMessageGenericS2iBuildFailed, 491 ) 492 return res, fmt.Errorf("could not seek to the beginning of the file %q: %v", fd.Name(), err) 493 } 494 495 if err := tar.ExtractTarStream(destinationPath, fd); err != nil { 496 res := utilstatus.NewFailureReason( 497 utilstatus.ReasonGenericS2IBuildFailed, 498 utilstatus.ReasonMessageGenericS2iBuildFailed, 499 ) 500 return res, fmt.Errorf("could not extract artifact %q into the directory %q: %v", sourcePath, destinationPath, err) 501 } 502 503 return utilstatus.NewFailureReason("", ""), nil 504 } 505 506 func checkLabelSize(labels map[string]string) error { 507 var sum = 0 508 for k, v := range labels { 509 sum += len(k) + len(v) 510 } 511 512 if sum > maximumLabelSize { 513 return fmt.Errorf("label size '%d' exceeds the maximum limit '%d'", sum, maximumLabelSize) 514 } 515 516 return nil 517 } 518 519 // check for new labels and apply to the output image. 520 func checkAndGetNewLabels(builder *STI, docker dockerpkg.Docker, tar s2itar.Tar, containerID string) error { 521 glog.V(3).Infof("Checking for new Labels to apply... ") 522 523 // metadata filename and its path inside the container 524 metadataFilename := "image_metadata.json" 525 sourceFilepath := filepath.Join("/tmp/.s2i", metadataFilename) 526 527 // create the 'downloadPath' folder if it doesn't exist 528 downloadPath := filepath.Join(builder.config.WorkingDir, "metadata") 529 glog.V(3).Infof("Creating the download path '%s'", downloadPath) 530 if err := os.MkdirAll(downloadPath, 0700); err != nil { 531 glog.Errorf("Error creating dir %q for '%s': %v", downloadPath, metadataFilename, err) 532 return err 533 } 534 535 // download & extract the file from container 536 if _, err := downloadAndExtractFileFromContainer(docker, tar, sourceFilepath, downloadPath, containerID); err != nil { 537 glog.V(3).Infof("unable to download and extract '%s' ... continuing", metadataFilename) 538 return nil 539 } 540 541 // open the file 542 filePath := filepath.Join(downloadPath, metadataFilename) 543 fd, err := os.Open(filePath) 544 if fd == nil || err != nil { 545 return fmt.Errorf("unable to open file '%s' : %v", downloadPath, err) 546 } 547 defer fd.Close() 548 549 // read the file to a string 550 str, err := ioutil.ReadAll(fd) 551 if err != nil { 552 return fmt.Errorf("error reading file '%s' in to a string: %v", filePath, err) 553 } 554 glog.V(3).Infof("new Labels File contents : \n%s\n", str) 555 556 // string into a map 557 var data map[string]interface{} 558 559 if err = json.Unmarshal([]byte(str), &data); err != nil { 560 return fmt.Errorf("JSON Unmarshal Error with '%s' file : %v", metadataFilename, err) 561 } 562 563 // update newLabels[] 564 labels := data["labels"] 565 for _, l := range labels.([]interface{}) { 566 for k, v := range l.(map[string]interface{}) { 567 builder.newLabels[k] = v.(string) 568 } 569 } 570 571 return nil 572 }