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