kubesphere.io/s2irun@v3.2.1+incompatible/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/kubesphere/s2irun/pkg/api" 16 "github.com/kubesphere/s2irun/pkg/api/constants" 17 dockerpkg "github.com/kubesphere/s2irun/pkg/docker" 18 s2ierr "github.com/kubesphere/s2irun/pkg/errors" 19 s2itar "github.com/kubesphere/s2irun/pkg/tar" 20 "github.com/kubesphere/s2irun/pkg/utils" 21 "github.com/kubesphere/s2irun/pkg/utils/fs" 22 utilstatus "github.com/kubesphere/s2irun/pkg/utils/status" 23 ) 24 25 const maximumLabelSize = 10240 26 27 type postExecutorStepContext struct { 28 // id of the previous image that we're holding because after committing the image, we'll lose it. 29 // Used only when build is incremental and RemovePreviousImage setting is enabled. 30 // See also: storePreviousImageStep and removePreviousImageStep 31 previousImageID string 32 33 // Container id that will be committed. 34 // See also: commitImageStep 35 containerID string 36 37 // Path to a directory in the image where scripts (for example, "run") will be placed. 38 // This location will be used for generation of the CMD directive. 39 // See also: commitImageStep 40 destination string 41 42 // Image id created by committing the container. 43 // See also: commitImageStep and reportAboutSuccessStep 44 imageID string 45 46 // Labels that will be passed to a callback. 47 // These labels are added to the image during commit. 48 // See also: commitImageStep and STI.Build() 49 labels map[string]string 50 } 51 52 type postExecutorStep interface { 53 execute(*postExecutorStepContext) error 54 } 55 56 type storePreviousImageStep struct { 57 builder *STI 58 docker dockerpkg.Docker 59 } 60 61 func (step *storePreviousImageStep) execute(ctx *postExecutorStepContext) error { 62 if step.builder.incremental && step.builder.config.RemovePreviousImage { 63 glog.V(3).Info("Executing step: store previous image") 64 ctx.previousImageID = step.getPreviousImage() 65 return nil 66 } 67 68 glog.V(3).Info("Skipping step: store previous image") 69 return nil 70 } 71 72 func (step *storePreviousImageStep) getPreviousImage() string { 73 previousImageID, err := step.docker.GetImageID(step.builder.config.Tag) 74 if err != nil { 75 glog.V(0).Infof("error: Error retrieving previous image's (%v) metadata: %v", step.builder.config.Tag, err) 76 return "" 77 } 78 return previousImageID 79 } 80 81 type removePreviousImageStep struct { 82 builder *STI 83 docker dockerpkg.Docker 84 } 85 86 func (step *removePreviousImageStep) execute(ctx *postExecutorStepContext) error { 87 if step.builder.incremental && step.builder.config.RemovePreviousImage { 88 glog.V(3).Info("Executing step: remove previous image") 89 step.removePreviousImage(ctx.previousImageID) 90 return nil 91 } 92 93 glog.V(3).Info("Skipping step: remove previous image") 94 return nil 95 } 96 97 func (step *removePreviousImageStep) removePreviousImage(previousImageID string) { 98 if previousImageID == "" { 99 return 100 } 101 102 glog.V(1).Infof("Removing previously-tagged image %s", previousImageID) 103 if err := step.docker.RemoveImage(previousImageID); err != nil { 104 glog.V(0).Infof("error: Unable to remove previous image: %v", err) 105 } 106 } 107 108 type commitImageStep struct { 109 image string 110 builder *STI 111 docker dockerpkg.Docker 112 fs fs.FileSystem 113 tar s2itar.Tar 114 } 115 116 func (step *commitImageStep) execute(ctx *postExecutorStepContext) error { 117 glog.V(3).Infof("Executing step: commit image") 118 119 user, err := step.docker.GetImageUser(step.image) 120 if err != nil { 121 return fmt.Errorf("could not get user of %q image: %v", step.image, err) 122 } 123 124 cmd := createCommandForExecutingRunScript(step.builder.scriptsURL, ctx.destination) 125 126 if err = checkAndGetNewLabels(step.builder, step.docker, step.tar, ctx.containerID); err != nil { 127 return fmt.Errorf("could not check for new labels for %q image: %v", step.image, err) 128 } 129 130 ctx.labels = createLabelsForResultingImage(step.builder, step.docker, step.image) 131 132 if err = checkLabelSize(ctx.labels); err != nil { 133 return fmt.Errorf("label validation failed for %q image: %v", step.image, err) 134 } 135 136 // Set the image entrypoint back to its original value on commit, the running 137 // container has "env" as its entrypoint and we don't want to commit that. 138 entrypoint, err := step.docker.GetImageEntrypoint(step.image) 139 if err != nil { 140 return fmt.Errorf("could not get entrypoint of %q image: %v", step.image, err) 141 } 142 // If the image has no explicit entrypoint, set it to an empty array 143 // so we don't default to leaving the entrypoint as "env" upon commit. 144 if entrypoint == nil { 145 entrypoint = []string{} 146 } 147 startTime := time.Now() 148 ctx.imageID, err = commitContainer( 149 step.docker, 150 ctx.containerID, 151 cmd, 152 user, 153 step.builder.config.Tag, 154 step.builder.env, 155 entrypoint, 156 ctx.labels, 157 ) 158 step.builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(step.builder.result.BuildInfo.Stages, api.StageCommit, api.StepCommitContainer, startTime, time.Now()) 159 if err != nil { 160 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 161 utilstatus.ReasonCommitContainerFailed, 162 utilstatus.ReasonMessageCommitContainerFailed, 163 ) 164 return err 165 } 166 167 return nil 168 } 169 170 type downloadFilesFromBuilderImageStep struct { 171 builder *STI 172 docker dockerpkg.Docker 173 fs fs.FileSystem 174 tar s2itar.Tar 175 } 176 177 func (step *downloadFilesFromBuilderImageStep) execute(ctx *postExecutorStepContext) error { 178 glog.V(3).Info("Executing step: download files from the builder image") 179 180 artifactsDir := filepath.Join(step.builder.config.WorkingDir, constants.RuntimeArtifactsDir) 181 if err := step.fs.Mkdir(artifactsDir); err != nil { 182 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 183 utilstatus.ReasonFSOperationFailed, 184 utilstatus.ReasonMessageFSOperationFailed, 185 ) 186 return fmt.Errorf("could not create directory %q: %v", artifactsDir, err) 187 } 188 189 for _, artifact := range step.builder.config.RuntimeArtifacts { 190 if err := step.downloadAndExtractFile(artifact.Source, artifactsDir, ctx.containerID); err != nil { 191 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 192 utilstatus.ReasonRuntimeArtifactsFetchFailed, 193 utilstatus.ReasonMessageRuntimeArtifactsFetchFailed, 194 ) 195 return err 196 } 197 198 // for mapping like "/tmp/foo.txt -> app" we should create "app" and move "foo.txt" to that directory 199 dstSubDir := path.Clean(artifact.Destination) 200 if dstSubDir != "." && dstSubDir != "/" { 201 dstDir := filepath.Join(artifactsDir, dstSubDir) 202 glog.V(5).Infof("Creating directory %q", dstDir) 203 if err := step.fs.MkdirAll(dstDir); err != nil { 204 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 205 utilstatus.ReasonFSOperationFailed, 206 utilstatus.ReasonMessageFSOperationFailed, 207 ) 208 return fmt.Errorf("could not create directory %q: %v", dstDir, err) 209 } 210 211 currentFile := filepath.Base(artifact.Source) 212 oldFile := filepath.Join(artifactsDir, currentFile) 213 newFile := filepath.Join(artifactsDir, dstSubDir, currentFile) 214 glog.V(5).Infof("Renaming %q to %q", oldFile, newFile) 215 if err := step.fs.Rename(oldFile, newFile); err != nil { 216 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 217 utilstatus.ReasonFSOperationFailed, 218 utilstatus.ReasonMessageFSOperationFailed, 219 ) 220 return fmt.Errorf("could not rename %q -> %q: %v", oldFile, newFile, err) 221 } 222 } 223 } 224 225 return nil 226 } 227 228 func (step *downloadFilesFromBuilderImageStep) downloadAndExtractFile(artifactPath, artifactsDir, containerID string) error { 229 if res, err := downloadAndExtractFileFromContainer(step.docker, step.tar, artifactPath, artifactsDir, containerID); err != nil { 230 step.builder.result.BuildInfo.FailureReason = res 231 return err 232 } 233 return nil 234 } 235 236 type startRuntimeImageAndUploadFilesStep struct { 237 builder *STI 238 docker dockerpkg.Docker 239 fs fs.FileSystem 240 } 241 242 func (step *startRuntimeImageAndUploadFilesStep) execute(ctx *postExecutorStepContext) error { 243 glog.V(3).Info("Executing step: start runtime image and upload files") 244 245 fd, err := ioutil.TempFile("", "s2i-upload-done") 246 if err != nil { 247 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 248 utilstatus.ReasonGenericS2IBuildFailed, 249 utilstatus.ReasonMessageGenericS2iBuildFailed, 250 ) 251 return err 252 } 253 fd.Close() 254 lastFilePath := fd.Name() 255 defer func() { 256 os.Remove(lastFilePath) 257 }() 258 259 lastFileDstPath := "/tmp/" + filepath.Base(lastFilePath) 260 261 outReader, outWriter := io.Pipe() 262 errReader, errWriter := io.Pipe() 263 264 artifactsDir := filepath.Join(step.builder.config.WorkingDir, constants.RuntimeArtifactsDir) 265 266 // We copy scripts to a directory with artifacts to upload files in one shot 267 for _, script := range []string{constants.AssembleRuntime, constants.Run} { 268 // scripts must be inside of "scripts" subdir, see createCommandForExecutingRunScript() 269 destinationDir := filepath.Join(artifactsDir, "scripts") 270 err = step.copyScriptIfNeeded(script, destinationDir) 271 if err != nil { 272 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 273 utilstatus.ReasonGenericS2IBuildFailed, 274 utilstatus.ReasonMessageGenericS2iBuildFailed, 275 ) 276 return err 277 } 278 } 279 280 image := step.builder.config.RuntimeImage 281 workDir, err := step.docker.GetImageWorkdir(image) 282 if err != nil { 283 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 284 utilstatus.ReasonGenericS2IBuildFailed, 285 utilstatus.ReasonMessageGenericS2iBuildFailed, 286 ) 287 return fmt.Errorf("could not get working dir of %q image: %v", image, err) 288 } 289 290 commandBaseDir := filepath.Join(workDir, "scripts") 291 useExternalAssembleScript := step.builder.externalScripts[constants.AssembleRuntime] 292 if !useExternalAssembleScript { 293 // script already inside of the image 294 var scriptsURL string 295 scriptsURL, err = step.docker.GetScriptsURL(image) 296 if err != nil { 297 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 298 utilstatus.ReasonGenericS2IBuildFailed, 299 utilstatus.ReasonMessageGenericS2iBuildFailed, 300 ) 301 return err 302 } 303 if len(scriptsURL) == 0 { 304 step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 305 utilstatus.ReasonGenericS2IBuildFailed, 306 utilstatus.ReasonMessageGenericS2iBuildFailed, 307 ) 308 return fmt.Errorf("could not determine scripts URL for image %q", image) 309 } 310 commandBaseDir = strings.TrimPrefix(scriptsURL, "image://") 311 } 312 313 cmd := fmt.Sprintf( 314 "while [ ! -f %q ]; do sleep 0.5; done; %s/%s; exit $?", 315 lastFileDstPath, 316 commandBaseDir, 317 constants.AssembleRuntime, 318 ) 319 320 opts := dockerpkg.RunContainerOptions{ 321 Image: image, 322 PullImage: false, // The PullImage is false because we've already pulled the image 323 CommandExplicit: []string{"/bin/sh", "-c", cmd}, 324 Stdout: outWriter, 325 Stderr: errWriter, 326 NetworkMode: string(step.builder.config.DockerNetworkMode), 327 CGroupLimits: step.builder.config.CGroupLimits, 328 CapDrop: step.builder.config.DropCapabilities, 329 PostExec: step.builder.postExecutor, 330 Env: step.builder.env, 331 } 332 333 opts.OnStart = func(containerID string) error { 334 setStandardPerms := func(writer io.Writer) s2itar.Writer { 335 return s2itar.ChmodAdapter{Writer: tar.NewWriter(writer), NewFileMode: 0644, NewExecFileMode: 0755, NewDirMode: 0755} 336 } 337 338 glog.V(5).Infof("Uploading directory %q -> %q", artifactsDir, workDir) 339 onStartErr := step.docker.UploadToContainerWithTarWriter(step.fs, artifactsDir, workDir, containerID, setStandardPerms) 340 if onStartErr != nil { 341 return fmt.Errorf("could not upload directory (%q -> %q) into container %s: %v", artifactsDir, workDir, containerID, err) 342 } 343 344 glog.V(5).Infof("Uploading file %q -> %q", lastFilePath, lastFileDstPath) 345 onStartErr = step.docker.UploadToContainerWithTarWriter(step.fs, lastFilePath, lastFileDstPath, containerID, setStandardPerms) 346 if onStartErr != nil { 347 return fmt.Errorf("could not upload file (%q -> %q) into container %s: %v", lastFilePath, lastFileDstPath, containerID, err) 348 } 349 350 return onStartErr 351 } 352 353 dockerpkg.StreamContainerIO(outReader, nil, func(s string) { glog.V(0).Info(s) }) 354 355 errOutput := "" 356 c := dockerpkg.StreamContainerIO(errReader, &errOutput, func(s string) { glog.Info(s) }) 357 358 // switch to the next stage of post executors steps 359 step.builder.postExecutorStage++ 360 361 err = step.docker.RunContainer(opts) 362 if e, ok := err.(s2ierr.ContainerError); ok { 363 // Must wait for StreamContainerIO goroutine above to exit before reading errOutput. 364 <-c 365 err = s2ierr.NewContainerError(image, e.ErrorCode, errOutput+e.Output) 366 } 367 368 return err 369 } 370 371 func (step *startRuntimeImageAndUploadFilesStep) copyScriptIfNeeded(script, destinationDir string) error { 372 useExternalScript := step.builder.externalScripts[script] 373 if useExternalScript { 374 src := filepath.Join(step.builder.config.WorkingDir, constants.UploadScripts, script) 375 dst := filepath.Join(destinationDir, script) 376 glog.V(5).Infof("Copying file %q -> %q", src, dst) 377 if err := step.fs.MkdirAll(destinationDir); err != nil { 378 return fmt.Errorf("could not create directory %q: %v", destinationDir, err) 379 } 380 if err := step.fs.Copy(src, dst); err != nil { 381 return fmt.Errorf("could not copy file (%q -> %q): %v", src, dst, err) 382 } 383 } 384 return nil 385 } 386 387 type reportSuccessStep struct { 388 builder *STI 389 } 390 391 func (step *reportSuccessStep) execute(ctx *postExecutorStepContext) error { 392 glog.V(3).Info("Executing step: report success") 393 394 step.builder.result.Success = true 395 396 glog.V(3).Infof("Successfully built %s", utils.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 := utils.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[constants.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", constants.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 }