github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/builder_build_test.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 main 18 19 import ( 20 "fmt" 21 "os" 22 "path/filepath" 23 "strings" 24 "testing" 25 26 "github.com/containerd/nerdctl/pkg/testutil" 27 "gotest.tools/v3/assert" 28 ) 29 30 func TestBuild(t *testing.T) { 31 testutil.RequiresBuild(t) 32 base := testutil.NewBase(t) 33 defer base.Cmd("builder", "prune").Run() 34 imageName := testutil.Identifier(t) 35 defer base.Cmd("rmi", imageName).Run() 36 37 dockerfile := fmt.Sprintf(`FROM %s 38 CMD ["echo", "nerdctl-build-test-string"] 39 `, testutil.CommonImage) 40 41 buildCtx, err := createBuildContext(dockerfile) 42 assert.NilError(t, err) 43 defer os.RemoveAll(buildCtx) 44 45 base.Cmd("build", "-t", imageName, buildCtx).AssertOK() 46 base.Cmd("build", buildCtx, "-t", imageName).AssertOK() 47 base.Cmd("run", "--rm", imageName).AssertOutExactly("nerdctl-build-test-string\n") 48 49 // DOCKER_BUILDKIT (v20.10): `Error response from daemon: exporter "docker" could not be found` 50 if base.Target == testutil.Nerdctl { 51 ignoredImageNamed := imageName + "-" + "ignored" 52 outputOpt := fmt.Sprintf("--output=type=docker,name=%s", ignoredImageNamed) 53 base.Cmd("build", buildCtx, "-t", imageName, outputOpt).AssertOK() 54 55 base.Cmd("run", "--rm", imageName).AssertOutExactly("nerdctl-build-test-string\n") 56 base.Cmd("run", "--rm", ignoredImageNamed).AssertFail() 57 } 58 } 59 60 // TestBuildBaseImage tests if an image can be built on the previously built image. 61 // This isn't currently supported by nerdctl with BuildKit OCI worker. 62 func TestBuildBaseImage(t *testing.T) { 63 testutil.RequiresBuild(t) 64 base := testutil.NewBase(t) 65 defer base.Cmd("builder", "prune").Run() 66 imageName := testutil.Identifier(t) 67 defer base.Cmd("rmi", imageName).Run() 68 imageName2 := imageName + "-2" 69 defer base.Cmd("rmi", imageName2).Run() 70 71 dockerfile := fmt.Sprintf(`FROM %s 72 RUN echo hello > /hello 73 CMD ["echo", "nerdctl-build-test-string"] 74 `, testutil.CommonImage) 75 76 buildCtx, err := createBuildContext(dockerfile) 77 assert.NilError(t, err) 78 defer os.RemoveAll(buildCtx) 79 80 base.Cmd("build", "-t", imageName, buildCtx).AssertOK() 81 base.Cmd("build", buildCtx, "-t", imageName).AssertOK() 82 83 dockerfile2 := fmt.Sprintf(`FROM %s 84 RUN echo hello2 > /hello2 85 CMD ["cat", "/hello2"] 86 `, imageName) 87 88 buildCtx2, err := createBuildContext(dockerfile2) 89 assert.NilError(t, err) 90 defer os.RemoveAll(buildCtx2) 91 92 base.Cmd("build", "-t", imageName2, buildCtx2).AssertOK() 93 base.Cmd("build", buildCtx2, "-t", imageName2).AssertOK() 94 95 base.Cmd("run", "--rm", imageName2).AssertOutExactly("hello2\n") 96 } 97 98 // TestBuildFromContainerd tests if an image can be built on an image pulled by nerdctl. 99 // This isn't currently supported by nerdctl with BuildKit OCI worker. 100 func TestBuildFromContainerd(t *testing.T) { 101 testutil.DockerIncompatible(t) 102 testutil.RequiresBuild(t) 103 base := testutil.NewBase(t) 104 defer base.Cmd("builder", "prune").Run() 105 imageName := testutil.Identifier(t) 106 defer base.Cmd("rmi", imageName).Run() 107 imageName2 := imageName + "-2" 108 defer base.Cmd("rmi", imageName2).Run() 109 110 // FIXME: BuildKit sometimes tries to use base image manifests of platforms that hasn't been 111 // pulled by `nerdctl pull`. This leads to "not found" error for the base image. 112 // To avoid this issue, images shared to BuildKit should always be pulled by manifest 113 // digest or `--all-platforms` needs to be added. 114 base.Cmd("pull", "--all-platforms", testutil.CommonImage).AssertOK() 115 base.Cmd("tag", testutil.CommonImage, imageName).AssertOK() 116 base.Cmd("rmi", testutil.CommonImage).AssertOK() 117 118 dockerfile2 := fmt.Sprintf(`FROM %s 119 RUN echo hello2 > /hello2 120 CMD ["cat", "/hello2"] 121 `, imageName) 122 123 buildCtx2, err := createBuildContext(dockerfile2) 124 assert.NilError(t, err) 125 defer os.RemoveAll(buildCtx2) 126 127 base.Cmd("build", "-t", imageName2, buildCtx2).AssertOK() 128 base.Cmd("build", buildCtx2, "-t", imageName2).AssertOK() 129 130 base.Cmd("run", "--rm", imageName2).AssertOutExactly("hello2\n") 131 } 132 133 func TestBuildFromStdin(t *testing.T) { 134 t.Parallel() 135 testutil.RequiresBuild(t) 136 base := testutil.NewBase(t) 137 defer base.Cmd("builder", "prune").Run() 138 imageName := testutil.Identifier(t) 139 defer base.Cmd("rmi", imageName).Run() 140 141 dockerfile := fmt.Sprintf(`FROM %s 142 CMD ["echo", "nerdctl-build-test-stdin"] 143 `, testutil.CommonImage) 144 145 base.Cmd("build", "-t", imageName, "-f", "-", ".").CmdOption(testutil.WithStdin(strings.NewReader(dockerfile))).AssertCombinedOutContains(imageName) 146 } 147 148 func TestBuildWithDockerfile(t *testing.T) { 149 testutil.RequiresBuild(t) 150 base := testutil.NewBase(t) 151 defer base.Cmd("builder", "prune").Run() 152 imageName := testutil.Identifier(t) 153 defer base.Cmd("rmi", imageName).Run() 154 155 dockerfile := fmt.Sprintf(`FROM %s 156 CMD ["echo", "nerdctl-build-test-dockerfile"] 157 `, testutil.CommonImage) 158 159 buildCtx := filepath.Join(t.TempDir(), "test") 160 err := os.MkdirAll(buildCtx, 0755) 161 assert.NilError(t, err) 162 err = os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0644) 163 assert.NilError(t, err) 164 165 pwd, err := os.Getwd() 166 assert.NilError(t, err) 167 err = os.Chdir(buildCtx) 168 assert.NilError(t, err) 169 defer os.Chdir(pwd) 170 171 // hack os.Getwd return "(unreachable)" on rootless 172 t.Setenv("PWD", buildCtx) 173 174 base.Cmd("build", "-t", imageName, "-f", "Dockerfile", "..").AssertOK() 175 base.Cmd("build", "-t", imageName, "-f", "Dockerfile", ".").AssertOK() 176 // fail err: no such file or directory 177 base.Cmd("build", "-t", imageName, "-f", "../Dockerfile", ".").AssertFail() 178 } 179 180 func TestBuildLocal(t *testing.T) { 181 t.Parallel() 182 testutil.RequiresBuild(t) 183 base := testutil.NewBase(t) 184 if testutil.GetTarget() == testutil.Docker { 185 base.Env = append(base.Env, "DOCKER_BUILDKIT=1") 186 } 187 defer base.Cmd("builder", "prune").Run() 188 const testFileName = "nerdctl-build-test" 189 const testContent = "nerdctl" 190 outputDir := t.TempDir() 191 192 dockerfile := fmt.Sprintf(`FROM scratch 193 COPY %s /`, 194 testFileName) 195 196 buildCtx, err := createBuildContext(dockerfile) 197 assert.NilError(t, err) 198 defer os.RemoveAll(buildCtx) 199 200 if err := os.WriteFile(filepath.Join(buildCtx, testFileName), []byte(testContent), 0644); err != nil { 201 t.Fatal(err) 202 } 203 204 testFilePath := filepath.Join(outputDir, testFileName) 205 base.Cmd("build", "-o", fmt.Sprintf("type=local,dest=%s", outputDir), buildCtx).AssertOK() 206 if _, err := os.Stat(testFilePath); err != nil { 207 t.Fatal(err) 208 } 209 data, err := os.ReadFile(testFilePath) 210 assert.NilError(t, err) 211 assert.Equal(t, string(data), testContent) 212 213 aliasOutputDir := t.TempDir() 214 testAliasFilePath := filepath.Join(aliasOutputDir, testFileName) 215 base.Cmd("build", "-o", aliasOutputDir, buildCtx).AssertOK() 216 if _, err := os.Stat(testAliasFilePath); err != nil { 217 t.Fatal(err) 218 } 219 data, err = os.ReadFile(testAliasFilePath) 220 assert.NilError(t, err) 221 assert.Equal(t, string(data), testContent) 222 } 223 224 func createBuildContext(dockerfile string) (string, error) { 225 tmpDir, err := os.MkdirTemp("", "nerdctl-build-test") 226 if err != nil { 227 return "", err 228 } 229 if err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil { 230 return "", err 231 } 232 return tmpDir, nil 233 } 234 235 func TestBuildWithBuildArg(t *testing.T) { 236 testutil.RequiresBuild(t) 237 base := testutil.NewBase(t) 238 defer base.Cmd("builder", "prune").Run() 239 imageName := testutil.Identifier(t) 240 defer base.Cmd("rmi", imageName).Run() 241 242 dockerfile := fmt.Sprintf(`FROM %s 243 ARG TEST_STRING=1 244 ENV TEST_STRING=$TEST_STRING 245 CMD echo $TEST_STRING 246 `, testutil.CommonImage) 247 248 buildCtx, err := createBuildContext(dockerfile) 249 assert.NilError(t, err) 250 defer os.RemoveAll(buildCtx) 251 252 base.Cmd("build", buildCtx, "-t", imageName).AssertOK() 253 base.Cmd("run", "--rm", imageName).AssertOutExactly("1\n") 254 255 validCases := []struct { 256 name string 257 arg string 258 envValue string 259 envSet bool 260 expected string 261 }{ 262 {"ArgValueOverridesDefault", "TEST_STRING=2", "", false, "2\n"}, 263 {"EmptyArgValueOverridesDefault", "TEST_STRING=", "", false, "\n"}, 264 {"UnsetArgKeyPreservesDefault", "TEST_STRING", "", false, "1\n"}, 265 {"EnvValueOverridesDefault", "TEST_STRING", "3", true, "3\n"}, 266 {"EmptyEnvValueOverridesDefault", "TEST_STRING", "", true, "\n"}, 267 } 268 269 for _, tc := range validCases { 270 t.Run(tc.name, func(t *testing.T) { 271 if tc.envSet { 272 err := os.Setenv("TEST_STRING", tc.envValue) 273 assert.NilError(t, err) 274 defer os.Unsetenv("TEST_STRING") 275 } 276 277 base.Cmd("build", buildCtx, "-t", imageName, "--build-arg", tc.arg).AssertOK() 278 base.Cmd("run", "--rm", imageName).AssertOutExactly(tc.expected) 279 }) 280 } 281 } 282 283 func TestBuildWithIIDFile(t *testing.T) { 284 t.Parallel() 285 testutil.RequiresBuild(t) 286 base := testutil.NewBase(t) 287 defer base.Cmd("builder", "prune").Run() 288 imageName := testutil.Identifier(t) 289 defer base.Cmd("rmi", imageName).Run() 290 291 dockerfile := fmt.Sprintf(`FROM %s 292 CMD ["echo", "nerdctl-build-test-string"] 293 `, testutil.CommonImage) 294 295 buildCtx, err := createBuildContext(dockerfile) 296 assert.NilError(t, err) 297 defer os.RemoveAll(buildCtx) 298 fileName := filepath.Join(t.TempDir(), "id.txt") 299 300 base.Cmd("build", "-t", imageName, buildCtx, "--iidfile", fileName).AssertOK() 301 base.Cmd("build", buildCtx, "-t", imageName, "--iidfile", fileName).AssertOK() 302 defer os.Remove(fileName) 303 304 imageID, err := os.ReadFile(fileName) 305 assert.NilError(t, err) 306 307 base.Cmd("run", "--rm", string(imageID)).AssertOutExactly("nerdctl-build-test-string\n") 308 } 309 310 func TestBuildWithLabels(t *testing.T) { 311 t.Parallel() 312 testutil.RequiresBuild(t) 313 base := testutil.NewBase(t) 314 defer base.Cmd("builder", "prune").Run() 315 imageName := testutil.Identifier(t) 316 317 dockerfile := fmt.Sprintf(`FROM %s 318 LABEL name=nerdctl-build-test-label 319 `, testutil.CommonImage) 320 321 buildCtx, err := createBuildContext(dockerfile) 322 assert.NilError(t, err) 323 defer os.RemoveAll(buildCtx) 324 325 base.Cmd("build", "-t", imageName, buildCtx, "--label", "label=test").AssertOK() 326 defer base.Cmd("rmi", imageName).Run() 327 328 base.Cmd("inspect", imageName, "--format", "{{json .Config.Labels }}").AssertOutExactly("{\"label\":\"test\",\"name\":\"nerdctl-build-test-label\"}\n") 329 } 330 331 func TestBuildMultipleTags(t *testing.T) { 332 testutil.RequiresBuild(t) 333 base := testutil.NewBase(t) 334 defer base.Cmd("builder", "prune").Run() 335 img := testutil.Identifier(t) 336 imgWithNoTag, imgWithCustomTag := fmt.Sprintf("%s%d", img, 2), fmt.Sprintf("%s%d:hello", img, 3) 337 defer base.Cmd("rmi", img).Run() 338 defer base.Cmd("rmi", imgWithNoTag).Run() 339 defer base.Cmd("rmi", imgWithCustomTag).Run() 340 341 dockerfile := fmt.Sprintf(`FROM %s 342 CMD ["echo", "nerdctl-build-test-string"] 343 `, testutil.CommonImage) 344 345 buildCtx, err := createBuildContext(dockerfile) 346 assert.NilError(t, err) 347 defer os.RemoveAll(buildCtx) 348 349 base.Cmd("build", "-t", img, buildCtx).AssertOK() 350 base.Cmd("build", buildCtx, "-t", img, "-t", imgWithNoTag, "-t", imgWithCustomTag).AssertOK() 351 base.Cmd("run", "--rm", img).AssertOutExactly("nerdctl-build-test-string\n") 352 base.Cmd("run", "--rm", imgWithNoTag).AssertOutExactly("nerdctl-build-test-string\n") 353 base.Cmd("run", "--rm", imgWithCustomTag).AssertOutExactly("nerdctl-build-test-string\n") 354 } 355 356 func TestBuildWithContainerfile(t *testing.T) { 357 testutil.RequiresBuild(t) 358 testutil.DockerIncompatible(t) 359 base := testutil.NewBase(t) 360 defer base.Cmd("builder", "prune").Run() 361 imageName := testutil.Identifier(t) 362 defer base.Cmd("rmi", imageName).Run() 363 364 containerfile := fmt.Sprintf(`FROM %s 365 CMD ["echo", "nerdctl-build-test-string"] 366 `, testutil.CommonImage) 367 368 buildCtx := t.TempDir() 369 370 var err = os.WriteFile(filepath.Join(buildCtx, "Containerfile"), []byte(containerfile), 0644) 371 assert.NilError(t, err) 372 base.Cmd("build", "-t", imageName, buildCtx).AssertOK() 373 base.Cmd("run", "--rm", imageName).AssertOutExactly("nerdctl-build-test-string\n") 374 } 375 376 func TestBuildWithDockerFileAndContainerfile(t *testing.T) { 377 testutil.RequiresBuild(t) 378 base := testutil.NewBase(t) 379 defer base.Cmd("builder", "prune").Run() 380 imageName := testutil.Identifier(t) 381 defer base.Cmd("rmi", imageName).Run() 382 383 dockerfile := fmt.Sprintf(`FROM %s 384 CMD ["echo", "dockerfile"] 385 `, testutil.CommonImage) 386 387 containerfile := fmt.Sprintf(`FROM %s 388 CMD ["echo", "containerfile"] 389 `, testutil.CommonImage) 390 391 tmpDir := t.TempDir() 392 393 var err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644) 394 assert.NilError(t, err) 395 396 err = os.WriteFile(filepath.Join(tmpDir, "Containerfile"), []byte(containerfile), 0644) 397 assert.NilError(t, err) 398 399 buildCtx, err := createBuildContext(dockerfile) 400 assert.NilError(t, err) 401 defer os.RemoveAll(buildCtx) 402 403 base.Cmd("build", "-t", imageName, buildCtx).AssertOK() 404 base.Cmd("run", "--rm", imageName).AssertOutExactly("dockerfile\n") 405 } 406 407 func TestBuildNoTag(t *testing.T) { 408 testutil.RequiresBuild(t) 409 base := testutil.NewBase(t) 410 defer base.Cmd("builder", "prune").AssertOK() 411 base.Cmd("image", "prune", "--force", "--all").AssertOK() 412 413 dockerfile := fmt.Sprintf(`FROM %s 414 CMD ["echo", "nerdctl-build-notag-string"] 415 `, testutil.CommonImage) 416 buildCtx, err := createBuildContext(dockerfile) 417 assert.NilError(t, err) 418 defer os.RemoveAll(buildCtx) 419 420 base.Cmd("build", buildCtx).AssertOK() 421 base.Cmd("images").AssertOutContains("<none>") 422 base.Cmd("image", "prune", "--force", "--all").AssertOK() 423 } 424 425 // TestBuildSourceDateEpoch tests that $SOURCE_DATE_EPOCH is propagated from the client env 426 // https://github.com/docker/buildx/pull/1482 427 func TestBuildSourceDateEpoch(t *testing.T) { 428 testutil.RequiresBuild(t) 429 testutil.DockerIncompatible(t) // Needs buildx v0.10 (https://github.com/docker/buildx/pull/1489) 430 base := testutil.NewBase(t) 431 imageName := testutil.Identifier(t) 432 defer base.Cmd("rmi", imageName).AssertOK() 433 434 dockerfile := fmt.Sprintf(`FROM %s 435 ARG SOURCE_DATE_EPOCH 436 RUN echo $SOURCE_DATE_EPOCH >/source-date-epoch 437 CMD ["cat", "/source-date-epoch"] 438 `, testutil.CommonImage) 439 440 buildCtx, err := createBuildContext(dockerfile) 441 assert.NilError(t, err) 442 defer os.RemoveAll(buildCtx) 443 444 const sourceDateEpochEnvStr = "1111111111" 445 t.Setenv("SOURCE_DATE_EPOCH", sourceDateEpochEnvStr) 446 base.Cmd("build", "-t", imageName, buildCtx).AssertOK() 447 base.Cmd("run", "--rm", imageName).AssertOutExactly(sourceDateEpochEnvStr + "\n") 448 449 const sourceDateEpochArgStr = "2222222222" 450 base.Cmd("build", "-t", imageName, "--build-arg", "SOURCE_DATE_EPOCH="+sourceDateEpochArgStr, buildCtx).AssertOK() 451 base.Cmd("run", "--rm", imageName).AssertOutExactly(sourceDateEpochArgStr + "\n") 452 } 453 454 func TestBuildNetwork(t *testing.T) { 455 testutil.RequiresBuild(t) 456 base := testutil.NewBase(t) 457 defer base.Cmd("builder", "prune").AssertOK() 458 459 dockerfile := fmt.Sprintf(`FROM %s 460 RUN apk add --no-cache curl 461 RUN curl -I http://google.com 462 `, testutil.CommonImage) 463 buildCtx, err := createBuildContext(dockerfile) 464 assert.NilError(t, err) 465 defer os.RemoveAll(buildCtx) 466 467 validCases := []struct { 468 name string 469 network string 470 exitCode int 471 }{ 472 // When network=none, can't connect to internet, therefore cannot download packages in the dockerfile 473 // Order is important here, test fails for `-test.target=docker` in CI 474 {"test_with_no_network", "none", 1}, 475 {"test_with_empty_network", "", 0}, 476 {"test_with_default_network", "default", 0}, 477 } 478 479 for _, tc := range validCases { 480 tc := tc 481 t.Run(tc.name, func(t *testing.T) { 482 // --no-cache is intentional here for `-test.target=docker` 483 base.Cmd("build", buildCtx, "-t", tc.name, "--no-cache", "--network", tc.network).AssertExitCode(tc.exitCode) 484 if tc.exitCode != 1 { 485 defer base.Cmd("rmi", tc.name).AssertOK() 486 } 487 }) 488 } 489 } 490 491 func TestBuildNetworkShellCompletion(t *testing.T) { 492 testutil.DockerIncompatible(t) 493 base := testutil.NewBase(t) 494 const gsc = "__complete" 495 // Tests with build network 496 networkName := "default" 497 base.Cmd(gsc, "build", "--network", "").AssertOutContains(networkName) 498 }