github.com/openshift/source-to-image@v1.4.1-0.20240516041539-bf52fc02204e/pkg/build/strategies/sti/sti.go (about) 1 package sti 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "path" 10 "path/filepath" 11 "regexp" 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 "github.com/openshift/source-to-image/pkg/build" 18 "github.com/openshift/source-to-image/pkg/build/strategies/layered" 19 dockerpkg "github.com/openshift/source-to-image/pkg/docker" 20 s2ierr "github.com/openshift/source-to-image/pkg/errors" 21 "github.com/openshift/source-to-image/pkg/ignore" 22 "github.com/openshift/source-to-image/pkg/scm" 23 "github.com/openshift/source-to-image/pkg/scm/git" 24 "github.com/openshift/source-to-image/pkg/scripts" 25 "github.com/openshift/source-to-image/pkg/tar" 26 "github.com/openshift/source-to-image/pkg/util" 27 "github.com/openshift/source-to-image/pkg/util/cmd" 28 "github.com/openshift/source-to-image/pkg/util/fs" 29 utillog "github.com/openshift/source-to-image/pkg/util/log" 30 utilstatus "github.com/openshift/source-to-image/pkg/util/status" 31 ) 32 33 const ( 34 injectionResultFile = "/tmp/injection-result" 35 rmInjectionsScript = "/tmp/rm-injections" 36 ) 37 38 var ( 39 log = utillog.StderrLog 40 41 // List of directories that needs to be present inside working dir 42 workingDirs = []string{ 43 constants.UploadScripts, 44 constants.Source, 45 constants.DefaultScripts, 46 constants.UserScripts, 47 } 48 49 errMissingRequirements = errors.New("missing requirements") 50 ) 51 52 // STI strategy executes the S2I build. 53 // For more details about S2I, visit https://github.com/openshift/source-to-image 54 type STI struct { 55 config *api.Config 56 result *api.Result 57 postExecutor dockerpkg.PostExecutor 58 installer scripts.Installer 59 runtimeInstaller scripts.Installer 60 git git.Git 61 fs fs.FileSystem 62 tar tar.Tar 63 docker dockerpkg.Docker 64 incrementalDocker dockerpkg.Docker 65 runtimeDocker dockerpkg.Docker 66 callbackInvoker util.CallbackInvoker 67 requiredScripts []string 68 optionalScripts []string 69 optionalRuntimeScripts []string 70 externalScripts map[string]bool 71 installedScripts map[string]bool 72 scriptsURL map[string]string 73 incremental bool 74 sourceInfo *git.SourceInfo 75 env []string 76 newLabels map[string]string 77 78 // Interfaces 79 preparer build.Preparer 80 ignorer build.Ignorer 81 artifacts build.IncrementalBuilder 82 scripts build.ScriptsHandler 83 source build.Downloader 84 garbage build.Cleaner 85 layered build.Builder 86 87 // post executors steps 88 postExecutorStage int 89 postExecutorFirstStageSteps []postExecutorStep 90 postExecutorSecondStageSteps []postExecutorStep 91 postExecutorStepsContext *postExecutorStepContext 92 } 93 94 // New returns the instance of STI builder strategy for the given config. 95 // If the layeredBuilder parameter is specified, then the builder provided will 96 // be used for the case that the base Docker image does not have 'tar' or 'bash' 97 // installed. 98 func New(client dockerpkg.Client, config *api.Config, fs fs.FileSystem, overrides build.Overrides) (*STI, error) { 99 excludePattern, err := regexp.Compile(config.ExcludeRegExp) 100 if err != nil { 101 return nil, err 102 } 103 104 docker := dockerpkg.New(client, config.PullAuthentication) 105 var incrementalDocker dockerpkg.Docker 106 if config.Incremental { 107 incrementalDocker = dockerpkg.New(client, config.IncrementalAuthentication) 108 } 109 110 inst := scripts.NewInstaller( 111 config.BuilderImage, 112 config.ScriptsURL, 113 config.ScriptDownloadProxyConfig, 114 docker, 115 config.PullAuthentication, 116 fs, 117 config, 118 ) 119 tarHandler := tar.NewParanoid(fs) 120 tarHandler.SetExclusionPattern(excludePattern) 121 122 builder := &STI{ 123 installer: inst, 124 config: config, 125 docker: docker, 126 incrementalDocker: incrementalDocker, 127 git: git.New(fs, cmd.NewCommandRunner()), 128 fs: fs, 129 tar: tarHandler, 130 callbackInvoker: util.NewCallbackInvoker(), 131 requiredScripts: scripts.RequiredScripts, 132 optionalScripts: scripts.OptionalScripts, 133 optionalRuntimeScripts: []string{constants.AssembleRuntime}, 134 externalScripts: map[string]bool{}, 135 installedScripts: map[string]bool{}, 136 scriptsURL: map[string]string{}, 137 newLabels: map[string]string{}, 138 } 139 140 if len(config.RuntimeImage) > 0 { 141 builder.runtimeDocker = dockerpkg.New(client, config.RuntimeAuthentication) 142 143 builder.runtimeInstaller = scripts.NewInstaller( 144 config.RuntimeImage, 145 config.ScriptsURL, 146 config.ScriptDownloadProxyConfig, 147 builder.runtimeDocker, 148 config.RuntimeAuthentication, 149 builder.fs, 150 config, 151 ) 152 } 153 154 // The sources are downloaded using the Git downloader. 155 // TODO: Add more SCM in future. 156 // TODO: explicit decision made to customize processing for usage specifically vs. 157 // leveraging overrides; also, we ultimately want to simplify s2i usage a good bit, 158 // which would lead to replacing this quick short circuit (so this change is tactical) 159 builder.source = overrides.Downloader 160 if builder.source == nil && !config.Usage { 161 downloader, err := scm.DownloaderForSource(builder.fs, config.Source, config.ForceCopy) 162 if err != nil { 163 return nil, err 164 } 165 builder.source = downloader 166 } 167 builder.garbage = build.NewDefaultCleaner(builder.fs, builder.docker) 168 169 builder.layered, err = layered.New(client, config, builder.fs, builder, overrides) 170 if err != nil { 171 return nil, err 172 } 173 174 // Set interfaces 175 builder.preparer = builder 176 // later on, if we support say .gitignore func in addition to .dockerignore 177 // func, setting ignorer will be based on config setting 178 builder.ignorer = &ignore.DockerIgnorer{} 179 builder.artifacts = builder 180 builder.scripts = builder 181 builder.postExecutor = builder 182 builder.initPostExecutorSteps() 183 184 return builder, nil 185 } 186 187 // Build processes a Request and returns a *api.Result and an error. 188 // An error represents a failure performing the build rather than a failure 189 // of the build itself. Callers should check the Success field of the result 190 // to determine whether a build succeeded or not. 191 func (builder *STI) Build(config *api.Config) (*api.Result, error) { 192 builder.result = &api.Result{} 193 194 if len(builder.config.CallbackURL) > 0 { 195 defer func() { 196 builder.result.Messages = builder.callbackInvoker.ExecuteCallback( 197 builder.config.CallbackURL, 198 builder.result.Success, 199 builder.postExecutorStepsContext.labels, 200 builder.result.Messages, 201 ) 202 }() 203 } 204 defer builder.garbage.Cleanup(config) 205 206 log.V(1).Infof("Preparing to build %s", config.Tag) 207 if err := builder.preparer.Prepare(config); err != nil { 208 return builder.result, err 209 } 210 211 if builder.incremental = builder.artifacts.Exists(config); builder.incremental { 212 tag := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag) 213 log.V(1).Infof("Existing image for tag %s detected for incremental build", tag) 214 } else { 215 log.V(1).Info("Clean build will be performed") 216 } 217 218 log.V(2).Infof("Performing source build from %s", config.Source) 219 if builder.incremental { 220 if err := builder.artifacts.Save(config); err != nil { 221 log.Warning("Clean build will be performed because of error saving previous build artifacts") 222 log.V(2).Infof("error: %v", err) 223 } 224 } 225 226 if len(config.AssembleUser) > 0 { 227 log.V(1).Infof("Running %q in %q as %q user", constants.Assemble, config.Tag, config.AssembleUser) 228 } else { 229 log.V(1).Infof("Running %q in %q", constants.Assemble, config.Tag) 230 } 231 startTime := time.Now() 232 if err := builder.scripts.Execute(constants.Assemble, config.AssembleUser, config); err != nil { 233 if err == errMissingRequirements { 234 log.V(1).Info("Image is missing basic requirements (sh or tar), layered build will be performed") 235 return builder.layered.Build(config) 236 } 237 if e, ok := err.(s2ierr.ContainerError); ok { 238 if !isMissingRequirements(e.Output) { 239 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 240 utilstatus.ReasonAssembleFailed, 241 utilstatus.ReasonMessageAssembleFailed, 242 ) 243 return builder.result, err 244 } 245 log.V(1).Info("Image is missing basic requirements (sh or tar), layered build will be performed") 246 buildResult, err := builder.layered.Build(config) 247 return buildResult, err 248 } 249 250 return builder.result, err 251 } 252 builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(builder.result.BuildInfo.Stages, api.StageAssemble, api.StepAssembleBuildScripts, startTime, time.Now()) 253 builder.result.Success = true 254 255 return builder.result, nil 256 } 257 258 // Prepare prepares the source code and tar for build. 259 // NOTE: this func serves both the sti and onbuild strategies, as the OnBuild 260 // struct Build func leverages the STI struct Prepare func directly below. 261 func (builder *STI) Prepare(config *api.Config) error { 262 var err error 263 if builder.result == nil { 264 builder.result = &api.Result{} 265 } 266 267 if len(config.WorkingDir) == 0 { 268 if config.WorkingDir, err = builder.fs.CreateWorkingDirectory(); err != nil { 269 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 270 utilstatus.ReasonFSOperationFailed, 271 utilstatus.ReasonMessageFSOperationFailed, 272 ) 273 return err 274 } 275 } 276 277 builder.result.WorkingDir = config.WorkingDir 278 279 if len(config.RuntimeImage) > 0 { 280 startTime := time.Now() 281 dockerpkg.GetRuntimeImage(builder.runtimeDocker, config) 282 builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(builder.result.BuildInfo.Stages, api.StagePullImages, api.StepPullRuntimeImage, startTime, time.Now()) 283 284 if err != nil { 285 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 286 utilstatus.ReasonPullRuntimeImageFailed, 287 utilstatus.ReasonMessagePullRuntimeImageFailed, 288 ) 289 log.Errorf("Unable to pull runtime image %q: %v", config.RuntimeImage, err) 290 return err 291 } 292 293 // user didn't specify mapping, let's take it from the runtime image then 294 if len(builder.config.RuntimeArtifacts) == 0 { 295 var mapping string 296 mapping, err = builder.docker.GetAssembleInputFiles(config.RuntimeImage) 297 if err != nil { 298 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 299 utilstatus.ReasonInvalidArtifactsMapping, 300 utilstatus.ReasonMessageInvalidArtifactsMapping, 301 ) 302 return err 303 } 304 if len(mapping) == 0 { 305 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 306 utilstatus.ReasonGenericS2IBuildFailed, 307 utilstatus.ReasonMessageGenericS2iBuildFailed, 308 ) 309 return errors.New("no runtime artifacts to copy were specified") 310 } 311 for _, value := range strings.Split(mapping, ";") { 312 if err = builder.config.RuntimeArtifacts.Set(value); err != nil { 313 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 314 utilstatus.ReasonGenericS2IBuildFailed, 315 utilstatus.ReasonMessageGenericS2iBuildFailed, 316 ) 317 return fmt.Errorf("could not parse %q label with value %q on image %q: %v", 318 constants.AssembleInputFilesLabel, mapping, config.RuntimeImage, err) 319 } 320 } 321 } 322 323 if len(config.AssembleRuntimeUser) == 0 { 324 if config.AssembleRuntimeUser, err = builder.docker.GetAssembleRuntimeUser(config.RuntimeImage); err != nil { 325 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 326 utilstatus.ReasonGenericS2IBuildFailed, 327 utilstatus.ReasonMessageGenericS2iBuildFailed, 328 ) 329 return fmt.Errorf("could not get %q label value on image %q: %v", 330 constants.AssembleRuntimeUserLabel, config.RuntimeImage, err) 331 } 332 } 333 334 // we're validating values here to be sure that we're handling both of the cases of the invocation: 335 // from main() and as a method from OpenShift 336 for _, volumeSpec := range builder.config.RuntimeArtifacts { 337 var volumeErr error 338 339 switch { 340 case !path.IsAbs(filepath.ToSlash(volumeSpec.Source)): 341 volumeErr = fmt.Errorf("invalid runtime artifacts mapping: %q -> %q: source must be an absolute path", volumeSpec.Source, volumeSpec.Destination) 342 case path.IsAbs(volumeSpec.Destination): 343 volumeErr = fmt.Errorf("invalid runtime artifacts mapping: %q -> %q: destination must be a relative path", volumeSpec.Source, volumeSpec.Destination) 344 case strings.HasPrefix(volumeSpec.Destination, ".."): 345 volumeErr = fmt.Errorf("invalid runtime artifacts mapping: %q -> %q: destination cannot start with '..'", volumeSpec.Source, volumeSpec.Destination) 346 default: 347 continue 348 } 349 if volumeErr != nil { 350 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 351 utilstatus.ReasonInvalidArtifactsMapping, 352 utilstatus.ReasonMessageInvalidArtifactsMapping, 353 ) 354 return volumeErr 355 } 356 } 357 } 358 359 // Setup working directories 360 for _, v := range workingDirs { 361 if err = builder.fs.MkdirAllWithPermissions(filepath.Join(config.WorkingDir, v), 0755); err != nil { 362 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 363 utilstatus.ReasonFSOperationFailed, 364 utilstatus.ReasonMessageFSOperationFailed, 365 ) 366 return err 367 } 368 } 369 370 // fetch sources, for their .s2i/bin might contain s2i scripts 371 if config.Source != nil { 372 if builder.sourceInfo, err = builder.source.Download(config); err != nil { 373 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 374 utilstatus.ReasonFetchSourceFailed, 375 utilstatus.ReasonMessageFetchSourceFailed, 376 ) 377 return err 378 } 379 if config.SourceInfo != nil { 380 builder.sourceInfo = config.SourceInfo 381 } 382 } 383 384 // get the scripts 385 required, err := builder.installer.InstallRequired(builder.requiredScripts, config.WorkingDir) 386 if err != nil { 387 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 388 utilstatus.ReasonInstallScriptsFailed, 389 utilstatus.ReasonMessageInstallScriptsFailed, 390 ) 391 return err 392 } 393 optional := builder.installer.InstallOptional(builder.optionalScripts, config.WorkingDir) 394 395 requiredAndOptional := append(required, optional...) 396 397 if len(config.RuntimeImage) > 0 && builder.runtimeInstaller != nil { 398 optionalRuntime := builder.runtimeInstaller.InstallOptional(builder.optionalRuntimeScripts, config.WorkingDir) 399 requiredAndOptional = append(requiredAndOptional, optionalRuntime...) 400 } 401 402 // If a ScriptsURL was specified, but no scripts were downloaded from it, throw an error 403 if len(config.ScriptsURL) > 0 { 404 failedCount := 0 405 for _, result := range requiredAndOptional { 406 if util.Includes(result.FailedSources, scripts.ScriptURLHandler) { 407 failedCount++ 408 } 409 } 410 if failedCount == len(requiredAndOptional) { 411 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 412 utilstatus.ReasonScriptsFetchFailed, 413 utilstatus.ReasonMessageScriptsFetchFailed, 414 ) 415 return fmt.Errorf("could not download any scripts from URL %v", config.ScriptsURL) 416 } 417 } 418 419 for _, r := range requiredAndOptional { 420 if r.Error != nil { 421 log.Warningf("Error getting %v from %s: %v", r.Script, r.URL, r.Error) 422 continue 423 } 424 425 builder.externalScripts[r.Script] = r.Downloaded 426 builder.installedScripts[r.Script] = r.Installed 427 builder.scriptsURL[r.Script] = r.URL 428 } 429 430 // see if there is a .s2iignore file, and if so, read in the patterns an then 431 // search and delete on 432 return builder.ignorer.Ignore(config) 433 } 434 435 // SetScripts allows to override default required and optional scripts 436 func (builder *STI) SetScripts(required, optional []string) { 437 builder.requiredScripts = required 438 builder.optionalScripts = optional 439 } 440 441 // PostExecute allows to execute post-build actions after the Docker 442 // container execution finishes. 443 func (builder *STI) PostExecute(containerID, destination string) error { 444 builder.postExecutorStepsContext.containerID = containerID 445 builder.postExecutorStepsContext.destination = destination 446 447 stageSteps := builder.postExecutorFirstStageSteps 448 if builder.postExecutorStage > 0 { 449 stageSteps = builder.postExecutorSecondStageSteps 450 } 451 452 for _, step := range stageSteps { 453 if err := step.execute(builder.postExecutorStepsContext); err != nil { 454 log.V(0).Info("error: Execution of post execute step failed") 455 return err 456 } 457 } 458 459 return nil 460 } 461 462 // CreateBuildEnvironment constructs the environment variables to be provided to the assemble 463 // script and committed in the new image. 464 func CreateBuildEnvironment(sourcePath string, cfgEnv api.EnvironmentList) []string { 465 s2iEnv, err := scripts.GetEnvironment(filepath.Join(sourcePath, constants.Source)) 466 if err != nil { 467 log.V(3).Infof("No user environment provided (%v)", err) 468 } 469 470 return append(scripts.ConvertEnvironmentList(s2iEnv), scripts.ConvertEnvironmentList(cfgEnv)...) 471 } 472 473 // Exists determines if the current build supports incremental workflow. 474 // It checks if the previous image exists in the system and if so, then it 475 // verifies that the save-artifacts script is present. 476 func (builder *STI) Exists(config *api.Config) bool { 477 if !config.Incremental { 478 return false 479 } 480 481 policy := config.PreviousImagePullPolicy 482 if len(policy) == 0 { 483 policy = api.DefaultPreviousImagePullPolicy 484 } 485 486 tag := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag) 487 488 startTime := time.Now() 489 result, err := dockerpkg.PullImage(tag, builder.incrementalDocker, policy) 490 builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(builder.result.BuildInfo.Stages, api.StagePullImages, api.StepPullPreviousImage, startTime, time.Now()) 491 492 if err != nil { 493 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 494 utilstatus.ReasonPullPreviousImageFailed, 495 utilstatus.ReasonMessagePullPreviousImageFailed, 496 ) 497 log.V(2).Infof("Unable to pull previously built image %q: %v", tag, err) 498 return false 499 } 500 501 return result.Image != nil && builder.installedScripts[constants.SaveArtifacts] 502 } 503 504 // Save extracts and restores the build artifacts from the previous build to 505 // the current build. 506 func (builder *STI) Save(config *api.Config) (err error) { 507 artifactTmpDir := filepath.Join(config.WorkingDir, "upload", "artifacts") 508 if builder.result == nil { 509 builder.result = &api.Result{} 510 } 511 512 if err = builder.fs.Mkdir(artifactTmpDir); err != nil { 513 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 514 utilstatus.ReasonFSOperationFailed, 515 utilstatus.ReasonMessageFSOperationFailed, 516 ) 517 return err 518 } 519 520 image := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag) 521 522 outReader, outWriter := io.Pipe() 523 errReader, errWriter := io.Pipe() 524 log.V(1).Infof("Saving build artifacts from image %s to path %s", image, artifactTmpDir) 525 extractFunc := func(string) error { 526 startTime := time.Now() 527 extractErr := builder.tar.ExtractTarStream(artifactTmpDir, outReader) 528 io.Copy(ioutil.Discard, outReader) // must ensure reader from container is drained 529 builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(builder.result.BuildInfo.Stages, api.StageRetrieve, api.StepRetrievePreviousArtifacts, startTime, time.Now()) 530 531 if extractErr != nil { 532 builder.fs.RemoveDirectory(artifactTmpDir) 533 } 534 535 return extractErr 536 } 537 538 user := config.AssembleUser 539 if len(user) == 0 { 540 user, err = builder.docker.GetImageUser(image) 541 if err != nil { 542 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 543 utilstatus.ReasonGenericS2IBuildFailed, 544 utilstatus.ReasonMessageGenericS2iBuildFailed, 545 ) 546 return err 547 } 548 log.V(3).Infof("The assemble user is not set, defaulting to %q user", user) 549 } else { 550 log.V(3).Infof("Using assemble user %q to extract artifacts", user) 551 } 552 553 opts := dockerpkg.RunContainerOptions{ 554 Image: image, 555 User: user, 556 ExternalScripts: builder.externalScripts[constants.SaveArtifacts], 557 ScriptsURL: config.ScriptsURL, 558 Destination: config.Destination, 559 PullImage: false, 560 Command: constants.SaveArtifacts, 561 Stdout: outWriter, 562 Stderr: errWriter, 563 OnStart: extractFunc, 564 NetworkMode: string(config.DockerNetworkMode), 565 CGroupLimits: config.CGroupLimits, 566 CapDrop: config.DropCapabilities, 567 Binds: config.BuildVolumes, 568 SecurityOpt: config.SecurityOpt, 569 AddHost: config.AddHost, 570 } 571 572 dockerpkg.StreamContainerIO(errReader, nil, func(s string) { log.Info(s) }) 573 err = builder.docker.RunContainer(opts) 574 if e, ok := err.(s2ierr.ContainerError); ok { 575 err = s2ierr.NewSaveArtifactsError(image, e.Output, err) 576 } 577 578 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 579 utilstatus.ReasonGenericS2IBuildFailed, 580 utilstatus.ReasonMessageGenericS2iBuildFailed, 581 ) 582 return err 583 } 584 585 // Execute runs the specified STI script in the builder image. 586 func (builder *STI) Execute(command string, user string, config *api.Config) error { 587 log.V(2).Infof("Using image name %s", config.BuilderImage) 588 589 // Ensure that the builder image is present in the local Docker daemon. 590 // The image should have been pulled when the strategy was created, so 591 // this should be a quick inspect of the existing image. However, if 592 // the image has been deleted since the strategy was created, this will ensure 593 // it exists before executing a script on it. 594 builder.docker.CheckAndPullImage(config.BuilderImage) 595 596 // we can't invoke this method before (for example in New() method) 597 // because of later initialization of config.WorkingDir 598 builder.env = CreateBuildEnvironment(config.WorkingDir, config.Environment) 599 600 errOutput := "" 601 outReader, outWriter := io.Pipe() 602 errReader, errWriter := io.Pipe() 603 externalScripts := builder.externalScripts[command] 604 // if LayeredBuild is called then all the scripts will be placed inside the image 605 if config.LayeredBuild { 606 externalScripts = false 607 } 608 609 opts := dockerpkg.RunContainerOptions{ 610 Image: config.BuilderImage, 611 Stdout: outWriter, 612 Stderr: errWriter, 613 // The PullImage is false because the PullImage function should be called 614 // before we run the container 615 PullImage: false, 616 ExternalScripts: externalScripts, 617 ScriptsURL: config.ScriptsURL, 618 Destination: config.Destination, 619 Command: command, 620 Env: builder.env, 621 User: user, 622 PostExec: builder.postExecutor, 623 NetworkMode: string(config.DockerNetworkMode), 624 CGroupLimits: config.CGroupLimits, 625 CapDrop: config.DropCapabilities, 626 Binds: config.BuildVolumes, 627 SecurityOpt: config.SecurityOpt, 628 AddHost: config.AddHost, 629 } 630 631 // If there are injections specified, override the original assemble script 632 // and wait till all injections are uploaded into the container that runs the 633 // assemble script. 634 injectionError := make(chan error) 635 if len(config.Injections) > 0 && command == constants.Assemble { 636 workdir, err := builder.docker.GetImageWorkdir(config.BuilderImage) 637 if err != nil { 638 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 639 utilstatus.ReasonGenericS2IBuildFailed, 640 utilstatus.ReasonMessageGenericS2iBuildFailed, 641 ) 642 return err 643 } 644 config.Injections = util.FixInjectionsWithRelativePath(workdir, config.Injections) 645 truncatedFiles, err := util.ListFilesToTruncate(builder.fs, config.Injections) 646 if err != nil { 647 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 648 utilstatus.ReasonInstallScriptsFailed, 649 utilstatus.ReasonMessageInstallScriptsFailed, 650 ) 651 return err 652 } 653 rmScript, err := util.CreateTruncateFilesScript(truncatedFiles, rmInjectionsScript) 654 if len(rmScript) != 0 { 655 defer os.Remove(rmScript) 656 } 657 if err != nil { 658 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 659 utilstatus.ReasonGenericS2IBuildFailed, 660 utilstatus.ReasonMessageGenericS2iBuildFailed, 661 ) 662 return err 663 } 664 opts.CommandOverrides = func(cmd string) string { 665 // If an s2i build has injections, the s2i container's main command must be altered to 666 // do the following: 667 // 1) Wait for the injections to be uploaded 668 // 2) Check if there were any errors uploading the injections 669 // 3) Run the injection removal script after `assemble` completes 670 // 671 // The injectionResultFile should always be uploaded to the s2i container after the 672 // injected volumes are added. If this file is non-empty, it indicates that an error 673 // occurred during the injection process and the s2i build should fail. 674 return fmt.Sprintf("while [ ! -f %[1]q ]; do sleep 0.5; done; if [ -s %[1]q ]; then exit 1; fi; %[2]s; result=$?; . %[3]s; exit $result", 675 injectionResultFile, cmd, rmInjectionsScript) 676 } 677 originalOnStart := opts.OnStart 678 opts.OnStart = func(containerID string) error { 679 defer close(injectionError) 680 injectErr := builder.uploadInjections(config, rmScript, containerID) 681 if err := builder.uploadInjectionResult(injectErr, containerID); err != nil { 682 injectionError <- err 683 return err 684 } 685 if originalOnStart != nil { 686 return originalOnStart(containerID) 687 } 688 return nil 689 } 690 } else { 691 close(injectionError) 692 } 693 694 if !config.LayeredBuild { 695 r, w := io.Pipe() 696 opts.Stdin = r 697 698 go func() { 699 // Wait for the injections to complete and check the error. Do not start 700 // streaming the sources when the injection failed. 701 if <-injectionError != nil { 702 w.Close() 703 return 704 } 705 log.V(2).Info("starting the source uploading ...") 706 uploadDir := filepath.Join(config.WorkingDir, "upload") 707 w.CloseWithError(builder.tar.CreateTarStream(uploadDir, false, w)) 708 }() 709 } 710 711 dockerpkg.StreamContainerIO(outReader, nil, func(s string) { 712 if !config.Quiet { 713 log.Info(strings.TrimSpace(s)) 714 } 715 }) 716 717 c := dockerpkg.StreamContainerIO(errReader, &errOutput, func(s string) { log.Info(s) }) 718 719 err := builder.docker.RunContainer(opts) 720 if err != nil { 721 // Must wait for StreamContainerIO goroutine above to exit before reading errOutput. 722 <-c 723 724 if isMissingRequirements(errOutput) { 725 err = errMissingRequirements 726 } else if e, ok := err.(s2ierr.ContainerError); ok { 727 err = s2ierr.NewContainerError(config.BuilderImage, e.ErrorCode, errOutput+e.Output) 728 } 729 } 730 731 return err 732 } 733 734 // uploadInjections uploads the injected volumes to the s2i container, along with the source 735 // removal script to truncate volumes that should not be kept. 736 func (builder *STI) uploadInjections(config *api.Config, rmScript, containerID string) error { 737 log.V(2).Info("starting the injections uploading ...") 738 for _, s := range config.Injections { 739 if err := builder.docker.UploadToContainer(builder.fs, s.Source, s.Destination, containerID); err != nil { 740 return util.HandleInjectionError(s, err) 741 } 742 } 743 if err := builder.docker.UploadToContainer(builder.fs, rmScript, rmInjectionsScript, containerID); err != nil { 744 return util.HandleInjectionError(api.VolumeSpec{Source: rmScript, Destination: rmInjectionsScript}, err) 745 } 746 return nil 747 } 748 749 func (builder *STI) initPostExecutorSteps() { 750 builder.postExecutorStepsContext = &postExecutorStepContext{} 751 if len(builder.config.RuntimeImage) == 0 { 752 builder.postExecutorFirstStageSteps = []postExecutorStep{ 753 &storePreviousImageStep{ 754 builder: builder, 755 docker: builder.docker, 756 }, 757 &commitImageStep{ 758 image: builder.config.BuilderImage, 759 builder: builder, 760 docker: builder.docker, 761 fs: builder.fs, 762 tar: builder.tar, 763 }, 764 &reportSuccessStep{ 765 builder: builder, 766 }, 767 &removePreviousImageStep{ 768 builder: builder, 769 docker: builder.docker, 770 }, 771 } 772 } else { 773 builder.postExecutorFirstStageSteps = []postExecutorStep{ 774 &downloadFilesFromBuilderImageStep{ 775 builder: builder, 776 docker: builder.docker, 777 fs: builder.fs, 778 tar: builder.tar, 779 }, 780 &startRuntimeImageAndUploadFilesStep{ 781 builder: builder, 782 docker: builder.docker, 783 fs: builder.fs, 784 }, 785 } 786 builder.postExecutorSecondStageSteps = []postExecutorStep{ 787 &commitImageStep{ 788 image: builder.config.RuntimeImage, 789 builder: builder, 790 docker: builder.docker, 791 tar: builder.tar, 792 }, 793 &reportSuccessStep{ 794 builder: builder, 795 }, 796 } 797 } 798 } 799 800 // uploadInjectionResult uploads a result file to the s2i container, indicating 801 // that the injections have completed. If a non-nil error is passed in, it is returned 802 // to ensure the error status of the injection upload is reported. 803 func (builder *STI) uploadInjectionResult(startErr error, containerID string) error { 804 resultFile, err := util.CreateInjectionResultFile(startErr) 805 if len(resultFile) > 0 { 806 defer os.Remove(resultFile) 807 } 808 if err != nil { 809 return err 810 } 811 err = builder.docker.UploadToContainer(builder.fs, resultFile, injectionResultFile, containerID) 812 if err != nil { 813 return util.HandleInjectionError(api.VolumeSpec{Source: resultFile, Destination: injectionResultFile}, err) 814 } 815 return startErr 816 } 817 818 func isMissingRequirements(text string) bool { 819 tarCommand, _ := regexp.MatchString(`.*tar.*not found`, text) 820 shCommand, _ := regexp.MatchString(`.*/bin/sh.*no such file or directory`, text) 821 return tarCommand || shCommand 822 }