github.com/lazyboychen7/engine@v17.12.1-ce-rc2+incompatible/builder/dockerfile/internals.go (about) 1 package dockerfile 2 3 // internals for handling commands. Covers many areas and a lot of 4 // non-contiguous functionality. Please read the comments. 5 6 import ( 7 "crypto/sha256" 8 "encoding/hex" 9 "fmt" 10 "io" 11 "os" 12 "path" 13 "path/filepath" 14 "strings" 15 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/api/types/backend" 18 "github.com/docker/docker/api/types/container" 19 "github.com/docker/docker/image" 20 "github.com/docker/docker/pkg/archive" 21 "github.com/docker/docker/pkg/chrootarchive" 22 "github.com/docker/docker/pkg/containerfs" 23 "github.com/docker/docker/pkg/idtools" 24 "github.com/docker/docker/pkg/stringid" 25 "github.com/docker/docker/pkg/system" 26 "github.com/docker/go-connections/nat" 27 "github.com/pkg/errors" 28 ) 29 30 // Archiver defines an interface for copying files from one destination to 31 // another using Tar/Untar. 32 type Archiver interface { 33 TarUntar(src, dst string) error 34 UntarPath(src, dst string) error 35 CopyWithTar(src, dst string) error 36 CopyFileWithTar(src, dst string) error 37 IDMappings() *idtools.IDMappings 38 } 39 40 // The builder will use the following interfaces if the container fs implements 41 // these for optimized copies to and from the container. 42 type extractor interface { 43 ExtractArchive(src io.Reader, dst string, opts *archive.TarOptions) error 44 } 45 46 type archiver interface { 47 ArchivePath(src string, opts *archive.TarOptions) (io.ReadCloser, error) 48 } 49 50 // helper functions to get tar/untar func 51 func untarFunc(i interface{}) containerfs.UntarFunc { 52 if ea, ok := i.(extractor); ok { 53 return ea.ExtractArchive 54 } 55 return chrootarchive.Untar 56 } 57 58 func tarFunc(i interface{}) containerfs.TarFunc { 59 if ap, ok := i.(archiver); ok { 60 return ap.ArchivePath 61 } 62 return archive.TarWithOptions 63 } 64 65 func (b *Builder) getArchiver(src, dst containerfs.Driver) Archiver { 66 t, u := tarFunc(src), untarFunc(dst) 67 return &containerfs.Archiver{ 68 SrcDriver: src, 69 DstDriver: dst, 70 Tar: t, 71 Untar: u, 72 IDMappingsVar: b.idMappings, 73 } 74 } 75 76 func (b *Builder) commit(dispatchState *dispatchState, comment string) error { 77 if b.disableCommit { 78 return nil 79 } 80 if !dispatchState.hasFromImage() { 81 return errors.New("Please provide a source image with `from` prior to commit") 82 } 83 84 optionsPlatform := system.ParsePlatform(b.options.Platform) 85 runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment, optionsPlatform.OS)) 86 hit, err := b.probeCache(dispatchState, runConfigWithCommentCmd) 87 if err != nil || hit { 88 return err 89 } 90 id, err := b.create(runConfigWithCommentCmd) 91 if err != nil { 92 return err 93 } 94 95 return b.commitContainer(dispatchState, id, runConfigWithCommentCmd) 96 } 97 98 func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error { 99 if b.disableCommit { 100 return nil 101 } 102 103 commitCfg := &backend.ContainerCommitConfig{ 104 ContainerCommitConfig: types.ContainerCommitConfig{ 105 Author: dispatchState.maintainer, 106 Pause: true, 107 // TODO: this should be done by Commit() 108 Config: copyRunConfig(dispatchState.runConfig), 109 }, 110 ContainerConfig: containerConfig, 111 } 112 113 // Commit the container 114 imageID, err := b.docker.Commit(id, commitCfg) 115 if err != nil { 116 return err 117 } 118 119 dispatchState.imageID = imageID 120 return nil 121 } 122 123 func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runConfig *container.Config) error { 124 optionsPlatform := system.ParsePlatform(b.options.Platform) 125 newLayer, err := imageMount.Layer().Commit(optionsPlatform.OS) 126 if err != nil { 127 return err 128 } 129 130 // add an image mount without an image so the layer is properly unmounted 131 // if there is an error before we can add the full mount with image 132 b.imageSources.Add(newImageMount(nil, newLayer)) 133 134 parentImage, ok := imageMount.Image().(*image.Image) 135 if !ok { 136 return errors.Errorf("unexpected image type") 137 } 138 139 newImage := image.NewChildImage(parentImage, image.ChildConfig{ 140 Author: state.maintainer, 141 ContainerConfig: runConfig, 142 DiffID: newLayer.DiffID(), 143 Config: copyRunConfig(state.runConfig), 144 }, parentImage.OS) 145 146 // TODO: it seems strange to marshal this here instead of just passing in the 147 // image struct 148 config, err := newImage.MarshalJSON() 149 if err != nil { 150 return errors.Wrap(err, "failed to encode image config") 151 } 152 153 exportedImage, err := b.docker.CreateImage(config, state.imageID, parentImage.OS) 154 if err != nil { 155 return errors.Wrapf(err, "failed to export image") 156 } 157 158 state.imageID = exportedImage.ImageID() 159 b.imageSources.Add(newImageMount(exportedImage, newLayer)) 160 return nil 161 } 162 163 func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error { 164 srcHash := getSourceHashFromInfos(inst.infos) 165 166 var chownComment string 167 if inst.chownStr != "" { 168 chownComment = fmt.Sprintf("--chown=%s", inst.chownStr) 169 } 170 commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest) 171 172 // TODO: should this have been using origPaths instead of srcHash in the comment? 173 optionsPlatform := system.ParsePlatform(b.options.Platform) 174 runConfigWithCommentCmd := copyRunConfig( 175 state.runConfig, 176 withCmdCommentString(commentStr, optionsPlatform.OS)) 177 hit, err := b.probeCache(state, runConfigWithCommentCmd) 178 if err != nil || hit { 179 return err 180 } 181 182 imageMount, err := b.imageSources.Get(state.imageID, true) 183 if err != nil { 184 return errors.Wrapf(err, "failed to get destination image %q", state.imageID) 185 } 186 187 destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, imageMount, b.options.Platform) 188 if err != nil { 189 return err 190 } 191 192 chownPair := b.idMappings.RootPair() 193 // if a chown was requested, perform the steps to get the uid, gid 194 // translated (if necessary because of user namespaces), and replace 195 // the root pair with the chown pair for copy operations 196 if inst.chownStr != "" { 197 chownPair, err = parseChownFlag(inst.chownStr, destInfo.root.Path(), b.idMappings) 198 if err != nil { 199 return errors.Wrapf(err, "unable to convert uid/gid chown string to host mapping") 200 } 201 } 202 203 for _, info := range inst.infos { 204 opts := copyFileOptions{ 205 decompress: inst.allowLocalDecompression, 206 archiver: b.getArchiver(info.root, destInfo.root), 207 chownPair: chownPair, 208 } 209 if err := performCopyForInfo(destInfo, info, opts); err != nil { 210 return errors.Wrapf(err, "failed to copy files") 211 } 212 } 213 return b.exportImage(state, imageMount, runConfigWithCommentCmd) 214 } 215 216 func createDestInfo(workingDir string, inst copyInstruction, imageMount *imageMount, platform string) (copyInfo, error) { 217 // Twiddle the destination when it's a relative path - meaning, make it 218 // relative to the WORKINGDIR 219 dest, err := normalizeDest(workingDir, inst.dest, platform) 220 if err != nil { 221 return copyInfo{}, errors.Wrapf(err, "invalid %s", inst.cmdName) 222 } 223 224 destMount, err := imageMount.Source() 225 if err != nil { 226 return copyInfo{}, errors.Wrapf(err, "failed to mount copy source") 227 } 228 229 return newCopyInfoFromSource(destMount, dest, ""), nil 230 } 231 232 // normalizeDest normalises the destination of a COPY/ADD command in a 233 // platform semantically consistent way. 234 func normalizeDest(workingDir, requested string, platform string) (string, error) { 235 dest := fromSlash(requested, platform) 236 endsInSlash := strings.HasSuffix(dest, string(separator(platform))) 237 238 if platform != "windows" { 239 if !path.IsAbs(requested) { 240 dest = path.Join("/", filepath.ToSlash(workingDir), dest) 241 // Make sure we preserve any trailing slash 242 if endsInSlash { 243 dest += "/" 244 } 245 } 246 return dest, nil 247 } 248 249 // We are guaranteed that the working directory is already consistent, 250 // However, Windows also has, for now, the limitation that ADD/COPY can 251 // only be done to the system drive, not any drives that might be present 252 // as a result of a bind mount. 253 // 254 // So... if the path requested is Linux-style absolute (/foo or \\foo), 255 // we assume it is the system drive. If it is a Windows-style absolute 256 // (DRIVE:\\foo), error if DRIVE is not C. And finally, ensure we 257 // strip any configured working directories drive letter so that it 258 // can be subsequently legitimately converted to a Windows volume-style 259 // pathname. 260 261 // Not a typo - filepath.IsAbs, not system.IsAbs on this next check as 262 // we only want to validate where the DriveColon part has been supplied. 263 if filepath.IsAbs(dest) { 264 if strings.ToUpper(string(dest[0])) != "C" { 265 return "", fmt.Errorf("Windows does not support destinations not on the system drive (C:)") 266 } 267 dest = dest[2:] // Strip the drive letter 268 } 269 270 // Cannot handle relative where WorkingDir is not the system drive. 271 if len(workingDir) > 0 { 272 if ((len(workingDir) > 1) && !system.IsAbs(workingDir[2:])) || (len(workingDir) == 1) { 273 return "", fmt.Errorf("Current WorkingDir %s is not platform consistent", workingDir) 274 } 275 if !system.IsAbs(dest) { 276 if string(workingDir[0]) != "C" { 277 return "", fmt.Errorf("Windows does not support relative paths when WORKDIR is not the system drive") 278 } 279 dest = filepath.Join(string(os.PathSeparator), workingDir[2:], dest) 280 // Make sure we preserve any trailing slash 281 if endsInSlash { 282 dest += string(os.PathSeparator) 283 } 284 } 285 } 286 return dest, nil 287 } 288 289 // For backwards compat, if there's just one info then use it as the 290 // cache look-up string, otherwise hash 'em all into one 291 func getSourceHashFromInfos(infos []copyInfo) string { 292 if len(infos) == 1 { 293 return infos[0].hash 294 } 295 var hashs []string 296 for _, info := range infos { 297 hashs = append(hashs, info.hash) 298 } 299 return hashStringSlice("multi", hashs) 300 } 301 302 func hashStringSlice(prefix string, slice []string) string { 303 hasher := sha256.New() 304 hasher.Write([]byte(strings.Join(slice, ","))) 305 return prefix + ":" + hex.EncodeToString(hasher.Sum(nil)) 306 } 307 308 type runConfigModifier func(*container.Config) 309 310 func withCmd(cmd []string) runConfigModifier { 311 return func(runConfig *container.Config) { 312 runConfig.Cmd = cmd 313 } 314 } 315 316 // withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for 317 // why there are two almost identical versions of this. 318 func withCmdComment(comment string, platform string) runConfigModifier { 319 return func(runConfig *container.Config) { 320 runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) ", comment) 321 } 322 } 323 324 // withCmdCommentString exists to maintain compatibility with older versions. 325 // A few instructions (workdir, copy, add) used a nop comment that is a single arg 326 // where as all the other instructions used a two arg comment string. This 327 // function implements the single arg version. 328 func withCmdCommentString(comment string, platform string) runConfigModifier { 329 return func(runConfig *container.Config) { 330 runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) "+comment) 331 } 332 } 333 334 func withEnv(env []string) runConfigModifier { 335 return func(runConfig *container.Config) { 336 runConfig.Env = env 337 } 338 } 339 340 // withEntrypointOverride sets an entrypoint on runConfig if the command is 341 // not empty. The entrypoint is left unmodified if command is empty. 342 // 343 // The dockerfile RUN instruction expect to run without an entrypoint 344 // so the runConfig entrypoint needs to be modified accordingly. ContainerCreate 345 // will change a []string{""} entrypoint to nil, so we probe the cache with the 346 // nil entrypoint. 347 func withEntrypointOverride(cmd []string, entrypoint []string) runConfigModifier { 348 return func(runConfig *container.Config) { 349 if len(cmd) > 0 { 350 runConfig.Entrypoint = entrypoint 351 } 352 } 353 } 354 355 func copyRunConfig(runConfig *container.Config, modifiers ...runConfigModifier) *container.Config { 356 copy := *runConfig 357 copy.Cmd = copyStringSlice(runConfig.Cmd) 358 copy.Env = copyStringSlice(runConfig.Env) 359 copy.Entrypoint = copyStringSlice(runConfig.Entrypoint) 360 copy.OnBuild = copyStringSlice(runConfig.OnBuild) 361 copy.Shell = copyStringSlice(runConfig.Shell) 362 363 if copy.Volumes != nil { 364 copy.Volumes = make(map[string]struct{}, len(runConfig.Volumes)) 365 for k, v := range runConfig.Volumes { 366 copy.Volumes[k] = v 367 } 368 } 369 370 if copy.ExposedPorts != nil { 371 copy.ExposedPorts = make(nat.PortSet, len(runConfig.ExposedPorts)) 372 for k, v := range runConfig.ExposedPorts { 373 copy.ExposedPorts[k] = v 374 } 375 } 376 377 if copy.Labels != nil { 378 copy.Labels = make(map[string]string, len(runConfig.Labels)) 379 for k, v := range runConfig.Labels { 380 copy.Labels[k] = v 381 } 382 } 383 384 for _, modifier := range modifiers { 385 modifier(©) 386 } 387 return © 388 } 389 390 func copyStringSlice(orig []string) []string { 391 if orig == nil { 392 return nil 393 } 394 return append([]string{}, orig...) 395 } 396 397 // getShell is a helper function which gets the right shell for prefixing the 398 // shell-form of RUN, ENTRYPOINT and CMD instructions 399 func getShell(c *container.Config, os string) []string { 400 if 0 == len(c.Shell) { 401 return append([]string{}, defaultShellForOS(os)[:]...) 402 } 403 return append([]string{}, c.Shell[:]...) 404 } 405 406 func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) { 407 cachedID, err := b.imageProber.Probe(dispatchState.imageID, runConfig) 408 if cachedID == "" || err != nil { 409 return false, err 410 } 411 fmt.Fprint(b.Stdout, " ---> Using cache\n") 412 413 dispatchState.imageID = cachedID 414 return true, nil 415 } 416 417 var defaultLogConfig = container.LogConfig{Type: "none"} 418 419 func (b *Builder) probeAndCreate(dispatchState *dispatchState, runConfig *container.Config) (string, error) { 420 if hit, err := b.probeCache(dispatchState, runConfig); err != nil || hit { 421 return "", err 422 } 423 // Set a log config to override any default value set on the daemon 424 hostConfig := &container.HostConfig{LogConfig: defaultLogConfig} 425 optionsPlatform := system.ParsePlatform(b.options.Platform) 426 container, err := b.containerManager.Create(runConfig, hostConfig, optionsPlatform.OS) 427 return container.ID, err 428 } 429 430 func (b *Builder) create(runConfig *container.Config) (string, error) { 431 hostConfig := hostConfigFromOptions(b.options) 432 optionsPlatform := system.ParsePlatform(b.options.Platform) 433 container, err := b.containerManager.Create(runConfig, hostConfig, optionsPlatform.OS) 434 if err != nil { 435 return "", err 436 } 437 // TODO: could this be moved into containerManager.Create() ? 438 for _, warning := range container.Warnings { 439 fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning) 440 } 441 fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(container.ID)) 442 return container.ID, nil 443 } 444 445 func hostConfigFromOptions(options *types.ImageBuildOptions) *container.HostConfig { 446 resources := container.Resources{ 447 CgroupParent: options.CgroupParent, 448 CPUShares: options.CPUShares, 449 CPUPeriod: options.CPUPeriod, 450 CPUQuota: options.CPUQuota, 451 CpusetCpus: options.CPUSetCPUs, 452 CpusetMems: options.CPUSetMems, 453 Memory: options.Memory, 454 MemorySwap: options.MemorySwap, 455 Ulimits: options.Ulimits, 456 } 457 458 return &container.HostConfig{ 459 SecurityOpt: options.SecurityOpt, 460 Isolation: options.Isolation, 461 ShmSize: options.ShmSize, 462 Resources: resources, 463 NetworkMode: container.NetworkMode(options.NetworkMode), 464 // Set a log config to override any default value set on the daemon 465 LogConfig: defaultLogConfig, 466 ExtraHosts: options.ExtraHosts, 467 } 468 } 469 470 // fromSlash works like filepath.FromSlash but with a given OS platform field 471 func fromSlash(path, platform string) string { 472 if platform == "windows" { 473 return strings.Replace(path, "/", "\\", -1) 474 } 475 return path 476 } 477 478 // separator returns a OS path separator for the given OS platform 479 func separator(platform string) byte { 480 if platform == "windows" { 481 return '\\' 482 } 483 return '/' 484 }