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 }