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