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