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

     1  package layered
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"regexp"
    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/docker"
    19  	s2ierr "github.com/openshift/source-to-image/pkg/errors"
    20  	"github.com/openshift/source-to-image/pkg/tar"
    21  	"github.com/openshift/source-to-image/pkg/util/fs"
    22  	utillog "github.com/openshift/source-to-image/pkg/util/log"
    23  	utilstatus "github.com/openshift/source-to-image/pkg/util/status"
    24  )
    25  
    26  var log = utillog.StderrLog
    27  
    28  const defaultDestination = "/tmp"
    29  
    30  // A Layered builder builds images by first performing a docker build to inject
    31  // (layer) the source code and s2i scripts into the builder image, prior to
    32  // running the new image with the assemble script. This is necessary when the
    33  // builder image does not include "sh" and "tar" as those tools are needed
    34  // during the normal source injection process.
    35  type Layered struct {
    36  	config     *api.Config
    37  	docker     docker.Docker
    38  	fs         fs.FileSystem
    39  	tar        tar.Tar
    40  	scripts    build.ScriptsHandler
    41  	hasOnBuild bool
    42  }
    43  
    44  // New creates a Layered builder.
    45  func New(client docker.Client, config *api.Config, fs fs.FileSystem, scripts build.ScriptsHandler, overrides build.Overrides) (*Layered, error) {
    46  	excludePattern, err := regexp.Compile(config.ExcludeRegExp)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  
    51  	d := docker.New(client, config.PullAuthentication)
    52  	tarHandler := tar.New(fs)
    53  	tarHandler.SetExclusionPattern(excludePattern)
    54  
    55  	return &Layered{
    56  		docker:  d,
    57  		config:  config,
    58  		fs:      fs,
    59  		tar:     tarHandler,
    60  		scripts: scripts,
    61  	}, nil
    62  }
    63  
    64  // getDestination returns the destination directory from the config.
    65  func getDestination(config *api.Config) string {
    66  	destination := config.Destination
    67  	if len(destination) == 0 {
    68  		destination = defaultDestination
    69  	}
    70  	return destination
    71  }
    72  
    73  // checkValidDirWithContents returns true if the parameter provided is a valid,
    74  // accessible and non-empty directory.
    75  func checkValidDirWithContents(name string) bool {
    76  	items, err := ioutil.ReadDir(name)
    77  	if os.IsNotExist(err) {
    78  		log.Warningf("Unable to access directory %q: %v", name, err)
    79  	}
    80  	return !(err != nil || len(items) == 0)
    81  }
    82  
    83  // CreateDockerfile takes the various inputs and creates the Dockerfile used by
    84  // the docker cmd to create the image produced by s2i.
    85  func (builder *Layered) CreateDockerfile(config *api.Config) error {
    86  	buffer := bytes.Buffer{}
    87  
    88  	user, err := builder.docker.GetImageUser(builder.config.BuilderImage)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	scriptsDir := filepath.Join(getDestination(config), "scripts")
    94  	sourcesDir := filepath.Join(getDestination(config), "src")
    95  
    96  	uploadScriptsDir := path.Join(config.WorkingDir, constants.UploadScripts)
    97  
    98  	buffer.WriteString(fmt.Sprintf("FROM %s\n", builder.config.BuilderImage))
    99  	// only COPY scripts dir if required scripts are present, i.e. the dir is not empty;
   100  	// even if the "scripts" dir exists, the COPY would fail if it was empty
   101  	scriptsIncluded := checkValidDirWithContents(uploadScriptsDir)
   102  	if scriptsIncluded {
   103  		log.V(2).Infof("The scripts are included in %q directory", uploadScriptsDir)
   104  		buffer.WriteString(fmt.Sprintf("COPY scripts %s\n", filepath.ToSlash(scriptsDir)))
   105  	} else {
   106  		// if an err on reading or opening dir, can't copy it
   107  		log.V(2).Infof("Could not gather scripts from the directory %q", uploadScriptsDir)
   108  	}
   109  	buffer.WriteString(fmt.Sprintf("COPY src %s\n", filepath.ToSlash(sourcesDir)))
   110  
   111  	//TODO: We need to account for images that may not have chown. There is a proposal
   112  	//      to specify the owner for COPY here: https://github.com/docker/docker/pull/28499
   113  	if len(user) > 0 {
   114  		buffer.WriteString("USER root\n")
   115  		if scriptsIncluded {
   116  			buffer.WriteString(fmt.Sprintf("RUN chown -R %s -- %s %s\n", user, filepath.ToSlash(scriptsDir), filepath.ToSlash(sourcesDir)))
   117  		} else {
   118  			buffer.WriteString(fmt.Sprintf("RUN chown -R %s -- %s\n", user, filepath.ToSlash(sourcesDir)))
   119  		}
   120  		buffer.WriteString(fmt.Sprintf("USER %s\n", user))
   121  	}
   122  
   123  	uploadDir := filepath.Join(builder.config.WorkingDir, "upload")
   124  	if err := builder.fs.WriteFile(filepath.Join(uploadDir, "Dockerfile"), buffer.Bytes()); err != nil {
   125  		return err
   126  	}
   127  	log.V(2).Infof("Writing custom Dockerfile to %s", uploadDir)
   128  	return nil
   129  }
   130  
   131  // Build handles the `docker build` equivalent execution, returning the
   132  // success/failure details.
   133  func (builder *Layered) Build(config *api.Config) (*api.Result, error) {
   134  	buildResult := &api.Result{}
   135  
   136  	if config.HasOnBuild && config.BlockOnBuild {
   137  		buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
   138  			utilstatus.ReasonOnBuildForbidden,
   139  			utilstatus.ReasonMessageOnBuildForbidden,
   140  		)
   141  		return buildResult, errors.New("builder image uses ONBUILD instructions but ONBUILD is not allowed")
   142  	}
   143  
   144  	if config.BuilderImage == "" {
   145  		buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
   146  			utilstatus.ReasonGenericS2IBuildFailed,
   147  			utilstatus.ReasonMessageGenericS2iBuildFailed,
   148  		)
   149  		return buildResult, errors.New("builder image name cannot be empty")
   150  	}
   151  
   152  	if err := builder.CreateDockerfile(config); err != nil {
   153  		buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
   154  			utilstatus.ReasonDockerfileCreateFailed,
   155  			utilstatus.ReasonMessageDockerfileCreateFailed,
   156  		)
   157  		return buildResult, err
   158  	}
   159  
   160  	log.V(2).Info("Creating application source code image")
   161  	tarStream := builder.tar.CreateTarStreamReader(filepath.Join(config.WorkingDir, "upload"), false)
   162  	defer tarStream.Close()
   163  
   164  	newBuilderImage := fmt.Sprintf("s2i-layered-temp-image-%d", time.Now().UnixNano())
   165  
   166  	outReader, outWriter := io.Pipe()
   167  	opts := docker.BuildImageOptions{
   168  		Name:         newBuilderImage,
   169  		Stdin:        tarStream,
   170  		Stdout:       outWriter,
   171  		CGroupLimits: config.CGroupLimits,
   172  	}
   173  	docker.StreamContainerIO(outReader, nil, func(s string) { log.V(2).Info(s) })
   174  
   175  	log.V(2).Infof("Building new image %s with scripts and sources already inside", newBuilderImage)
   176  	startTime := time.Now()
   177  	err := builder.docker.BuildImage(opts)
   178  	buildResult.BuildInfo.Stages = api.RecordStageAndStepInfo(buildResult.BuildInfo.Stages, api.StageBuild, api.StepBuildDockerImage, startTime, time.Now())
   179  	if err != nil {
   180  		buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
   181  			utilstatus.ReasonDockerImageBuildFailed,
   182  			utilstatus.ReasonMessageDockerImageBuildFailed,
   183  		)
   184  		return buildResult, err
   185  	}
   186  
   187  	// upon successful build we need to modify current config
   188  	builder.config.LayeredBuild = true
   189  	// new image name
   190  	builder.config.BuilderImage = newBuilderImage
   191  	// see CreateDockerfile, conditional copy, location of scripts
   192  	scriptsIncluded := checkValidDirWithContents(path.Join(config.WorkingDir, constants.UploadScripts))
   193  	log.V(2).Infof("Scripts dir has contents %v", scriptsIncluded)
   194  	if scriptsIncluded {
   195  		builder.config.ScriptsURL = "image://" + path.Join(getDestination(config), "scripts")
   196  	} else {
   197  		var err error
   198  		builder.config.ScriptsURL, err = builder.docker.GetScriptsURL(newBuilderImage)
   199  		if err != nil {
   200  			buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
   201  				utilstatus.ReasonGenericS2IBuildFailed,
   202  				utilstatus.ReasonMessageGenericS2iBuildFailed,
   203  			)
   204  			return buildResult, err
   205  		}
   206  	}
   207  
   208  	log.V(2).Infof("Building %s using sti-enabled image", builder.config.Tag)
   209  	startTime = time.Now()
   210  	err = builder.scripts.Execute(constants.Assemble, config.AssembleUser, builder.config)
   211  	buildResult.BuildInfo.Stages = api.RecordStageAndStepInfo(buildResult.BuildInfo.Stages, api.StageAssemble, api.StepAssembleBuildScripts, startTime, time.Now())
   212  	if err != nil {
   213  		buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
   214  			utilstatus.ReasonAssembleFailed,
   215  			utilstatus.ReasonMessageAssembleFailed,
   216  		)
   217  		switch e := err.(type) {
   218  		case s2ierr.ContainerError:
   219  			return buildResult, s2ierr.NewAssembleError(builder.config.Tag, e.Output, e)
   220  		default:
   221  			return buildResult, err
   222  		}
   223  	}
   224  	buildResult.Success = true
   225  
   226  	return buildResult, nil
   227  }