github.com/rumpl/bof@v23.0.0-rc.2+incompatible/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 "strings" 12 13 "github.com/docker/docker/api/types" 14 "github.com/docker/docker/api/types/backend" 15 "github.com/docker/docker/api/types/container" 16 "github.com/docker/docker/builder" 17 "github.com/docker/docker/image" 18 "github.com/docker/docker/pkg/archive" 19 "github.com/docker/docker/pkg/chrootarchive" 20 "github.com/docker/docker/pkg/containerfs" 21 "github.com/docker/docker/pkg/idtools" 22 "github.com/docker/docker/pkg/stringid" 23 "github.com/docker/go-connections/nat" 24 specs "github.com/opencontainers/image-spec/specs-go/v1" 25 "github.com/pkg/errors" 26 "github.com/sirupsen/logrus" 27 ) 28 29 // Archiver defines an interface for copying files from one destination to 30 // another using Tar/Untar. 31 type Archiver interface { 32 TarUntar(src, dst string) error 33 UntarPath(src, dst string) error 34 CopyWithTar(src, dst string) error 35 CopyFileWithTar(src, dst string) error 36 IdentityMapping() idtools.IdentityMapping 37 } 38 39 // The builder will use the following interfaces if the container fs implements 40 // these for optimized copies to and from the container. 41 type extractor interface { 42 ExtractArchive(src io.Reader, dst string, opts *archive.TarOptions) error 43 } 44 45 type archiver interface { 46 ArchivePath(src string, opts *archive.TarOptions) (io.ReadCloser, error) 47 } 48 49 // helper functions to get tar/untar func 50 func untarFunc(i interface{}) containerfs.UntarFunc { 51 if ea, ok := i.(extractor); ok { 52 return ea.ExtractArchive 53 } 54 return chrootarchive.Untar 55 } 56 57 func tarFunc(i interface{}) containerfs.TarFunc { 58 if ap, ok := i.(archiver); ok { 59 return ap.ArchivePath 60 } 61 return archive.TarWithOptions 62 } 63 64 func (b *Builder) getArchiver(src, dst containerfs.Driver) Archiver { 65 t, u := tarFunc(src), untarFunc(dst) 66 return &containerfs.Archiver{ 67 SrcDriver: src, 68 DstDriver: dst, 69 Tar: t, 70 Untar: u, 71 IDMapping: b.idMapping, 72 } 73 } 74 75 func (b *Builder) commit(dispatchState *dispatchState, comment string) error { 76 if b.disableCommit { 77 return nil 78 } 79 if !dispatchState.hasFromImage() { 80 return errors.New("Please provide a source image with `from` prior to commit") 81 } 82 83 runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment, dispatchState.operatingSystem)) 84 id, err := b.probeAndCreate(dispatchState, runConfigWithCommentCmd) 85 if err != nil || id == "" { 86 return err 87 } 88 89 return b.commitContainer(dispatchState, id, runConfigWithCommentCmd) 90 } 91 92 func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error { 93 if b.disableCommit { 94 return nil 95 } 96 97 commitCfg := backend.CommitConfig{ 98 Author: dispatchState.maintainer, 99 // TODO: this copy should be done by Commit() 100 Config: copyRunConfig(dispatchState.runConfig), 101 ContainerConfig: containerConfig, 102 ContainerID: id, 103 } 104 105 imageID, err := b.docker.CommitBuildStep(commitCfg) 106 dispatchState.imageID = string(imageID) 107 return err 108 } 109 110 func (b *Builder) exportImage(state *dispatchState, layer builder.RWLayer, parent builder.Image, runConfig *container.Config) error { 111 newLayer, err := layer.Commit() 112 if err != nil { 113 return err 114 } 115 116 parentImage, ok := parent.(*image.Image) 117 if !ok { 118 return errors.Errorf("unexpected image type") 119 } 120 121 platform := &specs.Platform{ 122 OS: parentImage.OS, 123 Architecture: parentImage.Architecture, 124 Variant: parentImage.Variant, 125 } 126 127 // add an image mount without an image so the layer is properly unmounted 128 // if there is an error before we can add the full mount with image 129 b.imageSources.Add(newImageMount(nil, newLayer), platform) 130 131 newImage := image.NewChildImage(parentImage, image.ChildConfig{ 132 Author: state.maintainer, 133 ContainerConfig: runConfig, 134 DiffID: newLayer.DiffID(), 135 Config: copyRunConfig(state.runConfig), 136 }, parentImage.OS) 137 138 // TODO: it seems strange to marshal this here instead of just passing in the 139 // image struct 140 config, err := newImage.MarshalJSON() 141 if err != nil { 142 return errors.Wrap(err, "failed to encode image config") 143 } 144 145 exportedImage, err := b.docker.CreateImage(config, state.imageID) 146 if err != nil { 147 return errors.Wrapf(err, "failed to export image") 148 } 149 150 state.imageID = exportedImage.ImageID() 151 b.imageSources.Add(newImageMount(exportedImage, newLayer), platform) 152 return nil 153 } 154 155 func (b *Builder) performCopy(req dispatchRequest, inst copyInstruction) error { 156 state := req.state 157 srcHash := getSourceHashFromInfos(inst.infos) 158 159 var chownComment string 160 if inst.chownStr != "" { 161 chownComment = fmt.Sprintf("--chown=%s", inst.chownStr) 162 } 163 commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest) 164 165 // TODO: should this have been using origPaths instead of srcHash in the comment? 166 runConfigWithCommentCmd := copyRunConfig( 167 state.runConfig, 168 withCmdCommentString(commentStr, state.operatingSystem)) 169 hit, err := b.probeCache(state, runConfigWithCommentCmd) 170 if err != nil || hit { 171 return err 172 } 173 174 imageMount, err := b.imageSources.Get(state.imageID, true, req.builder.platform) 175 if err != nil { 176 return errors.Wrapf(err, "failed to get destination image %q", state.imageID) 177 } 178 179 rwLayer, err := imageMount.NewRWLayer() 180 if err != nil { 181 return err 182 } 183 defer rwLayer.Release() 184 185 destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, rwLayer, state.operatingSystem) 186 if err != nil { 187 return err 188 } 189 190 identity := b.idMapping.RootPair() 191 // if a chown was requested, perform the steps to get the uid, gid 192 // translated (if necessary because of user namespaces), and replace 193 // the root pair with the chown pair for copy operations 194 if inst.chownStr != "" { 195 identity, err = parseChownFlag(b, state, inst.chownStr, destInfo.root.Path(), b.idMapping) 196 if err != nil { 197 if b.options.Platform != "windows" { 198 return errors.Wrapf(err, "unable to convert uid/gid chown string to host mapping") 199 } 200 201 return errors.Wrapf(err, "unable to map container user account name to SID") 202 } 203 } 204 205 for _, info := range inst.infos { 206 opts := copyFileOptions{ 207 decompress: inst.allowLocalDecompression, 208 archiver: b.getArchiver(info.root, destInfo.root), 209 } 210 if !inst.preserveOwnership { 211 opts.identity = &identity 212 } 213 if err := performCopyForInfo(destInfo, info, opts); err != nil { 214 return errors.Wrapf(err, "failed to copy files") 215 } 216 } 217 return b.exportImage(state, rwLayer, imageMount.Image(), runConfigWithCommentCmd) 218 } 219 220 func createDestInfo(workingDir string, inst copyInstruction, rwLayer builder.RWLayer, platform string) (copyInfo, error) { 221 // Twiddle the destination when it's a relative path - meaning, make it 222 // relative to the WORKINGDIR 223 dest, err := normalizeDest(workingDir, inst.dest) 224 if err != nil { 225 return copyInfo{}, errors.Wrapf(err, "invalid %s", inst.cmdName) 226 } 227 228 return copyInfo{root: rwLayer.Root(), path: dest}, nil 229 } 230 231 // For backwards compat, if there's just one info then use it as the 232 // cache look-up string, otherwise hash 'em all into one 233 func getSourceHashFromInfos(infos []copyInfo) string { 234 if len(infos) == 1 { 235 return infos[0].hash 236 } 237 var hashs []string 238 for _, info := range infos { 239 hashs = append(hashs, info.hash) 240 } 241 return hashStringSlice("multi", hashs) 242 } 243 244 func hashStringSlice(prefix string, slice []string) string { 245 hasher := sha256.New() 246 hasher.Write([]byte(strings.Join(slice, ","))) 247 return prefix + ":" + hex.EncodeToString(hasher.Sum(nil)) 248 } 249 250 type runConfigModifier func(*container.Config) 251 252 func withCmd(cmd []string) runConfigModifier { 253 return func(runConfig *container.Config) { 254 runConfig.Cmd = cmd 255 } 256 } 257 258 func withArgsEscaped(argsEscaped bool) runConfigModifier { 259 return func(runConfig *container.Config) { 260 runConfig.ArgsEscaped = argsEscaped 261 } 262 } 263 264 // withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for 265 // why there are two almost identical versions of this. 266 func withCmdComment(comment string, platform string) runConfigModifier { 267 return func(runConfig *container.Config) { 268 runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) ", comment) 269 } 270 } 271 272 // withCmdCommentString exists to maintain compatibility with older versions. 273 // A few instructions (workdir, copy, add) used a nop comment that is a single arg 274 // where as all the other instructions used a two arg comment string. This 275 // function implements the single arg version. 276 func withCmdCommentString(comment string, platform string) runConfigModifier { 277 return func(runConfig *container.Config) { 278 runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) "+comment) 279 } 280 } 281 282 func withEnv(env []string) runConfigModifier { 283 return func(runConfig *container.Config) { 284 runConfig.Env = env 285 } 286 } 287 288 // withEntrypointOverride sets an entrypoint on runConfig if the command is 289 // not empty. The entrypoint is left unmodified if command is empty. 290 // 291 // The dockerfile RUN instruction expect to run without an entrypoint 292 // so the runConfig entrypoint needs to be modified accordingly. ContainerCreate 293 // will change a []string{""} entrypoint to nil, so we probe the cache with the 294 // nil entrypoint. 295 func withEntrypointOverride(cmd []string, entrypoint []string) runConfigModifier { 296 return func(runConfig *container.Config) { 297 if len(cmd) > 0 { 298 runConfig.Entrypoint = entrypoint 299 } 300 } 301 } 302 303 // withoutHealthcheck disables healthcheck. 304 // 305 // The dockerfile RUN instruction expect to run without healthcheck 306 // so the runConfig Healthcheck needs to be disabled. 307 func withoutHealthcheck() runConfigModifier { 308 return func(runConfig *container.Config) { 309 runConfig.Healthcheck = &container.HealthConfig{ 310 Test: []string{"NONE"}, 311 } 312 } 313 } 314 315 func copyRunConfig(runConfig *container.Config, modifiers ...runConfigModifier) *container.Config { 316 copy := *runConfig 317 copy.Cmd = copyStringSlice(runConfig.Cmd) 318 copy.Env = copyStringSlice(runConfig.Env) 319 copy.Entrypoint = copyStringSlice(runConfig.Entrypoint) 320 copy.OnBuild = copyStringSlice(runConfig.OnBuild) 321 copy.Shell = copyStringSlice(runConfig.Shell) 322 323 if copy.Volumes != nil { 324 copy.Volumes = make(map[string]struct{}, len(runConfig.Volumes)) 325 for k, v := range runConfig.Volumes { 326 copy.Volumes[k] = v 327 } 328 } 329 330 if copy.ExposedPorts != nil { 331 copy.ExposedPorts = make(nat.PortSet, len(runConfig.ExposedPorts)) 332 for k, v := range runConfig.ExposedPorts { 333 copy.ExposedPorts[k] = v 334 } 335 } 336 337 if copy.Labels != nil { 338 copy.Labels = make(map[string]string, len(runConfig.Labels)) 339 for k, v := range runConfig.Labels { 340 copy.Labels[k] = v 341 } 342 } 343 344 for _, modifier := range modifiers { 345 modifier(©) 346 } 347 return © 348 } 349 350 func copyStringSlice(orig []string) []string { 351 if orig == nil { 352 return nil 353 } 354 return append([]string{}, orig...) 355 } 356 357 // getShell is a helper function which gets the right shell for prefixing the 358 // shell-form of RUN, ENTRYPOINT and CMD instructions 359 func getShell(c *container.Config, os string) []string { 360 if 0 == len(c.Shell) { 361 return append([]string{}, defaultShellForOS(os)[:]...) 362 } 363 return append([]string{}, c.Shell[:]...) 364 } 365 366 func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) { 367 cachedID, err := b.imageProber.Probe(dispatchState.imageID, runConfig) 368 if cachedID == "" || err != nil { 369 return false, err 370 } 371 fmt.Fprint(b.Stdout, " ---> Using cache\n") 372 373 dispatchState.imageID = cachedID 374 return true, nil 375 } 376 377 var defaultLogConfig = container.LogConfig{Type: "none"} 378 379 func (b *Builder) probeAndCreate(dispatchState *dispatchState, runConfig *container.Config) (string, error) { 380 if hit, err := b.probeCache(dispatchState, runConfig); err != nil || hit { 381 return "", err 382 } 383 return b.create(runConfig) 384 } 385 386 func (b *Builder) create(runConfig *container.Config) (string, error) { 387 logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd) 388 389 hostConfig := hostConfigFromOptions(b.options) 390 container, err := b.containerManager.Create(runConfig, hostConfig) 391 if err != nil { 392 return "", err 393 } 394 // TODO: could this be moved into containerManager.Create() ? 395 for _, warning := range container.Warnings { 396 fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning) 397 } 398 fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(container.ID)) 399 return container.ID, nil 400 } 401 402 func hostConfigFromOptions(options *types.ImageBuildOptions) *container.HostConfig { 403 resources := container.Resources{ 404 CgroupParent: options.CgroupParent, 405 CPUShares: options.CPUShares, 406 CPUPeriod: options.CPUPeriod, 407 CPUQuota: options.CPUQuota, 408 CpusetCpus: options.CPUSetCPUs, 409 CpusetMems: options.CPUSetMems, 410 Memory: options.Memory, 411 MemorySwap: options.MemorySwap, 412 Ulimits: options.Ulimits, 413 } 414 415 hc := &container.HostConfig{ 416 SecurityOpt: options.SecurityOpt, 417 Isolation: options.Isolation, 418 ShmSize: options.ShmSize, 419 Resources: resources, 420 NetworkMode: container.NetworkMode(options.NetworkMode), 421 // Set a log config to override any default value set on the daemon 422 LogConfig: defaultLogConfig, 423 ExtraHosts: options.ExtraHosts, 424 } 425 return hc 426 }