github.com/openshift/source-to-image@v1.4.1-0.20240516041539-bf52fc02204e/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 "github.com/openshift/source-to-image/pkg/api/constants" 17 dockerpkg "github.com/openshift/source-to-image/pkg/docker" 18 s2ierr "github.com/openshift/source-to-image/pkg/errors" 19 s2itar "github.com/openshift/source-to-image/pkg/tar" 20 "github.com/openshift/source-to-image/pkg/util" 21 "github.com/openshift/source-to-image/pkg/util/fs" 22 utilstatus "github.com/openshift/source-to-image/pkg/util/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 log.V(3).Info("Executing step: store previous image") 64 ctx.previousImageID = step.getPreviousImage() 65 return nil 66 } 67 68 log.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 log.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 log.V(3).Info("Executing step: remove previous image") 89 step.removePreviousImage(ctx.previousImageID) 90 return nil 91 } 92 93 log.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 log.V(1).Infof("Removing previously-tagged image %s", previousImageID) 103 if err := step.docker.RemoveImage(previousImageID); err != nil { 104 log.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 log.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 log.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 log.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 log.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 log.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 User: step.builder.config.AssembleRuntimeUser, 332 } 333 334 opts.OnStart = func(containerID string) error { 335 setStandardPerms := func(writer io.Writer) s2itar.Writer { 336 return s2itar.ChmodAdapter{Writer: tar.NewWriter(writer), NewFileMode: 0644, NewExecFileMode: 0755, NewDirMode: 0755} 337 } 338 339 log.V(5).Infof("Uploading directory %q -> %q", artifactsDir, workDir) 340 onStartErr := step.docker.UploadToContainerWithTarWriter(step.fs, artifactsDir, workDir, containerID, setStandardPerms) 341 if onStartErr != nil { 342 return fmt.Errorf("could not upload directory (%q -> %q) into container %s: %v", artifactsDir, workDir, containerID, err) 343 } 344 345 log.V(5).Infof("Uploading file %q -> %q", lastFilePath, lastFileDstPath) 346 onStartErr = step.docker.UploadToContainerWithTarWriter(step.fs, lastFilePath, lastFileDstPath, containerID, setStandardPerms) 347 if onStartErr != nil { 348 return fmt.Errorf("could not upload file (%q -> %q) into container %s: %v", lastFilePath, lastFileDstPath, containerID, err) 349 } 350 351 return onStartErr 352 } 353 354 dockerpkg.StreamContainerIO(outReader, nil, func(s string) { log.V(0).Info(s) }) 355 356 errOutput := "" 357 c := dockerpkg.StreamContainerIO(errReader, &errOutput, func(s string) { log.Info(s) }) 358 359 // switch to the next stage of post executors steps 360 step.builder.postExecutorStage++ 361 362 err = step.docker.RunContainer(opts) 363 if e, ok := err.(s2ierr.ContainerError); ok { 364 // Must wait for StreamContainerIO goroutine above to exit before reading errOutput. 365 <-c 366 err = s2ierr.NewContainerError(image, e.ErrorCode, errOutput+e.Output) 367 } 368 369 return err 370 } 371 372 func (step *startRuntimeImageAndUploadFilesStep) copyScriptIfNeeded(script, destinationDir string) error { 373 useExternalScript := step.builder.externalScripts[script] 374 if useExternalScript { 375 src := filepath.Join(step.builder.config.WorkingDir, constants.UploadScripts, script) 376 dst := filepath.Join(destinationDir, script) 377 log.V(5).Infof("Copying file %q -> %q", src, dst) 378 if err := step.fs.MkdirAll(destinationDir); err != nil { 379 return fmt.Errorf("could not create directory %q: %v", destinationDir, err) 380 } 381 if err := step.fs.Copy(src, dst, nil); err != nil { 382 return fmt.Errorf("could not copy file (%q -> %q): %v", src, dst, err) 383 } 384 } 385 return nil 386 } 387 388 type reportSuccessStep struct { 389 builder *STI 390 } 391 392 func (step *reportSuccessStep) execute(ctx *postExecutorStepContext) error { 393 log.V(3).Info("Executing step: report success") 394 395 step.builder.result.Success = true 396 step.builder.result.ImageID = ctx.imageID 397 398 log.V(3).Infof("Successfully built %s", util.FirstNonEmpty(step.builder.config.Tag, ctx.imageID)) 399 400 return nil 401 } 402 403 // shared methods 404 405 func commitContainer(docker dockerpkg.Docker, containerID, cmd, user, tag string, env, entrypoint []string, labels map[string]string) (string, error) { 406 opts := dockerpkg.CommitContainerOptions{ 407 Command: []string{cmd}, 408 Env: env, 409 Entrypoint: entrypoint, 410 ContainerID: containerID, 411 Repository: tag, 412 User: user, 413 Labels: labels, 414 } 415 416 imageID, err := docker.CommitContainer(opts) 417 if err != nil { 418 return "", s2ierr.NewCommitError(tag, err) 419 } 420 421 return imageID, nil 422 } 423 424 func createLabelsForResultingImage(builder *STI, docker dockerpkg.Docker, baseImage string) map[string]string { 425 generatedLabels := util.GenerateOutputImageLabels(builder.sourceInfo, builder.config) 426 427 existingLabels, err := docker.GetLabels(baseImage) 428 if err != nil { 429 log.V(0).Infof("error: Unable to read existing labels from the base image %s", baseImage) 430 } 431 432 configLabels := builder.config.Labels 433 newLabels := builder.newLabels 434 435 return mergeLabels(existingLabels, generatedLabels, configLabels, newLabels) 436 } 437 438 func mergeLabels(labels ...map[string]string) map[string]string { 439 mergedLabels := map[string]string{} 440 441 for _, labelMap := range labels { 442 for k, v := range labelMap { 443 mergedLabels[k] = v 444 } 445 } 446 return mergedLabels 447 } 448 449 func createCommandForExecutingRunScript(scriptsURL map[string]string, location string) string { 450 cmd := scriptsURL[constants.Run] 451 if strings.HasPrefix(cmd, "image://") { 452 // scripts from inside of the image, we need to strip the image part 453 // NOTE: We use path.Join instead of filepath.Join to avoid converting the 454 // path to UNC (Windows) format as we always run this inside container. 455 cmd = strings.TrimPrefix(cmd, "image://") 456 } else { 457 // external scripts, in which case we're taking the directory to which they 458 // were extracted and append scripts dir and name 459 cmd = path.Join(location, "scripts", constants.Run) 460 } 461 return cmd 462 } 463 464 func downloadAndExtractFileFromContainer(docker dockerpkg.Docker, tar s2itar.Tar, sourcePath, destinationPath, containerID string) (api.FailureReason, error) { 465 log.V(5).Infof("Downloading file %q", sourcePath) 466 467 fd, err := ioutil.TempFile(destinationPath, "s2i-runtime-artifact") 468 if err != nil { 469 res := utilstatus.NewFailureReason( 470 utilstatus.ReasonFSOperationFailed, 471 utilstatus.ReasonMessageFSOperationFailed, 472 ) 473 return res, fmt.Errorf("could not create temporary file for runtime artifact: %v", err) 474 } 475 defer func() { 476 fd.Close() 477 os.Remove(fd.Name()) 478 }() 479 480 if err := docker.DownloadFromContainer(sourcePath, fd, containerID); err != nil { 481 res := utilstatus.NewFailureReason( 482 utilstatus.ReasonGenericS2IBuildFailed, 483 utilstatus.ReasonMessageGenericS2iBuildFailed, 484 ) 485 return res, fmt.Errorf("could not download file (%q -> %q) from container %s: %v", sourcePath, fd.Name(), containerID, err) 486 } 487 488 // after writing to the file descriptor we need to rewind pointer to the beginning of the file before next reading 489 if _, err := fd.Seek(0, io.SeekStart); err != nil { 490 res := utilstatus.NewFailureReason( 491 utilstatus.ReasonGenericS2IBuildFailed, 492 utilstatus.ReasonMessageGenericS2iBuildFailed, 493 ) 494 return res, fmt.Errorf("could not seek to the beginning of the file %q: %v", fd.Name(), err) 495 } 496 497 if err := tar.ExtractTarStream(destinationPath, fd); err != nil { 498 res := utilstatus.NewFailureReason( 499 utilstatus.ReasonGenericS2IBuildFailed, 500 utilstatus.ReasonMessageGenericS2iBuildFailed, 501 ) 502 return res, fmt.Errorf("could not extract artifact %q into the directory %q: %v", sourcePath, destinationPath, err) 503 } 504 505 return utilstatus.NewFailureReason("", ""), nil 506 } 507 508 func checkLabelSize(labels map[string]string) error { 509 var sum = 0 510 for k, v := range labels { 511 sum += len(k) + len(v) 512 } 513 514 if sum > maximumLabelSize { 515 return fmt.Errorf("label size '%d' exceeds the maximum limit '%d'", sum, maximumLabelSize) 516 } 517 518 return nil 519 } 520 521 // check for new labels and apply to the output image. 522 func checkAndGetNewLabels(builder *STI, docker dockerpkg.Docker, tar s2itar.Tar, containerID string) error { 523 log.V(3).Infof("Checking for new Labels to apply... ") 524 525 // metadata filename and its path inside the container 526 metadataFilename := "image_metadata.json" 527 sourceFilepath := filepath.Join("/tmp/.s2i", metadataFilename) 528 529 // create the 'downloadPath' folder if it doesn't exist 530 downloadPath := filepath.Join(builder.config.WorkingDir, "metadata") 531 log.V(3).Infof("Creating the download path '%s'", downloadPath) 532 if err := os.MkdirAll(downloadPath, 0700); err != nil { 533 log.Errorf("Error creating dir %q for '%s': %v", downloadPath, metadataFilename, err) 534 return err 535 } 536 537 // download & extract the file from container 538 if _, err := downloadAndExtractFileFromContainer(docker, tar, sourceFilepath, downloadPath, containerID); err != nil { 539 log.V(3).Infof("unable to download and extract '%s' ... continuing", metadataFilename) 540 return nil 541 } 542 543 // open the file 544 filePath := filepath.Join(downloadPath, metadataFilename) 545 fd, err := os.Open(filePath) 546 if fd == nil || err != nil { 547 return fmt.Errorf("unable to open file '%s' : %v", downloadPath, err) 548 } 549 defer fd.Close() 550 551 // read the file to a string 552 str, err := ioutil.ReadAll(fd) 553 if err != nil { 554 return fmt.Errorf("error reading file '%s' in to a string: %v", filePath, err) 555 } 556 log.V(3).Infof("new Labels File contents : \n%s\n", str) 557 558 // string into a map 559 var data map[string]interface{} 560 561 if err = json.Unmarshal([]byte(str), &data); err != nil { 562 return fmt.Errorf("JSON Unmarshal Error with '%s' file : %v", metadataFilename, err) 563 } 564 565 // update newLabels[] 566 labels := data["labels"] 567 for _, l := range labels.([]interface{}) { 568 for k, v := range l.(map[string]interface{}) { 569 builder.newLabels[k] = v.(string) 570 } 571 } 572 573 return nil 574 }