github.com/openshift/source-to-image@v1.4.1-0.20240516041539-bf52fc02204e/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/openshift/source-to-image/pkg/api" 13 "github.com/openshift/source-to-image/pkg/api/constants" 14 "github.com/openshift/source-to-image/pkg/build" 15 s2ierr "github.com/openshift/source-to-image/pkg/errors" 16 "github.com/openshift/source-to-image/pkg/ignore" 17 "github.com/openshift/source-to-image/pkg/scm" 18 "github.com/openshift/source-to-image/pkg/scm/downloaders/file" 19 "github.com/openshift/source-to-image/pkg/scm/git" 20 "github.com/openshift/source-to-image/pkg/scripts" 21 "github.com/openshift/source-to-image/pkg/util" 22 "github.com/openshift/source-to-image/pkg/util/fs" 23 utillog "github.com/openshift/source-to-image/pkg/util/log" 24 utilstatus "github.com/openshift/source-to-image/pkg/util/status" 25 "github.com/openshift/source-to-image/pkg/util/user" 26 ) 27 28 const ( 29 defaultDestination = "/tmp" 30 defaultScriptsDir = "/usr/libexec/s2i" 31 ) 32 33 var ( 34 log = utillog.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 log.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, providedScripts := getImageScriptsDir(config, builder) 131 132 if config.Incremental { 133 imageTag := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag) 134 if len(imageTag) == 0 { 135 return errors.New("Image tag is missing for incremental build") 136 } 137 // Incremental builds run via a multistage Dockerfile 138 buffer.WriteString(fmt.Sprintf("FROM %s as cached\n", imageTag)) 139 var artifactsScript string 140 if _, provided := providedScripts[constants.SaveArtifacts]; provided { 141 // switch to root to COPY and chown content 142 log.V(2).Infof("Override save-artifacts script is included in directory %q", builder.uploadScriptsDir) 143 buffer.WriteString("# Copying in override save-artifacts script\n") 144 buffer.WriteString("USER root\n") 145 artifactsScript = sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "save-artifacts"))) 146 uploadScript := sanitize(filepath.ToSlash(filepath.Join(builder.uploadScriptsDir, "save-artifacts"))) 147 buffer.WriteString(fmt.Sprintf("COPY %s %s\n", uploadScript, artifactsScript)) 148 buffer.WriteString(fmt.Sprintf("RUN chown %s:0 %s\n", sanitize(imageUser), artifactsScript)) 149 } else { 150 buffer.WriteString(fmt.Sprintf("# Save-artifacts script sourced from builder image based on user input or image metadata.\n")) 151 artifactsScript = sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "save-artifacts"))) 152 } 153 // switch to the image user if it is not root 154 if len(imageUser) > 0 && imageUser != "root" { 155 buffer.WriteString(fmt.Sprintf("USER %s\n", imageUser)) 156 } 157 buffer.WriteString(fmt.Sprintf("RUN if [ -s %[1]s ]; then %[1]s > %[2]s; else touch %[2]s; fi\n", artifactsScript, artifactsTar)) 158 } 159 160 // main stage of the Dockerfile 161 buffer.WriteString(fmt.Sprintf("FROM %s\n", config.BuilderImage)) 162 163 imageLabels := util.GenerateOutputImageLabels(builder.sourceInfo, config) 164 for k, v := range config.Labels { 165 imageLabels[k] = v 166 } 167 168 if len(config.ScriptsURL) > 0 { 169 imageLabels[constants.ScriptsURLLabel] = config.ScriptsURL 170 } 171 172 if len(config.Destination) > 0 { 173 imageLabels[constants.DestinationLabel] = config.Destination 174 } 175 176 if len(imageLabels) > 0 { 177 first := true 178 buffer.WriteString("LABEL ") 179 for k, v := range imageLabels { 180 if !first { 181 buffer.WriteString(fmt.Sprintf(" \\\n ")) 182 } 183 buffer.WriteString(fmt.Sprintf("%q=%q", k, v)) 184 first = false 185 } 186 buffer.WriteString("\n") 187 } 188 189 env := createBuildEnvironment(config.WorkingDir, config.Environment) 190 buffer.WriteString(fmt.Sprintf("%s", env)) 191 192 // run as root to COPY and chown source content 193 buffer.WriteString("USER root\n") 194 chownList := make([]string, 0) 195 196 if config.Incremental { 197 // COPY artifacts.tar from the `cached` stage 198 buffer.WriteString(fmt.Sprintf("COPY --from=cached %[1]s %[1]s\n", artifactsTar)) 199 chownList = append(chownList, artifactsTar) 200 } 201 202 if len(providedScripts) > 0 { 203 // Only COPY scripts dir if required scripts are present and needed. 204 // Even if the "scripts" dir exists, the COPY would fail if it was empty. 205 log.V(2).Infof("Override scripts are included in directory %q", builder.uploadScriptsDir) 206 scriptsDest := sanitize(filepath.ToSlash(scriptsDestDir)) 207 buffer.WriteString("# Copying in override assemble/run scripts\n") 208 buffer.WriteString(fmt.Sprintf("COPY %s %s\n", sanitize(filepath.ToSlash(builder.uploadScriptsDir)), scriptsDest)) 209 chownList = append(chownList, scriptsDest) 210 } 211 212 // copy in the user's source code. 213 buffer.WriteString("# Copying in source code\n") 214 sourceDest := sanitize(filepath.ToSlash(sourceDestDir)) 215 buffer.WriteString(fmt.Sprintf("COPY %s %s\n", sanitize(filepath.ToSlash(builder.uploadSrcDir)), sourceDest)) 216 chownList = append(chownList, sourceDest) 217 218 // add injections 219 log.V(4).Infof("Processing injected inputs: %#v", config.Injections) 220 config.Injections = util.FixInjectionsWithRelativePath(config.ImageWorkDir, config.Injections) 221 log.V(4).Infof("Processed injected inputs: %#v", config.Injections) 222 223 if len(config.Injections) > 0 { 224 buffer.WriteString("# Copying in injected content\n") 225 } 226 for _, injection := range config.Injections { 227 src := sanitize(filepath.ToSlash(filepath.Join(constants.Injections, injection.Source))) 228 dest := sanitize(filepath.ToSlash(injection.Destination)) 229 buffer.WriteString(fmt.Sprintf("COPY %s %s\n", src, dest)) 230 chownList = append(chownList, dest) 231 } 232 233 // chown directories COPYed to image 234 if len(chownList) > 0 { 235 buffer.WriteString("# Change file ownership to the assemble user. Builder image must support chown command.\n") 236 buffer.WriteString(fmt.Sprintf("RUN chown -R %s:0", sanitize(imageUser))) 237 for _, dir := range chownList { 238 buffer.WriteString(fmt.Sprintf(" %s", dir)) 239 } 240 buffer.WriteString("\n") 241 } 242 243 // run remaining commands as the image user 244 if len(imageUser) > 0 && imageUser != "root" { 245 buffer.WriteString(fmt.Sprintf("USER %s\n", imageUser)) 246 } 247 248 if config.Incremental { 249 buffer.WriteString("# Extract artifact content\n") 250 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)))) 251 buffer.WriteString(fmt.Sprintf(" rm %s\n", artifactsTar)) 252 } 253 254 if _, provided := providedScripts[constants.Assemble]; provided { 255 buffer.WriteString(fmt.Sprintf("RUN %s\n", sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "assemble"))))) 256 } else { 257 buffer.WriteString(fmt.Sprintf("# Assemble script sourced from builder image based on user input or image metadata.\n")) 258 buffer.WriteString(fmt.Sprintf("# If this file does not exist in the image, the build will fail.\n")) 259 buffer.WriteString(fmt.Sprintf("RUN %s\n", sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "assemble"))))) 260 } 261 262 filesToDelete, err := util.ListFilesToTruncate(builder.fs, config.Injections) 263 if err != nil { 264 return err 265 } 266 if len(filesToDelete) > 0 { 267 wroteRun := false 268 buffer.WriteString("# Cleaning up injected secret content\n") 269 for _, file := range filesToDelete { 270 if !wroteRun { 271 buffer.WriteString(fmt.Sprintf("RUN rm %s", file)) 272 wroteRun = true 273 continue 274 } 275 buffer.WriteString(fmt.Sprintf(" && \\\n")) 276 buffer.WriteString(fmt.Sprintf(" rm %s", file)) 277 } 278 buffer.WriteString("\n") 279 } 280 281 if _, provided := providedScripts[constants.Run]; provided { 282 buffer.WriteString(fmt.Sprintf("CMD %s\n", sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "run"))))) 283 } else { 284 buffer.WriteString(fmt.Sprintf("# Run script sourced from builder image based on user input or image metadata.\n")) 285 buffer.WriteString(fmt.Sprintf("# If this file does not exist in the image, the build will fail.\n")) 286 buffer.WriteString(fmt.Sprintf("CMD %s\n", sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "run"))))) 287 } 288 289 if err := builder.fs.WriteFile(filepath.Join(config.AsDockerfile), buffer.Bytes()); err != nil { 290 return err 291 } 292 log.V(2).Infof("Wrote custom Dockerfile to %s", config.AsDockerfile) 293 return nil 294 } 295 296 // Prepare prepares the source code and tar for build. 297 // NOTE: this func serves both the sti and onbuild strategies, as the OnBuild 298 // struct Build func leverages the STI struct Prepare func directly below. 299 func (builder *Dockerfile) Prepare(config *api.Config) error { 300 var err error 301 302 if len(config.WorkingDir) == 0 { 303 if config.WorkingDir, err = builder.fs.CreateWorkingDirectory(); err != nil { 304 builder.setFailureReason(utilstatus.ReasonFSOperationFailed, utilstatus.ReasonMessageFSOperationFailed) 305 return err 306 } 307 } 308 309 builder.result.WorkingDir = config.WorkingDir 310 311 // Setup working directories 312 for _, v := range workingDirs { 313 if err = builder.fs.MkdirAllWithPermissions(filepath.Join(config.WorkingDir, v), 0755); err != nil { 314 builder.setFailureReason(utilstatus.ReasonFSOperationFailed, utilstatus.ReasonMessageFSOperationFailed) 315 return err 316 } 317 } 318 319 // Default - install scripts specified by image metadata. 320 // Typically this will point to an image:// URL, and no scripts are downloaded. 321 // However, if builder image labels are specified, we'll go with those and not the default 322 if config.BuilderImageLabels == nil { 323 builder.installScripts(config.ImageScriptsURL, config) 324 } 325 326 // Fetch sources, since their .s2i/bin might contain s2i scripts which override defaults. 327 if config.Source != nil { 328 downloader, err := scm.DownloaderForSource(builder.fs, config.Source, config.ForceCopy) 329 if err != nil { 330 builder.setFailureReason(utilstatus.ReasonFetchSourceFailed, utilstatus.ReasonMessageFetchSourceFailed) 331 return err 332 } 333 if builder.sourceInfo, err = downloader.Download(config); err != nil { 334 builder.setFailureReason(utilstatus.ReasonFetchSourceFailed, utilstatus.ReasonMessageFetchSourceFailed) 335 switch err.(type) { 336 case file.RecursiveCopyError: 337 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) 338 } 339 return err 340 } 341 if config.SourceInfo != nil { 342 builder.sourceInfo = config.SourceInfo 343 } 344 } 345 346 // Install scripts provided by user, overriding all others. 347 // This _could_ be an image:// URL, which would override any scripts above. 348 urlScripts := builder.installScripts(config.ScriptsURL, config) 349 // If a ScriptsURL was specified, but no scripts were downloaded from it, throw an error 350 if len(config.ScriptsURL) > 0 { 351 failedCount := 0 352 for _, result := range urlScripts { 353 if util.Includes(result.FailedSources, scripts.ScriptURLHandler) { 354 failedCount++ 355 } 356 } 357 if failedCount == len(urlScripts) { 358 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason( 359 utilstatus.ReasonScriptsFetchFailed, 360 utilstatus.ReasonMessageScriptsFetchFailed, 361 ) 362 return fmt.Errorf("could not download any scripts from URL %v", config.ScriptsURL) 363 } 364 } 365 366 // Stage any injection(secrets) content into the working dir so the dockerfile can reference it. 367 for i, injection := range config.Injections { 368 // strip the C: from windows paths because it's not valid in the middle of a path 369 // like upload/injections/C:/tempdir/injection1 370 trimmedSrc := strings.TrimPrefix(injection.Source, filepath.VolumeName(injection.Source)) 371 dst := filepath.Join(config.WorkingDir, constants.Injections, trimmedSrc) 372 log.V(4).Infof("Copying injection content from %s to %s", injection.Source, dst) 373 if err := builder.fs.CopyContents(injection.Source, dst, nil); err != nil { 374 builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed) 375 return err 376 } 377 config.Injections[i].Source = trimmedSrc 378 } 379 380 // see if there is a .s2iignore file, and if so, read in the patterns and then 381 // search and delete on them. 382 err = builder.ignorer.Ignore(config) 383 if err != nil { 384 builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed) 385 return err 386 } 387 return nil 388 } 389 390 // installScripts installs scripts at the provided URL to the Dockerfile context 391 func (builder *Dockerfile) installScripts(scriptsURL string, config *api.Config) []api.InstallResult { 392 scriptInstaller := scripts.NewInstaller( 393 "", 394 scriptsURL, 395 config.ScriptDownloadProxyConfig, 396 nil, 397 api.AuthConfig{}, 398 builder.fs, 399 config, 400 ) 401 402 // all scripts are optional, we trust the image contains scripts if we don't find them 403 // in the source repo. 404 return scriptInstaller.InstallOptional(append(scripts.RequiredScripts, scripts.OptionalScripts...), config.WorkingDir) 405 } 406 407 // setFailureReason sets the builder's failure reason with the given reason and message. 408 func (builder *Dockerfile) setFailureReason(reason api.StepFailureReason, message api.StepFailureMessage) { 409 builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(reason, message) 410 } 411 412 // getDestination returns the destination directory from the config. 413 func getDestination(config *api.Config) string { 414 destination := config.Destination 415 if len(destination) == 0 { 416 destination = defaultDestination 417 } 418 return destination 419 } 420 421 // getImageScriptsDir returns the default directory which should contain the builder image scripts 422 // as well as a map of booleans identifying individual scripts provided in the repository as overrides 423 func getImageScriptsDir(config *api.Config, builder *Dockerfile) (string, map[string]bool) { 424 425 // 1st priority is the command line parameter (pointing to an image, overrides it all) 426 if strings.HasPrefix(config.ScriptsURL, "image://") { 427 return strings.TrimPrefix(config.ScriptsURL, "image://"), make(map[string]bool) 428 } 429 430 // 2nd priority (the source code repository), collect the locations 431 providedScripts := scanScripts(filepath.Join(config.WorkingDir, builder.uploadScriptsDir)) 432 433 // 3rd priority (the builder image), collect the locations 434 scriptsURL, _ := util.AdjustConfigWithImageLabels(config) 435 if strings.HasPrefix(scriptsURL, "image://") { 436 return strings.TrimPrefix(scriptsURL, "image://"), providedScripts 437 } 438 if strings.HasPrefix(config.ImageScriptsURL, "image://") { 439 return strings.TrimPrefix(config.ImageScriptsURL, "image://"), providedScripts 440 } 441 442 // If all else fails, use the default scripts dir 443 return defaultScriptsDir, providedScripts 444 } 445 446 // scanScripts returns a map of provided s2i scripts 447 func scanScripts(name string) map[string]bool { 448 scriptsMap := make(map[string]bool) 449 items, err := ioutil.ReadDir(name) 450 if os.IsNotExist(err) { 451 log.Warningf("Unable to access directory %q: %v", name, err) 452 } 453 if err != nil || len(items) == 0 { 454 return scriptsMap 455 } 456 457 assembleProvided := false 458 runProvided := false 459 saveArtifactsProvided := false 460 for _, f := range items { 461 log.V(2).Infof("found override script file %s", f.Name()) 462 if f.Name() == constants.Run { 463 runProvided = true 464 scriptsMap[constants.Run] = true 465 } else if f.Name() == constants.Assemble { 466 assembleProvided = true 467 scriptsMap[constants.Assemble] = true 468 } else if f.Name() == constants.SaveArtifacts { 469 saveArtifactsProvided = true 470 scriptsMap[constants.SaveArtifacts] = true 471 } 472 if runProvided && assembleProvided && saveArtifactsProvided { 473 break 474 } 475 } 476 return scriptsMap 477 } 478 479 func includes(arr []string, str string) bool { 480 for _, s := range arr { 481 if s == str { 482 return true 483 } 484 } 485 return false 486 } 487 488 func sanitize(s string) string { 489 return strings.Replace(s, "\n", "\\n", -1) 490 } 491 492 func createBuildEnvironment(sourcePath string, cfgEnv api.EnvironmentList) string { 493 s2iEnv, err := scripts.GetEnvironment(filepath.Join(sourcePath, constants.Source)) 494 if err != nil { 495 log.V(3).Infof("No user environment provided (%v)", err) 496 } 497 498 return scripts.ConvertEnvironmentToDocker(append(s2iEnv, cfgEnv...)) 499 }