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