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 }