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  }