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