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  }