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 }