kubesphere.io/s2irun@v3.2.1+incompatible/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/kubesphere/s2irun/pkg/api"
    13  	"github.com/kubesphere/s2irun/pkg/api/constants"
    14  	"github.com/kubesphere/s2irun/pkg/build"
    15  	s2ierr "github.com/kubesphere/s2irun/pkg/errors"
    16  	"github.com/kubesphere/s2irun/pkg/ignore"
    17  	"github.com/kubesphere/s2irun/pkg/scm"
    18  	"github.com/kubesphere/s2irun/pkg/scm/downloaders/file"
    19  	"github.com/kubesphere/s2irun/pkg/scm/git"
    20  	"github.com/kubesphere/s2irun/pkg/scripts"
    21  	"github.com/kubesphere/s2irun/pkg/utils"
    22  	"github.com/kubesphere/s2irun/pkg/utils/fs"
    23  	utilglog "github.com/kubesphere/s2irun/pkg/utils/glog"
    24  	utilstatus "github.com/kubesphere/s2irun/pkg/utils/status"
    25  	"github.com/kubesphere/s2irun/pkg/utils/user"
    26  )
    27  
    28  const (
    29  	defaultDestination = "/tmp"
    30  	defaultScriptsDir  = "/usr/libexec/s2i"
    31  )
    32  
    33  var (
    34  	glog = utilglog.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  	glog.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, hasAllScripts := getImageScriptsDir(config)
   131  	var providedScripts map[string]bool
   132  	if !hasAllScripts {
   133  		providedScripts = scanScripts(filepath.Join(config.WorkingDir, builder.uploadScriptsDir))
   134  	}
   135  
   136  	if config.Incremental {
   137  		imageTag := utils.FirstNonEmpty(config.IncrementalFromTag, config.Tag)
   138  		if len(imageTag) == 0 {
   139  			return errors.New("Image tag is missing for incremental build")
   140  		}
   141  		// Incremental builds run via a multistage Dockerfile
   142  		buffer.WriteString(fmt.Sprintf("FROM %s as cached\n", imageTag))
   143  		var artifactsScript string
   144  		if _, provided := providedScripts[constants.SaveArtifacts]; provided {
   145  			// switch to root to COPY and chown content
   146  			glog.V(2).Infof("Override save-artifacts script is included in directory %q", builder.uploadScriptsDir)
   147  			buffer.WriteString("# Copying in override save-artifacts script\n")
   148  			buffer.WriteString("USER root\n")
   149  			artifactsScript = sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "save-artifacts")))
   150  			uploadScript := sanitize(filepath.ToSlash(filepath.Join(builder.uploadScriptsDir, "save-artifacts")))
   151  			buffer.WriteString(fmt.Sprintf("COPY %s %s\n", uploadScript, artifactsScript))
   152  			buffer.WriteString(fmt.Sprintf("RUN chown %s:0 %s\n", sanitize(imageUser), artifactsScript))
   153  		} else {
   154  			buffer.WriteString(fmt.Sprintf("# Save-artifacts script sourced from builder image based on user input or image metadata.\n"))
   155  			artifactsScript = sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "save-artifacts")))
   156  		}
   157  		// switch to the image user if it is not root
   158  		if len(imageUser) > 0 && imageUser != "root" {
   159  			buffer.WriteString(fmt.Sprintf("USER %s\n", imageUser))
   160  		}
   161  		buffer.WriteString(fmt.Sprintf("RUN if [ -s %[1]s ]; then %[1]s > %[2]s; else touch %[2]s; fi\n", artifactsScript, artifactsTar))
   162  	}
   163  
   164  	// main stage of the Dockerfile
   165  	buffer.WriteString(fmt.Sprintf("FROM %s\n", config.BuilderImage))
   166  
   167  	imageLabels := utils.GenerateOutputImageLabels(builder.sourceInfo, config)
   168  	for k, v := range config.Labels {
   169  		imageLabels[k] = v
   170  	}
   171  	if len(imageLabels) > 0 {
   172  		first := true
   173  		buffer.WriteString("LABEL ")
   174  		for k, v := range imageLabels {
   175  			if !first {
   176  				buffer.WriteString(fmt.Sprintf(" \\\n      "))
   177  			}
   178  			buffer.WriteString(fmt.Sprintf("%q=%q", k, v))
   179  			first = false
   180  		}
   181  		buffer.WriteString("\n")
   182  	}
   183  
   184  	env := createBuildEnvironment(config.WorkingDir, config.Environment)
   185  	buffer.WriteString(fmt.Sprintf("%s", env))
   186  
   187  	// run as root to COPY and chown source content
   188  	buffer.WriteString("USER root\n")
   189  	chownList := make([]string, 0)
   190  
   191  	if config.Incremental {
   192  		// COPY artifacts.tar from the `cached` stage
   193  		buffer.WriteString(fmt.Sprintf("COPY --from=cached %[1]s %[1]s\n", artifactsTar))
   194  		chownList = append(chownList, artifactsTar)
   195  	}
   196  
   197  	if len(providedScripts) > 0 {
   198  		// Only COPY scripts dir if required scripts are present and needed.
   199  		// Even if the "scripts" dir exists, the COPY would fail if it was empty.
   200  		glog.V(2).Infof("Override scripts are included in directory %q", builder.uploadScriptsDir)
   201  		scriptsDest := sanitize(filepath.ToSlash(scriptsDestDir))
   202  		buffer.WriteString("# Copying in override assemble/run scripts\n")
   203  		buffer.WriteString(fmt.Sprintf("COPY %s %s\n", sanitize(filepath.ToSlash(builder.uploadScriptsDir)), scriptsDest))
   204  		chownList = append(chownList, scriptsDest)
   205  	}
   206  
   207  	// copy in the user's source code.
   208  	buffer.WriteString("# Copying in source code\n")
   209  	sourceDest := sanitize(filepath.ToSlash(sourceDestDir))
   210  	buffer.WriteString(fmt.Sprintf("COPY %s %s\n", sanitize(filepath.ToSlash(builder.uploadSrcDir)), sourceDest))
   211  	chownList = append(chownList, sourceDest)
   212  
   213  	// add injections
   214  	glog.V(4).Infof("Processing injected inputs: %#v", config.Injections)
   215  	config.Injections = utils.FixInjectionsWithRelativePath(config.ImageWorkDir, config.Injections)
   216  	glog.V(4).Infof("Processed injected inputs: %#v", config.Injections)
   217  
   218  	if len(config.Injections) > 0 {
   219  		buffer.WriteString("# Copying in injected content\n")
   220  	}
   221  	for _, injection := range config.Injections {
   222  		src := sanitize(filepath.ToSlash(filepath.Join(constants.Injections, injection.Source)))
   223  		dest := sanitize(filepath.ToSlash(injection.Destination))
   224  		buffer.WriteString(fmt.Sprintf("COPY %s %s\n", src, dest))
   225  		chownList = append(chownList, dest)
   226  	}
   227  
   228  	// chown directories COPYed to image
   229  	if len(chownList) > 0 {
   230  		buffer.WriteString("# Change file ownership to the assemble user. Builder image must support chown command.\n")
   231  		buffer.WriteString(fmt.Sprintf("RUN chown -R %s:0", sanitize(imageUser)))
   232  		for _, dir := range chownList {
   233  			buffer.WriteString(fmt.Sprintf(" %s", dir))
   234  		}
   235  		buffer.WriteString("\n")
   236  	}
   237  
   238  	// run remaining commands as the image user
   239  	if len(imageUser) > 0 && imageUser != "root" {
   240  		buffer.WriteString(fmt.Sprintf("USER %s\n", imageUser))
   241  	}
   242  
   243  	if config.Incremental {
   244  		buffer.WriteString("# Extract artifact content\n")
   245  		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))))
   246  		buffer.WriteString(fmt.Sprintf("    rm %s\n", artifactsTar))
   247  	}
   248  
   249  	if _, provided := providedScripts[constants.Assemble]; provided {
   250  		buffer.WriteString(fmt.Sprintf("RUN %s\n", sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "assemble")))))
   251  	} else {
   252  		buffer.WriteString(fmt.Sprintf("# Assemble script sourced from builder image based on user input or image metadata.\n"))
   253  		buffer.WriteString(fmt.Sprintf("# If this file does not exist in the image, the build will fail.\n"))
   254  		buffer.WriteString(fmt.Sprintf("RUN %s\n", sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "assemble")))))
   255  	}
   256  
   257  	filesToDelete, err := utils.ListFilesToTruncate(builder.fs, config.Injections)
   258  	if err != nil {
   259  		return err
   260  	}
   261  	if len(filesToDelete) > 0 {
   262  		wroteRun := false
   263  		buffer.WriteString("# Cleaning up injected secret content\n")
   264  		for _, file := range filesToDelete {
   265  			if !wroteRun {
   266  				buffer.WriteString(fmt.Sprintf("RUN rm %s", file))
   267  				wroteRun = true
   268  				continue
   269  			}
   270  			buffer.WriteString(fmt.Sprintf(" && \\\n"))
   271  			buffer.WriteString(fmt.Sprintf("    rm %s", file))
   272  		}
   273  		buffer.WriteString("\n")
   274  	}
   275  
   276  	if _, provided := providedScripts[constants.Run]; provided {
   277  		buffer.WriteString(fmt.Sprintf("CMD %s\n", sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "run")))))
   278  	} else {
   279  		buffer.WriteString(fmt.Sprintf("# Run script sourced from builder image based on user input or image metadata.\n"))
   280  		buffer.WriteString(fmt.Sprintf("# If this file does not exist in the image, the build will fail.\n"))
   281  		buffer.WriteString(fmt.Sprintf("CMD %s\n", sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "run")))))
   282  	}
   283  
   284  	if err := builder.fs.WriteFile(filepath.Join(config.AsDockerfile), buffer.Bytes()); err != nil {
   285  		return err
   286  	}
   287  	glog.V(2).Infof("Wrote custom Dockerfile to %s", config.AsDockerfile)
   288  	return nil
   289  }
   290  
   291  // Prepare prepares the source code and tar for build.
   292  // NOTE: this func serves both the sti and onbuild strategies, as the OnBuild
   293  // struct Build func leverages the STI struct Prepare func directly below.
   294  func (builder *Dockerfile) Prepare(config *api.Config) error {
   295  	var err error
   296  
   297  	if len(config.WorkingDir) == 0 {
   298  		if config.WorkingDir, err = builder.fs.CreateWorkingDirectory(); err != nil {
   299  			builder.setFailureReason(utilstatus.ReasonFSOperationFailed, utilstatus.ReasonMessageFSOperationFailed)
   300  			return err
   301  		}
   302  	}
   303  
   304  	builder.result.WorkingDir = config.WorkingDir
   305  
   306  	// Setup working directories
   307  	for _, v := range workingDirs {
   308  		if err = builder.fs.MkdirAllWithPermissions(filepath.Join(config.WorkingDir, v), 0755); err != nil {
   309  			builder.setFailureReason(utilstatus.ReasonFSOperationFailed, utilstatus.ReasonMessageFSOperationFailed)
   310  			return err
   311  		}
   312  	}
   313  
   314  	// Default - install scripts specified by image metadata.
   315  	// Typically this will point to an image:// URL, and no scripts are downloaded.
   316  	// However, this is not guaranteed.
   317  	builder.installScripts(config.ImageScriptsURL, config)
   318  
   319  	// Fetch sources, since their .s2i/bin might contain s2i scripts which override defaults.
   320  	if config.Source != nil {
   321  		downloader, err := scm.DownloaderForSource(builder.fs, config.Source, config.ForceCopy)
   322  		if err != nil {
   323  			builder.setFailureReason(utilstatus.ReasonFetchSourceFailed, utilstatus.ReasonMessageFetchSourceFailed)
   324  			return err
   325  		}
   326  		if builder.sourceInfo, err = downloader.Download(config); err != nil {
   327  			builder.setFailureReason(utilstatus.ReasonFetchSourceFailed, utilstatus.ReasonMessageFetchSourceFailed)
   328  			switch err.(type) {
   329  			case file.RecursiveCopyError:
   330  				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)
   331  			}
   332  			return err
   333  		}
   334  		if config.SourceInfo != nil {
   335  			builder.sourceInfo = config.SourceInfo
   336  		}
   337  	}
   338  
   339  	// Install scripts provided by user, overriding all others.
   340  	// This _could_ be an image:// URL, which would override any scripts above.
   341  	builder.installScripts(config.ScriptsURL, config)
   342  
   343  	// Stage any injection(secrets) content into the working dir so the dockerfile can reference it.
   344  	for i, injection := range config.Injections {
   345  		// strip the C: from windows paths because it's not valid in the middle of a path
   346  		// like upload/injections/C:/tempdir/injection1
   347  		trimmedSrc := strings.TrimPrefix(injection.Source, filepath.VolumeName(injection.Source))
   348  		dst := filepath.Join(config.WorkingDir, constants.Injections, trimmedSrc)
   349  		glog.V(4).Infof("Copying injection content from %s to %s", injection.Source, dst)
   350  		if err := builder.fs.CopyContents(injection.Source, dst); err != nil {
   351  			builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed)
   352  			return err
   353  		}
   354  		config.Injections[i].Source = trimmedSrc
   355  	}
   356  
   357  	// see if there is a .s2iignore file, and if so, read in the patterns and then
   358  	// search and delete on them.
   359  	err = builder.ignorer.Ignore(config)
   360  	if err != nil {
   361  		builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed)
   362  		return err
   363  	}
   364  	return nil
   365  }
   366  
   367  // installScripts installs scripts at the provided URL to the Dockerfile context
   368  func (builder *Dockerfile) installScripts(scriptsURL string, config *api.Config) []api.InstallResult {
   369  	scriptInstaller := scripts.NewInstaller(
   370  		"",
   371  		scriptsURL,
   372  		config.ScriptDownloadProxyConfig,
   373  		nil,
   374  		api.AuthConfig{},
   375  		builder.fs,
   376  	)
   377  
   378  	// all scripts are optional, we trust the image contains scripts if we don't find them
   379  	// in the source repo.
   380  	return scriptInstaller.InstallOptional(append(scripts.RequiredScripts, scripts.OptionalScripts...), config.WorkingDir)
   381  }
   382  
   383  // setFailureReason sets the builder's failure reason with the given reason and message.
   384  func (builder *Dockerfile) setFailureReason(reason api.StepFailureReason, message api.StepFailureMessage) {
   385  	builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(reason, message)
   386  }
   387  
   388  // getDestination returns the destination directory from the config.
   389  func getDestination(config *api.Config) string {
   390  	destination := config.Destination
   391  	if len(destination) == 0 {
   392  		destination = defaultDestination
   393  	}
   394  	return destination
   395  }
   396  
   397  // getImageScriptsDir returns the directory containing the builder image scripts and a bool
   398  // indicating that the directory is expected to contain all s2i scripts
   399  func getImageScriptsDir(config *api.Config) (string, bool) {
   400  	if strings.HasPrefix(config.ScriptsURL, "image://") {
   401  		return strings.TrimPrefix(config.ScriptsURL, "image://"), true
   402  	}
   403  	if strings.HasPrefix(config.ImageScriptsURL, "image://") {
   404  		return strings.TrimPrefix(config.ImageScriptsURL, "image://"), false
   405  	}
   406  	return defaultScriptsDir, false
   407  }
   408  
   409  // scanScripts returns a map of provided s2i scripts
   410  func scanScripts(name string) map[string]bool {
   411  	scriptsMap := make(map[string]bool)
   412  	items, err := ioutil.ReadDir(name)
   413  	if os.IsNotExist(err) {
   414  		glog.Warningf("Unable to access directory %q: %v", name, err)
   415  	}
   416  	if err != nil || len(items) == 0 {
   417  		return scriptsMap
   418  	}
   419  
   420  	assembleProvided := false
   421  	runProvided := false
   422  	saveArtifactsProvided := false
   423  	for _, f := range items {
   424  		glog.V(2).Infof("found override script file %s", f.Name())
   425  		if f.Name() == constants.Run {
   426  			runProvided = true
   427  			scriptsMap[constants.Run] = true
   428  		} else if f.Name() == constants.Assemble {
   429  			assembleProvided = true
   430  			scriptsMap[constants.Assemble] = true
   431  		} else if f.Name() == constants.SaveArtifacts {
   432  			saveArtifactsProvided = true
   433  			scriptsMap[constants.SaveArtifacts] = true
   434  		}
   435  		if runProvided && assembleProvided && saveArtifactsProvided {
   436  			break
   437  		}
   438  	}
   439  	return scriptsMap
   440  }
   441  
   442  func includes(arr []string, str string) bool {
   443  	for _, s := range arr {
   444  		if s == str {
   445  			return true
   446  		}
   447  	}
   448  	return false
   449  }
   450  
   451  func sanitize(s string) string {
   452  	return strings.Replace(s, "\n", "\\n", -1)
   453  }
   454  
   455  func createBuildEnvironment(sourcePath string, cfgEnv api.EnvironmentList) string {
   456  	s2iEnv, err := scripts.GetEnvironment(filepath.Join(sourcePath, constants.Source))
   457  	if err != nil {
   458  		glog.V(3).Infof("No user environment provided (%v)", err)
   459  	}
   460  
   461  	return scripts.ConvertEnvironmentToDocker(append(s2iEnv, cfgEnv...))
   462  }