github.com/rish1988/moby@v25.0.2+incompatible/integration/build/build_test.go (about) 1 package build // import "github.com/docker/docker/integration/build" 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "encoding/json" 7 "io" 8 "os" 9 "strings" 10 "testing" 11 12 "github.com/docker/docker/api/types" 13 "github.com/docker/docker/api/types/container" 14 "github.com/docker/docker/api/types/filters" 15 "github.com/docker/docker/errdefs" 16 "github.com/docker/docker/pkg/jsonmessage" 17 "github.com/docker/docker/testutil" 18 "github.com/docker/docker/testutil/fakecontext" 19 "gotest.tools/v3/assert" 20 is "gotest.tools/v3/assert/cmp" 21 "gotest.tools/v3/skip" 22 ) 23 24 func TestBuildWithRemoveAndForceRemove(t *testing.T) { 25 ctx := setupTest(t) 26 27 cases := []struct { 28 name string 29 dockerfile string 30 numberOfIntermediateContainers int 31 rm bool 32 forceRm bool 33 }{ 34 { 35 name: "successful build with no removal", 36 dockerfile: `FROM busybox 37 RUN exit 0 38 RUN exit 0`, 39 numberOfIntermediateContainers: 2, 40 rm: false, 41 forceRm: false, 42 }, 43 { 44 name: "successful build with remove", 45 dockerfile: `FROM busybox 46 RUN exit 0 47 RUN exit 0`, 48 numberOfIntermediateContainers: 0, 49 rm: true, 50 forceRm: false, 51 }, 52 { 53 name: "successful build with remove and force remove", 54 dockerfile: `FROM busybox 55 RUN exit 0 56 RUN exit 0`, 57 numberOfIntermediateContainers: 0, 58 rm: true, 59 forceRm: true, 60 }, 61 { 62 name: "failed build with no removal", 63 dockerfile: `FROM busybox 64 RUN exit 0 65 RUN exit 1`, 66 numberOfIntermediateContainers: 2, 67 rm: false, 68 forceRm: false, 69 }, 70 { 71 name: "failed build with remove", 72 dockerfile: `FROM busybox 73 RUN exit 0 74 RUN exit 1`, 75 numberOfIntermediateContainers: 1, 76 rm: true, 77 forceRm: false, 78 }, 79 { 80 name: "failed build with remove and force remove", 81 dockerfile: `FROM busybox 82 RUN exit 0 83 RUN exit 1`, 84 numberOfIntermediateContainers: 0, 85 rm: true, 86 forceRm: true, 87 }, 88 } 89 90 client := testEnv.APIClient() 91 for _, c := range cases { 92 c := c 93 t.Run(c.name, func(t *testing.T) { 94 t.Parallel() 95 ctx := testutil.StartSpan(ctx, t) 96 dockerfile := []byte(c.dockerfile) 97 98 buff := bytes.NewBuffer(nil) 99 tw := tar.NewWriter(buff) 100 assert.NilError(t, tw.WriteHeader(&tar.Header{ 101 Name: "Dockerfile", 102 Size: int64(len(dockerfile)), 103 })) 104 _, err := tw.Write(dockerfile) 105 assert.NilError(t, err) 106 assert.NilError(t, tw.Close()) 107 resp, err := client.ImageBuild(ctx, buff, types.ImageBuildOptions{Remove: c.rm, ForceRemove: c.forceRm, NoCache: true}) 108 assert.NilError(t, err) 109 defer resp.Body.Close() 110 filter, err := buildContainerIdsFilter(resp.Body) 111 assert.NilError(t, err) 112 remainingContainers, err := client.ContainerList(ctx, container.ListOptions{Filters: filter, All: true}) 113 assert.NilError(t, err) 114 assert.Equal(t, c.numberOfIntermediateContainers, len(remainingContainers), "Expected %v remaining intermediate containers, got %v", c.numberOfIntermediateContainers, len(remainingContainers)) 115 }) 116 } 117 } 118 119 func buildContainerIdsFilter(buildOutput io.Reader) (filters.Args, error) { 120 const intermediateContainerPrefix = " ---> Running in " 121 filter := filters.NewArgs() 122 123 dec := json.NewDecoder(buildOutput) 124 for { 125 m := jsonmessage.JSONMessage{} 126 err := dec.Decode(&m) 127 if err == io.EOF { 128 return filter, nil 129 } 130 if err != nil { 131 return filter, err 132 } 133 if ix := strings.Index(m.Stream, intermediateContainerPrefix); ix != -1 { 134 filter.Add("id", strings.TrimSpace(m.Stream[ix+len(intermediateContainerPrefix):])) 135 } 136 } 137 } 138 139 // TestBuildMultiStageCopy verifies that copying between stages works correctly. 140 // 141 // Regression test for docker/for-win#4349, ENGCORE-935, where creating the target 142 // directory failed on Windows, because `os.MkdirAll()` was called with a volume 143 // GUID path (\\?\Volume{dae8d3ac-b9a1-11e9-88eb-e8554b2ba1db}\newdir\hello}), 144 // which currently isn't supported by Golang. 145 func TestBuildMultiStageCopy(t *testing.T) { 146 ctx := setupTest(t) 147 148 dockerfile, err := os.ReadFile("testdata/Dockerfile." + t.Name()) 149 assert.NilError(t, err) 150 151 source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile))) 152 defer source.Close() 153 154 apiclient := testEnv.APIClient() 155 156 for _, target := range []string{"copy_to_root", "copy_to_newdir", "copy_to_newdir_nested", "copy_to_existingdir", "copy_to_newsubdir"} { 157 t.Run(target, func(t *testing.T) { 158 imgName := strings.ToLower(t.Name()) 159 160 resp, err := apiclient.ImageBuild( 161 ctx, 162 source.AsTarReader(t), 163 types.ImageBuildOptions{ 164 Remove: true, 165 ForceRemove: true, 166 Target: target, 167 Tags: []string{imgName}, 168 }, 169 ) 170 assert.NilError(t, err) 171 172 out := bytes.NewBuffer(nil) 173 _, err = io.Copy(out, resp.Body) 174 _ = resp.Body.Close() 175 if err != nil { 176 t.Log(out) 177 } 178 assert.NilError(t, err) 179 180 // verify the image was successfully built 181 _, _, err = apiclient.ImageInspectWithRaw(ctx, imgName) 182 if err != nil { 183 t.Log(out) 184 } 185 assert.NilError(t, err) 186 }) 187 } 188 } 189 190 func TestBuildMultiStageParentConfig(t *testing.T) { 191 dockerfile := ` 192 FROM busybox AS stage0 193 ENV WHO=parent 194 WORKDIR /foo 195 196 FROM stage0 197 ENV WHO=sibling1 198 WORKDIR sub1 199 200 FROM stage0 201 WORKDIR sub2 202 ` 203 204 ctx := setupTest(t) 205 source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) 206 defer source.Close() 207 208 apiclient := testEnv.APIClient() 209 imgName := strings.ToLower(t.Name()) 210 resp, err := apiclient.ImageBuild(ctx, 211 source.AsTarReader(t), 212 types.ImageBuildOptions{ 213 Remove: true, 214 ForceRemove: true, 215 Tags: []string{imgName}, 216 }) 217 assert.NilError(t, err) 218 _, err = io.Copy(io.Discard, resp.Body) 219 assert.Check(t, resp.Body.Close()) 220 assert.NilError(t, err) 221 222 img, _, err := apiclient.ImageInspectWithRaw(ctx, imgName) 223 assert.NilError(t, err) 224 225 expected := "/foo/sub2" 226 if testEnv.DaemonInfo.OSType == "windows" { 227 expected = `C:\foo\sub2` 228 } 229 assert.Check(t, is.Equal(expected, img.Config.WorkingDir)) 230 assert.Check(t, is.Contains(img.Config.Env, "WHO=parent")) 231 } 232 233 // Test cases in #36996 234 func TestBuildLabelWithTargets(t *testing.T) { 235 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 236 imgName := strings.ToLower(t.Name() + "-a") 237 testLabels := map[string]string{ 238 "foo": "bar", 239 "dead": "beef", 240 } 241 242 dockerfile := ` 243 FROM busybox AS target-a 244 CMD ["/dev"] 245 LABEL label-a=inline-a 246 FROM busybox AS target-b 247 CMD ["/dist"] 248 LABEL label-b=inline-b 249 ` 250 251 ctx := setupTest(t) 252 source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) 253 defer source.Close() 254 255 apiclient := testEnv.APIClient() 256 // For `target-a` build 257 resp, err := apiclient.ImageBuild(ctx, 258 source.AsTarReader(t), 259 types.ImageBuildOptions{ 260 Remove: true, 261 ForceRemove: true, 262 Tags: []string{imgName}, 263 Labels: testLabels, 264 Target: "target-a", 265 }) 266 assert.NilError(t, err) 267 _, err = io.Copy(io.Discard, resp.Body) 268 assert.Check(t, resp.Body.Close()) 269 assert.NilError(t, err) 270 271 img, _, err := apiclient.ImageInspectWithRaw(ctx, imgName) 272 assert.NilError(t, err) 273 274 testLabels["label-a"] = "inline-a" 275 for k, v := range testLabels { 276 x, ok := img.Config.Labels[k] 277 assert.Assert(t, ok) 278 assert.Assert(t, x == v) 279 } 280 281 // For `target-b` build 282 imgName = strings.ToLower(t.Name() + "-b") 283 delete(testLabels, "label-a") 284 resp, err = apiclient.ImageBuild(ctx, 285 source.AsTarReader(t), 286 types.ImageBuildOptions{ 287 Remove: true, 288 ForceRemove: true, 289 Tags: []string{imgName}, 290 Labels: testLabels, 291 Target: "target-b", 292 }) 293 assert.NilError(t, err) 294 _, err = io.Copy(io.Discard, resp.Body) 295 assert.Check(t, resp.Body.Close()) 296 assert.NilError(t, err) 297 298 img, _, err = apiclient.ImageInspectWithRaw(ctx, imgName) 299 assert.NilError(t, err) 300 301 testLabels["label-b"] = "inline-b" 302 for k, v := range testLabels { 303 x, ok := img.Config.Labels[k] 304 assert.Check(t, ok) 305 assert.Check(t, x == v) 306 } 307 } 308 309 func TestBuildWithEmptyLayers(t *testing.T) { 310 const dockerfile = ` 311 FROM busybox 312 COPY 1/ /target/ 313 COPY 2/ /target/ 314 COPY 3/ /target/ 315 ` 316 ctx := setupTest(t) 317 source := fakecontext.New(t, "", 318 fakecontext.WithDockerfile(dockerfile), 319 fakecontext.WithFile("1/a", "asdf"), 320 fakecontext.WithFile("2/a", "asdf"), 321 fakecontext.WithFile("3/a", "asdf")) 322 defer source.Close() 323 324 apiclient := testEnv.APIClient() 325 resp, err := apiclient.ImageBuild(ctx, 326 source.AsTarReader(t), 327 types.ImageBuildOptions{ 328 Remove: true, 329 ForceRemove: true, 330 }) 331 assert.NilError(t, err) 332 _, err = io.Copy(io.Discard, resp.Body) 333 assert.Check(t, resp.Body.Close()) 334 assert.NilError(t, err) 335 } 336 337 // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to 338 // multiple subsequent stages 339 // #35652 340 func TestBuildMultiStageOnBuild(t *testing.T) { 341 ctx := setupTest(t) 342 343 // test both metadata and layer based commands as they may be implemented differently 344 const dockerfile = ` 345 FROM busybox AS stage1 346 ONBUILD RUN echo 'foo' >somefile 347 ONBUILD ENV bar=baz 348 349 FROM stage1 350 # fails if ONBUILD RUN fails 351 RUN cat somefile 352 353 FROM stage1 354 RUN cat somefile` 355 356 source := fakecontext.New(t, "", 357 fakecontext.WithDockerfile(dockerfile)) 358 defer source.Close() 359 360 apiclient := testEnv.APIClient() 361 resp, err := apiclient.ImageBuild(ctx, 362 source.AsTarReader(t), 363 types.ImageBuildOptions{ 364 Remove: true, 365 ForceRemove: true, 366 }) 367 368 out := bytes.NewBuffer(nil) 369 assert.NilError(t, err) 370 _, err = io.Copy(out, resp.Body) 371 assert.Check(t, resp.Body.Close()) 372 assert.NilError(t, err) 373 374 assert.Check(t, is.Contains(out.String(), "Successfully built")) 375 376 imageIDs, err := getImageIDsFromBuild(out.Bytes()) 377 assert.NilError(t, err) 378 assert.Assert(t, is.Equal(3, len(imageIDs))) 379 380 img, _, err := apiclient.ImageInspectWithRaw(ctx, imageIDs[2]) 381 assert.NilError(t, err) 382 assert.Check(t, is.Contains(img.Config.Env, "bar=baz")) 383 } 384 385 // #35403 #36122 386 func TestBuildUncleanTarFilenames(t *testing.T) { 387 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 388 389 ctx := setupTest(t) 390 391 const dockerfile = ` 392 FROM scratch 393 COPY foo / 394 FROM scratch 395 COPY bar / 396 ` 397 398 buf := bytes.NewBuffer(nil) 399 w := tar.NewWriter(buf) 400 writeTarRecord(t, w, "Dockerfile", dockerfile) 401 writeTarRecord(t, w, "../foo", "foocontents0") 402 writeTarRecord(t, w, "/bar", "barcontents0") 403 err := w.Close() 404 assert.NilError(t, err) 405 406 apiclient := testEnv.APIClient() 407 resp, err := apiclient.ImageBuild(ctx, 408 buf, 409 types.ImageBuildOptions{ 410 Remove: true, 411 ForceRemove: true, 412 }) 413 414 out := bytes.NewBuffer(nil) 415 assert.NilError(t, err) 416 _, err = io.Copy(out, resp.Body) 417 assert.Check(t, resp.Body.Close()) 418 assert.NilError(t, err) 419 420 // repeat with changed data should not cause cache hits 421 422 buf = bytes.NewBuffer(nil) 423 w = tar.NewWriter(buf) 424 writeTarRecord(t, w, "Dockerfile", dockerfile) 425 writeTarRecord(t, w, "../foo", "foocontents1") 426 writeTarRecord(t, w, "/bar", "barcontents1") 427 err = w.Close() 428 assert.NilError(t, err) 429 430 resp, err = apiclient.ImageBuild(ctx, 431 buf, 432 types.ImageBuildOptions{ 433 Remove: true, 434 ForceRemove: true, 435 }) 436 437 out = bytes.NewBuffer(nil) 438 assert.NilError(t, err) 439 _, err = io.Copy(out, resp.Body) 440 assert.Check(t, resp.Body.Close()) 441 assert.NilError(t, err) 442 assert.Assert(t, !strings.Contains(out.String(), "Using cache")) 443 } 444 445 // docker/for-linux#135 446 // #35641 447 func TestBuildMultiStageLayerLeak(t *testing.T) { 448 ctx := setupTest(t) 449 450 // all commands need to match until COPY 451 const dockerfile = ` 452 FROM busybox 453 WORKDIR /foo 454 COPY foo . 455 FROM busybox 456 WORKDIR /foo 457 COPY bar . 458 RUN [ -f bar ] 459 RUN [ ! -f foo ] 460 ` 461 462 source := fakecontext.New(t, "", 463 fakecontext.WithFile("foo", "0"), 464 fakecontext.WithFile("bar", "1"), 465 fakecontext.WithDockerfile(dockerfile)) 466 defer source.Close() 467 468 apiClient := testEnv.APIClient() 469 resp, err := apiClient.ImageBuild(ctx, 470 source.AsTarReader(t), 471 types.ImageBuildOptions{ 472 Remove: true, 473 ForceRemove: true, 474 }) 475 476 out := bytes.NewBuffer(nil) 477 assert.NilError(t, err) 478 _, err = io.Copy(out, resp.Body) 479 assert.Check(t, resp.Body.Close()) 480 assert.NilError(t, err) 481 482 assert.Check(t, is.Contains(out.String(), "Successfully built")) 483 } 484 485 // #37581 486 // #40444 (Windows Containers only) 487 func TestBuildWithHugeFile(t *testing.T) { 488 ctx := setupTest(t) 489 490 var dockerfile string 491 if testEnv.DaemonInfo.OSType == "windows" { 492 dockerfile = ` 493 FROM busybox 494 495 # create a file with size of 8GB 496 RUN powershell "fsutil.exe file createnew bigfile.txt 8589934592 ; dir bigfile.txt" 497 ` 498 } else { 499 dockerfile = ` 500 FROM busybox 501 502 # create a sparse file with size over 8GB 503 RUN for g in $(seq 0 8); do dd if=/dev/urandom of=rnd bs=1K count=1 seek=$((1024*1024*g)) status=none; done \ 504 && ls -la rnd && du -sk rnd 505 ` 506 } 507 508 buf := bytes.NewBuffer(nil) 509 w := tar.NewWriter(buf) 510 writeTarRecord(t, w, "Dockerfile", dockerfile) 511 err := w.Close() 512 assert.NilError(t, err) 513 514 apiClient := testEnv.APIClient() 515 resp, err := apiClient.ImageBuild(ctx, 516 buf, 517 types.ImageBuildOptions{ 518 Remove: true, 519 ForceRemove: true, 520 }) 521 522 out := bytes.NewBuffer(nil) 523 assert.NilError(t, err) 524 _, err = io.Copy(out, resp.Body) 525 assert.Check(t, resp.Body.Close()) 526 assert.NilError(t, err) 527 assert.Check(t, is.Contains(out.String(), "Successfully built")) 528 } 529 530 func TestBuildWCOWSandboxSize(t *testing.T) { 531 t.Skip("FLAKY_TEST that needs to be fixed; see https://github.com/moby/moby/issues/42743") 532 skip.If(t, testEnv.DaemonInfo.OSType != "windows", "only Windows has sandbox size control") 533 ctx := setupTest(t) 534 535 const dockerfile = ` 536 FROM busybox AS intermediate 537 WORKDIR C:\\stuff 538 # Create and delete a 21GB file 539 RUN fsutil file createnew C:\\stuff\\bigfile_0.txt 22548578304 && del bigfile_0.txt 540 # Create three 7GB files 541 RUN fsutil file createnew C:\\stuff\\bigfile_1.txt 7516192768 542 RUN fsutil file createnew C:\\stuff\\bigfile_2.txt 7516192768 543 RUN fsutil file createnew C:\\stuff\\bigfile_3.txt 7516192768 544 # Copy that 21GB of data out into a new target 545 FROM busybox 546 COPY --from=intermediate C:\\stuff C:\\stuff 547 ` 548 549 buf := bytes.NewBuffer(nil) 550 w := tar.NewWriter(buf) 551 writeTarRecord(t, w, "Dockerfile", dockerfile) 552 err := w.Close() 553 assert.NilError(t, err) 554 555 apiClient := testEnv.APIClient() 556 resp, err := apiClient.ImageBuild(ctx, 557 buf, 558 types.ImageBuildOptions{ 559 Remove: true, 560 ForceRemove: true, 561 }) 562 563 out := bytes.NewBuffer(nil) 564 assert.NilError(t, err) 565 _, err = io.Copy(out, resp.Body) 566 assert.Check(t, resp.Body.Close()) 567 assert.NilError(t, err) 568 // The test passes if either: 569 // - the image build succeeded; or 570 // - The "COPY --from=intermediate" step ran out of space during re-exec'd writing of the transport layer information to hcsshim's temp directory 571 // The latter case means we finished the COPY operation, so the sandbox must have been larger than 20GB, which was the test, 572 // and _then_ ran out of space on the host during `importLayer` in the WindowsFilter graph driver, while committing the layer. 573 // See https://github.com/moby/moby/pull/41636#issuecomment-723038517 for more details on the operations being done here. 574 // Specifically, this happens on the Docker Jenkins CI Windows-RS5 build nodes. 575 // The two parts of the acceptable-failure case are on different lines, so we need two regexp checks. 576 assert.Check(t, is.Regexp("Successfully built|COPY --from=intermediate", out.String())) 577 assert.Check(t, is.Regexp("Successfully built|re-exec error: exit status 1: output: write.*daemon\\\\\\\\tmp\\\\\\\\hcs.*bigfile_[1-3].txt: There is not enough space on the disk.", out.String())) 578 } 579 580 func TestBuildWithEmptyDockerfile(t *testing.T) { 581 ctx := setupTest(t) 582 583 tests := []struct { 584 name string 585 dockerfile string 586 expectedErr string 587 }{ 588 { 589 name: "empty-dockerfile", 590 dockerfile: "", 591 expectedErr: "cannot be empty", 592 }, 593 { 594 name: "empty-lines-dockerfile", 595 dockerfile: ` 596 597 598 599 `, 600 expectedErr: "file with no instructions", 601 }, 602 { 603 name: "comment-only-dockerfile", 604 dockerfile: `# this is a comment`, 605 expectedErr: "file with no instructions", 606 }, 607 } 608 609 apiClient := testEnv.APIClient() 610 611 for _, tc := range tests { 612 tc := tc 613 t.Run(tc.name, func(t *testing.T) { 614 t.Parallel() 615 616 buf := bytes.NewBuffer(nil) 617 w := tar.NewWriter(buf) 618 writeTarRecord(t, w, "Dockerfile", tc.dockerfile) 619 err := w.Close() 620 assert.NilError(t, err) 621 622 _, err = apiClient.ImageBuild(ctx, 623 buf, 624 types.ImageBuildOptions{ 625 Remove: true, 626 ForceRemove: true, 627 }) 628 629 assert.Check(t, is.Contains(err.Error(), tc.expectedErr)) 630 }) 631 } 632 } 633 634 func TestBuildPreserveOwnership(t *testing.T) { 635 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME") 636 637 ctx := setupTest(t) 638 639 dockerfile, err := os.ReadFile("testdata/Dockerfile." + t.Name()) 640 assert.NilError(t, err) 641 642 source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile))) 643 defer source.Close() 644 645 apiClient := testEnv.APIClient() 646 647 for _, target := range []string{"copy_from", "copy_from_chowned"} { 648 t.Run(target, func(t *testing.T) { 649 ctx := testutil.StartSpan(ctx, t) 650 651 resp, err := apiClient.ImageBuild( 652 ctx, 653 source.AsTarReader(t), 654 types.ImageBuildOptions{ 655 Remove: true, 656 ForceRemove: true, 657 Target: target, 658 }, 659 ) 660 assert.NilError(t, err) 661 662 out := bytes.NewBuffer(nil) 663 _, err = io.Copy(out, resp.Body) 664 _ = resp.Body.Close() 665 if err != nil { 666 t.Log(out) 667 } 668 assert.NilError(t, err) 669 }) 670 } 671 } 672 673 func TestBuildPlatformInvalid(t *testing.T) { 674 ctx := setupTest(t) 675 676 buf := bytes.NewBuffer(nil) 677 w := tar.NewWriter(buf) 678 writeTarRecord(t, w, "Dockerfile", `FROM busybox`) 679 err := w.Close() 680 assert.NilError(t, err) 681 682 _, err = testEnv.APIClient().ImageBuild(ctx, buf, types.ImageBuildOptions{ 683 Remove: true, 684 ForceRemove: true, 685 Platform: "foobar", 686 }) 687 688 assert.Check(t, is.ErrorContains(err, "unknown operating system or architecture")) 689 assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter)) 690 } 691 692 func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) { 693 err := w.WriteHeader(&tar.Header{ 694 Name: fn, 695 Mode: 0o600, 696 Size: int64(len(contents)), 697 Typeflag: '0', 698 }) 699 assert.NilError(t, err) 700 _, err = w.Write([]byte(contents)) 701 assert.NilError(t, err) 702 } 703 704 type buildLine struct { 705 Stream string 706 Aux struct { 707 ID string 708 } 709 } 710 711 func getImageIDsFromBuild(output []byte) ([]string, error) { 712 var ids []string 713 for _, line := range bytes.Split(output, []byte("\n")) { 714 if len(line) == 0 { 715 continue 716 } 717 entry := buildLine{} 718 if err := json.Unmarshal(line, &entry); err != nil { 719 return nil, err 720 } 721 if entry.Aux.ID != "" { 722 ids = append(ids, entry.Aux.ID) 723 } 724 } 725 return ids, nil 726 }