github.com/containerd/nerdctl@v1.7.7/pkg/cmd/builder/build.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package builder 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "strconv" 29 "strings" 30 31 "github.com/containerd/containerd" 32 "github.com/containerd/containerd/images" 33 "github.com/containerd/containerd/images/archive" 34 "github.com/containerd/errdefs" 35 "github.com/containerd/log" 36 "github.com/containerd/nerdctl/pkg/api/types" 37 "github.com/containerd/nerdctl/pkg/buildkitutil" 38 "github.com/containerd/nerdctl/pkg/clientutil" 39 "github.com/containerd/nerdctl/pkg/platformutil" 40 "github.com/containerd/nerdctl/pkg/strutil" 41 "github.com/containerd/platforms" 42 dockerreference "github.com/distribution/reference" 43 ) 44 45 func Build(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) error { 46 buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, err := generateBuildctlArgs(ctx, client, options) 47 if err != nil { 48 return err 49 } 50 if cleanup != nil { 51 defer cleanup() 52 } 53 54 log.L.Debugf("running %s %v", buildctlBinary, buildctlArgs) 55 buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...) 56 buildctlCmd.Env = os.Environ() 57 58 var buildctlStdout io.Reader 59 if needsLoading { 60 buildctlStdout, err = buildctlCmd.StdoutPipe() 61 if err != nil { 62 return err 63 } 64 } else { 65 buildctlCmd.Stdout = options.Stdout 66 } 67 if !options.Quiet { 68 buildctlCmd.Stderr = options.Stderr 69 } 70 71 if err := buildctlCmd.Start(); err != nil { 72 return err 73 } 74 75 if needsLoading { 76 platMC, err := platformutil.NewMatchComparer(false, options.Platform) 77 if err != nil { 78 return err 79 } 80 if err = loadImage(ctx, buildctlStdout, options.GOptions.Namespace, options.GOptions.Address, options.GOptions.Snapshotter, options.Stdout, platMC, options.Quiet); err != nil { 81 return err 82 } 83 } 84 85 if err = buildctlCmd.Wait(); err != nil { 86 return err 87 } 88 89 if options.IidFile != "" { 90 id, err := getDigestFromMetaFile(metaFile) 91 if err != nil { 92 return err 93 } 94 if err := os.WriteFile(options.IidFile, []byte(id), 0644); err != nil { 95 return err 96 } 97 } 98 99 if len(tags) > 1 { 100 log.L.Debug("Found more than 1 tag") 101 imageService := client.ImageService() 102 image, err := imageService.Get(ctx, tags[0]) 103 if err != nil { 104 return fmt.Errorf("unable to tag image: %s", err) 105 } 106 for _, targetRef := range tags[1:] { 107 image.Name = targetRef 108 if _, err := imageService.Create(ctx, image); err != nil { 109 // if already exists; skip. 110 if errors.Is(err, errdefs.ErrAlreadyExists) { 111 continue 112 } 113 return fmt.Errorf("unable to tag image: %s", err) 114 } 115 } 116 } 117 118 return nil 119 } 120 121 // TODO: This struct and `loadImage` are duplicated with the code in `cmd/load.go`, remove it after `load.go` has been refactor 122 type readCounter struct { 123 io.Reader 124 N int 125 } 126 127 func loadImage(ctx context.Context, in io.Reader, namespace, address, snapshotter string, output io.Writer, platMC platforms.MatchComparer, quiet bool) error { 128 // In addition to passing WithImagePlatform() to client.Import(), we also need to pass WithDefaultPlatform() to NewClient(). 129 // Otherwise unpacking may fail. 130 client, ctx, cancel, err := clientutil.NewClient(ctx, namespace, address, containerd.WithDefaultPlatform(platMC)) 131 if err != nil { 132 return err 133 } 134 defer func() { 135 cancel() 136 client.Close() 137 }() 138 r := &readCounter{Reader: in} 139 imgs, err := client.Import(ctx, r, containerd.WithDigestRef(archive.DigestTranslator(snapshotter)), containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), containerd.WithImportPlatform(platMC)) 140 if err != nil { 141 if r.N == 0 { 142 // Avoid confusing "unrecognized image format" 143 return errors.New("no image was built") 144 } 145 if errors.Is(err, images.ErrEmptyWalk) { 146 err = fmt.Errorf("%w (Hint: set `--platform=PLATFORM` or `--all-platforms`)", err) 147 } 148 return err 149 } 150 for _, img := range imgs { 151 image := containerd.NewImageWithPlatform(client, img, platMC) 152 153 // TODO: Show unpack status 154 if !quiet { 155 fmt.Fprintf(output, "unpacking %s (%s)...\n", img.Name, img.Target.Digest) 156 } 157 err = image.Unpack(ctx, snapshotter) 158 if err != nil { 159 return err 160 } 161 if quiet { 162 fmt.Fprintln(output, img.Target.Digest) 163 } else { 164 fmt.Fprintf(output, "Loaded image: %s\n", img.Name) 165 } 166 } 167 168 return nil 169 } 170 171 func generateBuildctlArgs(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) (buildCtlBinary string, 172 buildctlArgs []string, needsLoading bool, metaFile string, tags []string, cleanup func(), err error) { 173 174 buildctlBinary, err := buildkitutil.BuildctlBinary() 175 if err != nil { 176 return "", nil, false, "", nil, nil, err 177 } 178 179 output := options.Output 180 if output == "" { 181 info, err := client.Server(ctx) 182 if err != nil { 183 return "", nil, false, "", nil, nil, err 184 } 185 sharable, err := isImageSharable(options.BuildKitHost, options.GOptions.Namespace, info.UUID, options.GOptions.Snapshotter, options.Platform) 186 if err != nil { 187 return "", nil, false, "", nil, nil, err 188 } 189 if sharable { 190 output = "type=image,unpack=true" // ensure the target stage is unlazied (needed for any snapshotters) 191 } else { 192 output = "type=docker" 193 if len(options.Platform) > 1 { 194 // For avoiding `error: failed to solve: docker exporter does not currently support exporting manifest lists` 195 // TODO: consider using type=oci for single-options.Platform build too 196 output = "type=oci" 197 } 198 needsLoading = true 199 } 200 } else { 201 if !strings.Contains(output, "type=") { 202 // should accept --output <DIR> as an alias of --output 203 // type=local,dest=<DIR> 204 output = fmt.Sprintf("type=local,dest=%s", output) 205 } 206 if strings.Contains(output, "type=docker") || strings.Contains(output, "type=oci") { 207 needsLoading = true 208 } 209 } 210 if tags = strutil.DedupeStrSlice(options.Tag); len(tags) > 0 { 211 ref := tags[0] 212 named, err := dockerreference.ParseNormalizedNamed(ref) 213 if err != nil { 214 return "", nil, false, "", nil, nil, err 215 } 216 output += ",name=" + dockerreference.TagNameOnly(named).String() 217 218 // pick the first tag and add it to output 219 for idx, tag := range tags { 220 named, err := dockerreference.ParseNormalizedNamed(tag) 221 if err != nil { 222 return "", nil, false, "", nil, nil, err 223 } 224 tags[idx] = dockerreference.TagNameOnly(named).String() 225 } 226 } else if len(tags) == 0 { 227 output = output + ",dangling-name-prefix=<none>" 228 } 229 230 buildctlArgs = buildkitutil.BuildctlBaseArgs(options.BuildKitHost) 231 232 buildctlArgs = append(buildctlArgs, []string{ 233 "build", 234 "--progress=" + options.Progress, 235 "--frontend=dockerfile.v0", 236 "--local=context=" + options.BuildContext, 237 "--output=" + output, 238 }...) 239 240 dir := options.BuildContext 241 file := buildkitutil.DefaultDockerfileName 242 if options.File != "" { 243 if options.File == "-" { 244 // Super Warning: this is a special trick to update the dir variable, Don't move this line!!!!!! 245 var err error 246 dir, err = buildkitutil.WriteTempDockerfile(options.Stdin) 247 if err != nil { 248 return "", nil, false, "", nil, nil, err 249 } 250 cleanup = func() { 251 os.RemoveAll(dir) 252 } 253 } else { 254 dir, file = filepath.Split(options.File) 255 } 256 257 if dir == "" { 258 dir = "." 259 } 260 } 261 dir, file, err = buildkitutil.BuildKitFile(dir, file) 262 if err != nil { 263 return "", nil, false, "", nil, nil, err 264 } 265 266 buildctlArgs = append(buildctlArgs, "--local=dockerfile="+dir) 267 buildctlArgs = append(buildctlArgs, "--opt=filename="+file) 268 269 if options.Target != "" { 270 buildctlArgs = append(buildctlArgs, "--opt=target="+options.Target) 271 } 272 273 if len(options.Platform) > 0 { 274 buildctlArgs = append(buildctlArgs, "--opt=platform="+strings.Join(options.Platform, ",")) 275 } 276 277 seenBuildArgs := make(map[string]struct{}) 278 for _, ba := range strutil.DedupeStrSlice(options.BuildArgs) { 279 arr := strings.Split(ba, "=") 280 seenBuildArgs[arr[0]] = struct{}{} 281 if len(arr) == 1 && len(arr[0]) > 0 { 282 // Avoid masking default build arg value from Dockerfile if environment variable is not set 283 // https://github.com/moby/moby/issues/24101 284 val, ok := os.LookupEnv(arr[0]) 285 if ok { 286 buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=build-arg:%s=%s", ba, val)) 287 } else { 288 log.L.Debugf("ignoring unset build arg %q", ba) 289 } 290 } else if len(arr) > 1 && len(arr[0]) > 0 { 291 buildctlArgs = append(buildctlArgs, "--opt=build-arg:"+ba) 292 293 // Support `--build-arg BUILDKIT_INLINE_CACHE=1` for compatibility with `docker buildx build` 294 // https://github.com/docker/buildx/blob/v0.6.3/docs/reference/buildx_build.md#-export-build-cache-to-an-external-cache-destination---cache-to 295 if strings.HasPrefix(ba, "BUILDKIT_INLINE_CACHE=") { 296 bic := strings.TrimPrefix(ba, "BUILDKIT_INLINE_CACHE=") 297 bicParsed, err := strconv.ParseBool(bic) 298 if err == nil { 299 if bicParsed { 300 buildctlArgs = append(buildctlArgs, "--export-cache=type=inline") 301 } 302 } else { 303 log.L.WithError(err).Warnf("invalid BUILDKIT_INLINE_CACHE: %q", bic) 304 } 305 } 306 } else { 307 return "", nil, false, "", nil, nil, fmt.Errorf("invalid build arg %q", ba) 308 } 309 } 310 311 // Propagate SOURCE_DATE_EPOCH from the client env 312 // https://github.com/docker/buildx/pull/1482 313 if v := os.Getenv("SOURCE_DATE_EPOCH"); v != "" { 314 if _, ok := seenBuildArgs["SOURCE_DATE_EPOCH"]; !ok { 315 buildctlArgs = append(buildctlArgs, "--opt=build-arg:SOURCE_DATE_EPOCH="+v) 316 } 317 } 318 319 for _, l := range strutil.DedupeStrSlice(options.Label) { 320 buildctlArgs = append(buildctlArgs, "--opt=label:"+l) 321 } 322 323 if options.NoCache { 324 buildctlArgs = append(buildctlArgs, "--no-cache") 325 } 326 327 for _, s := range strutil.DedupeStrSlice(options.Secret) { 328 buildctlArgs = append(buildctlArgs, "--secret="+s) 329 } 330 331 for _, s := range strutil.DedupeStrSlice(options.Allow) { 332 buildctlArgs = append(buildctlArgs, "--allow="+s) 333 } 334 335 for _, s := range strutil.DedupeStrSlice(options.SSH) { 336 buildctlArgs = append(buildctlArgs, "--ssh="+s) 337 } 338 339 for _, s := range strutil.DedupeStrSlice(options.CacheFrom) { 340 if !strings.Contains(s, "type=") { 341 s = "type=registry,ref=" + s 342 } 343 buildctlArgs = append(buildctlArgs, "--import-cache="+s) 344 } 345 346 for _, s := range strutil.DedupeStrSlice(options.CacheTo) { 347 if !strings.Contains(s, "type=") { 348 s = "type=registry,ref=" + s 349 } 350 buildctlArgs = append(buildctlArgs, "--export-cache="+s) 351 } 352 353 if !options.Rm { 354 log.L.Warn("ignoring deprecated flag: '--rm=false'") 355 } 356 357 if options.IidFile != "" { 358 file, err := os.CreateTemp("", "buildkit-meta-*") 359 if err != nil { 360 return "", nil, false, "", nil, cleanup, err 361 } 362 defer file.Close() 363 metaFile = file.Name() 364 buildctlArgs = append(buildctlArgs, "--metadata-file="+metaFile) 365 } 366 367 if options.NetworkMode != "" { 368 switch options.NetworkMode { 369 case "none": 370 buildctlArgs = append(buildctlArgs, "--opt=force-network-mode="+options.NetworkMode) 371 case "host": 372 buildctlArgs = append(buildctlArgs, "--opt=force-network-mode="+options.NetworkMode, "--allow=network.host", "--allow=security.insecure") 373 case "", "default": 374 default: 375 log.L.Debugf("ignoring network build arg %s", options.NetworkMode) 376 } 377 } 378 379 return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil 380 } 381 382 func getDigestFromMetaFile(path string) (string, error) { 383 data, err := os.ReadFile(path) 384 if err != nil { 385 return "", err 386 } 387 defer os.Remove(path) 388 389 metadata := map[string]json.RawMessage{} 390 if err := json.Unmarshal(data, &metadata); err != nil { 391 log.L.WithError(err).Errorf("failed to unmarshal metadata file %s", path) 392 return "", err 393 } 394 digestRaw, ok := metadata["containerimage.digest"] 395 if !ok { 396 return "", errors.New("failed to find containerimage.digest in metadata file") 397 } 398 var digest string 399 if err := json.Unmarshal(digestRaw, &digest); err != nil { 400 log.L.WithError(err).Errorf("failed to unmarshal digset") 401 return "", err 402 } 403 return digest, nil 404 } 405 406 func isImageSharable(buildkitHost, namespace, uuid, snapshotter string, platform []string) (bool, error) { 407 labels, err := buildkitutil.GetWorkerLabels(buildkitHost) 408 if err != nil { 409 return false, err 410 } 411 log.L.Debugf("worker labels: %+v", labels) 412 executor, ok := labels["org.mobyproject.buildkit.worker.executor"] 413 if !ok { 414 return false, nil 415 } 416 containerdUUID, ok := labels["org.mobyproject.buildkit.worker.containerd.uuid"] 417 if !ok { 418 return false, nil 419 } 420 containerdNamespace, ok := labels["org.mobyproject.buildkit.worker.containerd.namespace"] 421 if !ok { 422 return false, nil 423 } 424 workerSnapshotter, ok := labels["org.mobyproject.buildkit.worker.snapshotter"] 425 if !ok { 426 return false, nil 427 } 428 // NOTE: It's possible that BuildKit doesn't download the base image of non-default platform (e.g. when the provided 429 // Dockerfile doesn't contain instructions require base images like RUN) even if `--output type=image,unpack=true` 430 // is passed to BuildKit. Thus, we need to use `type=docker` or `type=oci` when nerdctl builds non-default platform 431 // image using `platform` option. 432 return executor == "containerd" && containerdUUID == uuid && containerdNamespace == namespace && workerSnapshotter == snapshotter && len(platform) == 0, nil 433 }