github.com/openshift/source-to-image@v1.4.1-0.20240516041539-bf52fc02204e/pkg/build/strategies/dockerfile/dockerfile.go (about)

     1  package dockerfile
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/openshift/source-to-image/pkg/api"
    13  	"github.com/openshift/source-to-image/pkg/api/constants"
    14  	"github.com/openshift/source-to-image/pkg/build"
    15  	s2ierr "github.com/openshift/source-to-image/pkg/errors"
    16  	"github.com/openshift/source-to-image/pkg/ignore"
    17  	"github.com/openshift/source-to-image/pkg/scm"
    18  	"github.com/openshift/source-to-image/pkg/scm/downloaders/file"
    19  	"github.com/openshift/source-to-image/pkg/scm/git"
    20  	"github.com/openshift/source-to-image/pkg/scripts"
    21  	"github.com/openshift/source-to-image/pkg/util"
    22  	"github.com/openshift/source-to-image/pkg/util/fs"
    23  	utillog "github.com/openshift/source-to-image/pkg/util/log"
    24  	utilstatus "github.com/openshift/source-to-image/pkg/util/status"
    25  	"github.com/openshift/source-to-image/pkg/util/user"
    26  )
    27  
    28  const (
    29  	defaultDestination = "/tmp"
    30  	defaultScriptsDir  = "/usr/libexec/s2i"
    31  )
    32  
    33  var (
    34  	log = utillog.StderrLog
    35  
    36  	// List of directories that needs to be present inside working dir
    37  	workingDirs = []string{
    38  		constants.UploadScripts,
    39  		constants.Source,
    40  		constants.DefaultScripts,
    41  		constants.UserScripts,
    42  	}
    43  )
    44  
    45  // Dockerfile builders produce a Dockerfile rather than an image.
    46  // Building the dockerfile w/ the right context will result in
    47  // an application image being produced.
    48  type Dockerfile struct {
    49  	fs               fs.FileSystem
    50  	uploadScriptsDir string
    51  	uploadSrcDir     string
    52  	sourceInfo       *git.SourceInfo
    53  	result           *api.Result
    54  	ignorer          build.Ignorer
    55  }
    56  
    57  // New creates a Dockerfile builder.
    58  func New(config *api.Config, fs fs.FileSystem) (*Dockerfile, error) {
    59  	return &Dockerfile{
    60  		fs: fs,
    61  		// where we will get the assemble/run scripts from on the host machine,
    62  		// if any are provided.
    63  		uploadScriptsDir: constants.UploadScripts,
    64  		uploadSrcDir:     constants.Source,
    65  		result:           &api.Result{},
    66  		ignorer:          &ignore.DockerIgnorer{},
    67  	}, nil
    68  }
    69  
    70  // Build produces a Dockerfile that when run with the correct filesystem
    71  // context, will produce the application image.
    72  func (builder *Dockerfile) Build(config *api.Config) (*api.Result, error) {
    73  
    74  	// Handle defaulting of the configuration that is unique to the dockerfile strategy
    75  	if strings.HasSuffix(config.AsDockerfile, string(os.PathSeparator)) {
    76  		config.AsDockerfile = config.AsDockerfile + "Dockerfile"
    77  	}
    78  	if len(config.AssembleUser) == 0 {
    79  		config.AssembleUser = "1001"
    80  	}
    81  	if !user.IsUserAllowed(config.AssembleUser, &config.AllowedUIDs) {
    82  		builder.setFailureReason(utilstatus.ReasonAssembleUserForbidden, utilstatus.ReasonMessageAssembleUserForbidden)
    83  		return builder.result, s2ierr.NewUserNotAllowedError(config.AssembleUser, false)
    84  	}
    85  
    86  	dir, _ := filepath.Split(config.AsDockerfile)
    87  	if len(dir) == 0 {
    88  		dir = "."
    89  	}
    90  	config.PreserveWorkingDir = true
    91  	config.WorkingDir = dir
    92  
    93  	if config.BuilderImage == "" {
    94  		builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed)
    95  		return builder.result, errors.New("builder image name cannot be empty")
    96  	}
    97  
    98  	if err := builder.Prepare(config); err != nil {
    99  		return builder.result, err
   100  	}
   101  
   102  	if err := builder.CreateDockerfile(config); err != nil {
   103  		builder.setFailureReason(utilstatus.ReasonDockerfileCreateFailed, utilstatus.ReasonMessageDockerfileCreateFailed)
   104  		return builder.result, err
   105  	}
   106  
   107  	builder.result.Success = true
   108  
   109  	return builder.result, nil
   110  }
   111  
   112  // CreateDockerfile takes the various inputs and creates the Dockerfile used by
   113  // the docker cmd to create the image produced by s2i.
   114  func (builder *Dockerfile) CreateDockerfile(config *api.Config) error {
   115  	log.V(4).Infof("Constructing image build context directory at %s", config.WorkingDir)
   116  	buffer := bytes.Buffer{}
   117  
   118  	if len(config.ImageWorkDir) == 0 {
   119  		config.ImageWorkDir = "/opt/app-root/src"
   120  	}
   121  
   122  	imageUser := config.AssembleUser
   123  
   124  	// where files will land inside the new image.
   125  	scriptsDestDir := filepath.Join(getDestination(config), "scripts")
   126  	sourceDestDir := filepath.Join(getDestination(config), "src")
   127  	artifactsDestDir := filepath.Join(getDestination(config), "artifacts")
   128  	artifactsTar := sanitize(filepath.ToSlash(filepath.Join(defaultDestination, "artifacts.tar")))
   129  	// hasAllScripts indicates that we blindly trust all scripts are provided in the image scripts dir
   130  	imageScriptsDir, providedScripts := getImageScriptsDir(config, builder)
   131  
   132  	if config.Incremental {
   133  		imageTag := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag)
   134  		if len(imageTag) == 0 {
   135  			return errors.New("Image tag is missing for incremental build")
   136  		}
   137  		// Incremental builds run via a multistage Dockerfile
   138  		buffer.WriteString(fmt.Sprintf("FROM %s as cached\n", imageTag))
   139  		var artifactsScript string
   140  		if _, provided := providedScripts[constants.SaveArtifacts]; provided {
   141  			// switch to root to COPY and chown content
   142  			log.V(2).Infof("Override save-artifacts script is included in directory %q", builder.uploadScriptsDir)
   143  			buffer.WriteString("# Copying in override save-artifacts script\n")
   144  			buffer.WriteString("USER root\n")
   145  			artifactsScript = sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "save-artifacts")))
   146  			uploadScript := sanitize(filepath.ToSlash(filepath.Join(builder.uploadScriptsDir, "save-artifacts")))
   147  			buffer.WriteString(fmt.Sprintf("COPY %s %s\n", uploadScript, artifactsScript))
   148  			buffer.WriteString(fmt.Sprintf("RUN chown %s:0 %s\n", sanitize(imageUser), artifactsScript))
   149  		} else {
   150  			buffer.WriteString(fmt.Sprintf("# Save-artifacts script sourced from builder image based on user input or image metadata.\n"))
   151  			artifactsScript = sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "save-artifacts")))
   152  		}
   153  		// switch to the image user if it is not root
   154  		if len(imageUser) > 0 && imageUser != "root" {
   155  			buffer.WriteString(fmt.Sprintf("USER %s\n", imageUser))
   156  		}
   157  		buffer.WriteString(fmt.Sprintf("RUN if [ -s %[1]s ]; then %[1]s > %[2]s; else touch %[2]s; fi\n", artifactsScript, artifactsTar))
   158  	}
   159  
   160  	// main stage of the Dockerfile
   161  	buffer.WriteString(fmt.Sprintf("FROM %s\n", config.BuilderImage))
   162  
   163  	imageLabels := util.GenerateOutputImageLabels(builder.sourceInfo, config)
   164  	for k, v := range config.Labels {
   165  		imageLabels[k] = v
   166  	}
   167  
   168  	if len(config.ScriptsURL) > 0 {
   169  		imageLabels[constants.ScriptsURLLabel] = config.ScriptsURL
   170  	}
   171  
   172  	if len(config.Destination) > 0 {
   173  		imageLabels[constants.DestinationLabel] = config.Destination
   174  	}
   175  
   176  	if len(imageLabels) > 0 {
   177  		first := true
   178  		buffer.WriteString("LABEL ")
   179  		for k, v := range imageLabels {
   180  			if !first {
   181  				buffer.WriteString(fmt.Sprintf(" \\\n      "))
   182  			}
   183  			buffer.WriteString(fmt.Sprintf("%q=%q", k, v))
   184  			first = false
   185  		}
   186  		buffer.WriteString("\n")
   187  	}
   188  
   189  	env := createBuildEnvironment(config.WorkingDir, config.Environment)
   190  	buffer.WriteString(fmt.Sprintf("%s", env))
   191  
   192  	// run as root to COPY and chown source content
   193  	buffer.WriteString("USER root\n")
   194  	chownList := make([]string, 0)
   195  
   196  	if config.Incremental {
   197  		// COPY artifacts.tar from the `cached` stage
   198  		buffer.WriteString(fmt.Sprintf("COPY --from=cached %[1]s %[1]s\n", artifactsTar))
   199  		chownList = append(chownList, artifactsTar)
   200  	}
   201  
   202  	if len(providedScripts) > 0 {
   203  		// Only COPY scripts dir if required scripts are present and needed.
   204  		// Even if the "scripts" dir exists, the COPY would fail if it was empty.
   205  		log.V(2).Infof("Override scripts are included in directory %q", builder.uploadScriptsDir)
   206  		scriptsDest := sanitize(filepath.ToSlash(scriptsDestDir))
   207  		buffer.WriteString("# Copying in override assemble/run scripts\n")
   208  		buffer.WriteString(fmt.Sprintf("COPY %s %s\n", sanitize(filepath.ToSlash(builder.uploadScriptsDir)), scriptsDest))
   209  		chownList = append(chownList, scriptsDest)
   210  	}
   211  
   212  	// copy in the user's source code.
   213  	buffer.WriteString("# Copying in source code\n")
   214  	sourceDest := sanitize(filepath.ToSlash(sourceDestDir))
   215  	buffer.WriteString(fmt.Sprintf("COPY %s %s\n", sanitize(filepath.ToSlash(builder.uploadSrcDir)), sourceDest))
   216  	chownList = append(chownList, sourceDest)
   217  
   218  	// add injections
   219  	log.V(4).Infof("Processing injected inputs: %#v", config.Injections)
   220  	config.Injections = util.FixInjectionsWithRelativePath(config.ImageWorkDir, config.Injections)
   221  	log.V(4).Infof("Processed injected inputs: %#v", config.Injections)
   222  
   223  	if len(config.Injections) > 0 {
   224  		buffer.WriteString("# Copying in injected content\n")
   225  	}
   226  	for _, injection := range config.Injections {
   227  		src := sanitize(filepath.ToSlash(filepath.Join(constants.Injections, injection.Source)))
   228  		dest := sanitize(filepath.ToSlash(injection.Destination))
   229  		buffer.WriteString(fmt.Sprintf("COPY %s %s\n", src, dest))
   230  		chownList = append(chownList, dest)
   231  	}
   232  
   233  	// chown directories COPYed to image
   234  	if len(chownList) > 0 {
   235  		buffer.WriteString("# Change file ownership to the assemble user. Builder image must support chown command.\n")
   236  		buffer.WriteString(fmt.Sprintf("RUN chown -R %s:0", sanitize(imageUser)))
   237  		for _, dir := range chownList {
   238  			buffer.WriteString(fmt.Sprintf(" %s", dir))
   239  		}
   240  		buffer.WriteString("\n")
   241  	}
   242  
   243  	// run remaining commands as the image user
   244  	if len(imageUser) > 0 && imageUser != "root" {
   245  		buffer.WriteString(fmt.Sprintf("USER %s\n", imageUser))
   246  	}
   247  
   248  	if config.Incremental {
   249  		buffer.WriteString("# Extract artifact content\n")
   250  		buffer.WriteString(fmt.Sprintf("RUN if [ -s %[1]s ]; then mkdir -p %[2]s; tar -xf %[1]s -C %[2]s; fi && \\\n", artifactsTar, sanitize(filepath.ToSlash(artifactsDestDir))))
   251  		buffer.WriteString(fmt.Sprintf("    rm %s\n", artifactsTar))
   252  	}
   253  
   254  	if _, provided := providedScripts[constants.Assemble]; provided {
   255  		buffer.WriteString(fmt.Sprintf("RUN %s\n", sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "assemble")))))
   256  	} else {
   257  		buffer.WriteString(fmt.Sprintf("# Assemble script sourced from builder image based on user input or image metadata.\n"))
   258  		buffer.WriteString(fmt.Sprintf("# If this file does not exist in the image, the build will fail.\n"))
   259  		buffer.WriteString(fmt.Sprintf("RUN %s\n", sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "assemble")))))
   260  	}
   261  
   262  	filesToDelete, err := util.ListFilesToTruncate(builder.fs, config.Injections)
   263  	if err != nil {
   264  		return err
   265  	}
   266  	if len(filesToDelete) > 0 {
   267  		wroteRun := false
   268  		buffer.WriteString("# Cleaning up injected secret content\n")
   269  		for _, file := range filesToDelete {
   270  			if !wroteRun {
   271  				buffer.WriteString(fmt.Sprintf("RUN rm %s", file))
   272  				wroteRun = true
   273  				continue
   274  			}
   275  			buffer.WriteString(fmt.Sprintf(" && \\\n"))
   276  			buffer.WriteString(fmt.Sprintf("    rm %s", file))
   277  		}
   278  		buffer.WriteString("\n")
   279  	}
   280  
   281  	if _, provided := providedScripts[constants.Run]; provided {
   282  		buffer.WriteString(fmt.Sprintf("CMD %s\n", sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "run")))))
   283  	} else {
   284  		buffer.WriteString(fmt.Sprintf("# Run script sourced from builder image based on user input or image metadata.\n"))
   285  		buffer.WriteString(fmt.Sprintf("# If this file does not exist in the image, the build will fail.\n"))
   286  		buffer.WriteString(fmt.Sprintf("CMD %s\n", sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "run")))))
   287  	}
   288  
   289  	if err := builder.fs.WriteFile(filepath.Join(config.AsDockerfile), buffer.Bytes()); err != nil {
   290  		return err
   291  	}
   292  	log.V(2).Infof("Wrote custom Dockerfile to %s", config.AsDockerfile)
   293  	return nil
   294  }
   295  
   296  // Prepare prepares the source code and tar for build.
   297  // NOTE: this func serves both the sti and onbuild strategies, as the OnBuild
   298  // struct Build func leverages the STI struct Prepare func directly below.
   299  func (builder *Dockerfile) Prepare(config *api.Config) error {
   300  	var err error
   301  
   302  	if len(config.WorkingDir) == 0 {
   303  		if config.WorkingDir, err = builder.fs.CreateWorkingDirectory(); err != nil {
   304  			builder.setFailureReason(utilstatus.ReasonFSOperationFailed, utilstatus.ReasonMessageFSOperationFailed)
   305  			return err
   306  		}
   307  	}
   308  
   309  	builder.result.WorkingDir = config.WorkingDir
   310  
   311  	// Setup working directories
   312  	for _, v := range workingDirs {
   313  		if err = builder.fs.MkdirAllWithPermissions(filepath.Join(config.WorkingDir, v), 0755); err != nil {
   314  			builder.setFailureReason(utilstatus.ReasonFSOperationFailed, utilstatus.ReasonMessageFSOperationFailed)
   315  			return err
   316  		}
   317  	}
   318  
   319  	// Default - install scripts specified by image metadata.
   320  	// Typically this will point to an image:// URL, and no scripts are downloaded.
   321  	// However, if builder image labels are specified, we'll go with those and not the default
   322  	if config.BuilderImageLabels == nil {
   323  		builder.installScripts(config.ImageScriptsURL, config)
   324  	}
   325  
   326  	// Fetch sources, since their .s2i/bin might contain s2i scripts which override defaults.
   327  	if config.Source != nil {
   328  		downloader, err := scm.DownloaderForSource(builder.fs, config.Source, config.ForceCopy)
   329  		if err != nil {
   330  			builder.setFailureReason(utilstatus.ReasonFetchSourceFailed, utilstatus.ReasonMessageFetchSourceFailed)
   331  			return err
   332  		}
   333  		if builder.sourceInfo, err = downloader.Download(config); err != nil {
   334  			builder.setFailureReason(utilstatus.ReasonFetchSourceFailed, utilstatus.ReasonMessageFetchSourceFailed)
   335  			switch err.(type) {
   336  			case file.RecursiveCopyError:
   337  				return fmt.Errorf("input source directory contains the target directory for the build, check that your Dockerfile output path does not reside within your input source path: %v", err)
   338  			}
   339  			return err
   340  		}
   341  		if config.SourceInfo != nil {
   342  			builder.sourceInfo = config.SourceInfo
   343  		}
   344  	}
   345  
   346  	// Install scripts provided by user, overriding all others.
   347  	// This _could_ be an image:// URL, which would override any scripts above.
   348  	urlScripts := builder.installScripts(config.ScriptsURL, config)
   349  	// If a ScriptsURL was specified, but no scripts were downloaded from it, throw an error
   350  	if len(config.ScriptsURL) > 0 {
   351  		failedCount := 0
   352  		for _, result := range urlScripts {
   353  			if util.Includes(result.FailedSources, scripts.ScriptURLHandler) {
   354  				failedCount++
   355  			}
   356  		}
   357  		if failedCount == len(urlScripts) {
   358  			builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
   359  				utilstatus.ReasonScriptsFetchFailed,
   360  				utilstatus.ReasonMessageScriptsFetchFailed,
   361  			)
   362  			return fmt.Errorf("could not download any scripts from URL %v", config.ScriptsURL)
   363  		}
   364  	}
   365  
   366  	// Stage any injection(secrets) content into the working dir so the dockerfile can reference it.
   367  	for i, injection := range config.Injections {
   368  		// strip the C: from windows paths because it's not valid in the middle of a path
   369  		// like upload/injections/C:/tempdir/injection1
   370  		trimmedSrc := strings.TrimPrefix(injection.Source, filepath.VolumeName(injection.Source))
   371  		dst := filepath.Join(config.WorkingDir, constants.Injections, trimmedSrc)
   372  		log.V(4).Infof("Copying injection content from %s to %s", injection.Source, dst)
   373  		if err := builder.fs.CopyContents(injection.Source, dst, nil); err != nil {
   374  			builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed)
   375  			return err
   376  		}
   377  		config.Injections[i].Source = trimmedSrc
   378  	}
   379  
   380  	// see if there is a .s2iignore file, and if so, read in the patterns and then
   381  	// search and delete on them.
   382  	err = builder.ignorer.Ignore(config)
   383  	if err != nil {
   384  		builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed)
   385  		return err
   386  	}
   387  	return nil
   388  }
   389  
   390  // installScripts installs scripts at the provided URL to the Dockerfile context
   391  func (builder *Dockerfile) installScripts(scriptsURL string, config *api.Config) []api.InstallResult {
   392  	scriptInstaller := scripts.NewInstaller(
   393  		"",
   394  		scriptsURL,
   395  		config.ScriptDownloadProxyConfig,
   396  		nil,
   397  		api.AuthConfig{},
   398  		builder.fs,
   399  		config,
   400  	)
   401  
   402  	// all scripts are optional, we trust the image contains scripts if we don't find them
   403  	// in the source repo.
   404  	return scriptInstaller.InstallOptional(append(scripts.RequiredScripts, scripts.OptionalScripts...), config.WorkingDir)
   405  }
   406  
   407  // setFailureReason sets the builder's failure reason with the given reason and message.
   408  func (builder *Dockerfile) setFailureReason(reason api.StepFailureReason, message api.StepFailureMessage) {
   409  	builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(reason, message)
   410  }
   411  
   412  // getDestination returns the destination directory from the config.
   413  func getDestination(config *api.Config) string {
   414  	destination := config.Destination
   415  	if len(destination) == 0 {
   416  		destination = defaultDestination
   417  	}
   418  	return destination
   419  }
   420  
   421  // getImageScriptsDir returns the default directory which should contain the builder image scripts
   422  // as well as a map of booleans identifying  individual scripts provided in the repository as overrides
   423  func getImageScriptsDir(config *api.Config, builder *Dockerfile) (string, map[string]bool) {
   424  
   425  	// 1st priority is the command line parameter (pointing to an image, overrides it all)
   426  	if strings.HasPrefix(config.ScriptsURL, "image://") {
   427  		return strings.TrimPrefix(config.ScriptsURL, "image://"), make(map[string]bool)
   428  	}
   429  
   430  	// 2nd priority (the source code repository), collect the locations
   431  	providedScripts := scanScripts(filepath.Join(config.WorkingDir, builder.uploadScriptsDir))
   432  
   433  	// 3rd priority (the builder image), collect the locations
   434  	scriptsURL, _ := util.AdjustConfigWithImageLabels(config)
   435  	if strings.HasPrefix(scriptsURL, "image://") {
   436  		return strings.TrimPrefix(scriptsURL, "image://"), providedScripts
   437  	}
   438  	if strings.HasPrefix(config.ImageScriptsURL, "image://") {
   439  		return strings.TrimPrefix(config.ImageScriptsURL, "image://"), providedScripts
   440  	}
   441  
   442  	// If all else fails, use the default scripts dir
   443  	return defaultScriptsDir, providedScripts
   444  }
   445  
   446  // scanScripts returns a map of provided s2i scripts
   447  func scanScripts(name string) map[string]bool {
   448  	scriptsMap := make(map[string]bool)
   449  	items, err := ioutil.ReadDir(name)
   450  	if os.IsNotExist(err) {
   451  		log.Warningf("Unable to access directory %q: %v", name, err)
   452  	}
   453  	if err != nil || len(items) == 0 {
   454  		return scriptsMap
   455  	}
   456  
   457  	assembleProvided := false
   458  	runProvided := false
   459  	saveArtifactsProvided := false
   460  	for _, f := range items {
   461  		log.V(2).Infof("found override script file %s", f.Name())
   462  		if f.Name() == constants.Run {
   463  			runProvided = true
   464  			scriptsMap[constants.Run] = true
   465  		} else if f.Name() == constants.Assemble {
   466  			assembleProvided = true
   467  			scriptsMap[constants.Assemble] = true
   468  		} else if f.Name() == constants.SaveArtifacts {
   469  			saveArtifactsProvided = true
   470  			scriptsMap[constants.SaveArtifacts] = true
   471  		}
   472  		if runProvided && assembleProvided && saveArtifactsProvided {
   473  			break
   474  		}
   475  	}
   476  	return scriptsMap
   477  }
   478  
   479  func includes(arr []string, str string) bool {
   480  	for _, s := range arr {
   481  		if s == str {
   482  			return true
   483  		}
   484  	}
   485  	return false
   486  }
   487  
   488  func sanitize(s string) string {
   489  	return strings.Replace(s, "\n", "\\n", -1)
   490  }
   491  
   492  func createBuildEnvironment(sourcePath string, cfgEnv api.EnvironmentList) string {
   493  	s2iEnv, err := scripts.GetEnvironment(filepath.Join(sourcePath, constants.Source))
   494  	if err != nil {
   495  		log.V(3).Infof("No user environment provided (%v)", err)
   496  	}
   497  
   498  	return scripts.ConvertEnvironmentToDocker(append(s2iEnv, cfgEnv...))
   499  }