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