github.com/xeptore/docker-cli@v20.10.14+incompatible/cli/command/image/build_test.go (about)

     1  package image
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"compress/gzip"
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  	"sort"
    14  	"testing"
    15  
    16  	"github.com/docker/cli/cli/streams"
    17  	"github.com/docker/cli/internal/test"
    18  	"github.com/docker/docker/api/types"
    19  	"github.com/docker/docker/pkg/archive"
    20  	"github.com/google/go-cmp/cmp"
    21  	"github.com/moby/buildkit/session/secrets/secretsprovider"
    22  	"gotest.tools/v3/assert"
    23  	"gotest.tools/v3/env"
    24  	"gotest.tools/v3/fs"
    25  	"gotest.tools/v3/skip"
    26  )
    27  
    28  func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
    29  	defer env.Patch(t, "DOCKER_BUILDKIT", "0")()
    30  	buffer := new(bytes.Buffer)
    31  	fakeBuild := newFakeBuild()
    32  	fakeImageBuild := func(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
    33  		tee := io.TeeReader(context, buffer)
    34  		gzipReader, err := gzip.NewReader(tee)
    35  		assert.NilError(t, err)
    36  		return fakeBuild.build(ctx, gzipReader, options)
    37  	}
    38  
    39  	cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
    40  	dockerfile := bytes.NewBufferString(`
    41  		FROM alpine:3.6
    42  		COPY foo /
    43  	`)
    44  	cli.SetIn(streams.NewIn(ioutil.NopCloser(dockerfile)))
    45  
    46  	dir := fs.NewDir(t, t.Name(),
    47  		fs.WithFile("foo", "some content"))
    48  	defer dir.Remove()
    49  
    50  	options := newBuildOptions()
    51  	options.compress = true
    52  	options.dockerfileName = "-"
    53  	options.context = dir.Path()
    54  	options.untrusted = true
    55  	assert.NilError(t, runBuild(cli, options))
    56  
    57  	expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "foo"}
    58  	assert.DeepEqual(t, expected, fakeBuild.filenames(t))
    59  
    60  	header := buffer.Bytes()[:10]
    61  	assert.Equal(t, archive.Gzip, archive.DetectCompression(header))
    62  }
    63  
    64  func TestRunBuildResetsUidAndGidInContext(t *testing.T) {
    65  	defer env.Patch(t, "DOCKER_BUILDKIT", "0")()
    66  	skip.If(t, os.Getuid() != 0, "root is required to chown files")
    67  	fakeBuild := newFakeBuild()
    68  	cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
    69  
    70  	dir := fs.NewDir(t, "test-build-context",
    71  		fs.WithFile("foo", "some content", fs.AsUser(65534, 65534)),
    72  		fs.WithFile("Dockerfile", `
    73  			FROM alpine:3.6
    74  			COPY foo bar /
    75  		`),
    76  	)
    77  	defer dir.Remove()
    78  
    79  	options := newBuildOptions()
    80  	options.context = dir.Path()
    81  	options.untrusted = true
    82  	assert.NilError(t, runBuild(cli, options))
    83  
    84  	headers := fakeBuild.headers(t)
    85  	expected := []*tar.Header{
    86  		{Name: "Dockerfile"},
    87  		{Name: "foo"},
    88  	}
    89  	var cmpTarHeaderNameAndOwner = cmp.Comparer(func(x, y tar.Header) bool {
    90  		return x.Name == y.Name && x.Uid == y.Uid && x.Gid == y.Gid
    91  	})
    92  	assert.DeepEqual(t, expected, headers, cmpTarHeaderNameAndOwner)
    93  }
    94  
    95  func TestRunBuildDockerfileOutsideContext(t *testing.T) {
    96  	defer env.Patch(t, "DOCKER_BUILDKIT", "0")()
    97  	dir := fs.NewDir(t, t.Name(),
    98  		fs.WithFile("data", "data file"))
    99  	defer dir.Remove()
   100  
   101  	// Dockerfile outside of build-context
   102  	df := fs.NewFile(t, t.Name(),
   103  		fs.WithContent(`
   104  FROM FOOBAR
   105  COPY data /data
   106  		`),
   107  	)
   108  	defer df.Remove()
   109  
   110  	fakeBuild := newFakeBuild()
   111  	cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
   112  
   113  	options := newBuildOptions()
   114  	options.context = dir.Path()
   115  	options.dockerfileName = df.Path()
   116  	options.untrusted = true
   117  	assert.NilError(t, runBuild(cli, options))
   118  
   119  	expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "data"}
   120  	assert.DeepEqual(t, expected, fakeBuild.filenames(t))
   121  }
   122  
   123  // TestRunBuildFromLocalGitHubDirNonExistingRepo tests that build contexts
   124  // starting with `github.com/` are special-cased, and the build command attempts
   125  // to clone the remote repo.
   126  // TODO: test "context selection" logic directly when runBuild is refactored
   127  // to support testing (ex: docker/cli#294)
   128  func TestRunBuildFromGitHubSpecialCase(t *testing.T) {
   129  	defer env.Patch(t, "DOCKER_BUILDKIT", "0")()
   130  	cmd := NewBuildCommand(test.NewFakeCli(&fakeClient{}))
   131  	// Clone a small repo that exists so git doesn't prompt for credentials
   132  	cmd.SetArgs([]string{"github.com/docker/for-win"})
   133  	cmd.SetOut(ioutil.Discard)
   134  	err := cmd.Execute()
   135  	assert.ErrorContains(t, err, "unable to prepare context")
   136  	assert.ErrorContains(t, err, "docker-build-git")
   137  }
   138  
   139  // TestRunBuildFromLocalGitHubDirNonExistingRepo tests that a local directory
   140  // starting with `github.com` takes precedence over the `github.com` special
   141  // case.
   142  func TestRunBuildFromLocalGitHubDir(t *testing.T) {
   143  	defer env.Patch(t, "DOCKER_BUILDKIT", "0")()
   144  	tmpDir, err := ioutil.TempDir("", "docker-build-from-local-dir-")
   145  	assert.NilError(t, err)
   146  	defer os.RemoveAll(tmpDir)
   147  
   148  	buildDir := filepath.Join(tmpDir, "github.com", "docker", "no-such-repository")
   149  	err = os.MkdirAll(buildDir, 0777)
   150  	assert.NilError(t, err)
   151  	err = ioutil.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0644)
   152  	assert.NilError(t, err)
   153  
   154  	client := test.NewFakeCli(&fakeClient{})
   155  	cmd := NewBuildCommand(client)
   156  	cmd.SetArgs([]string{buildDir})
   157  	cmd.SetOut(ioutil.Discard)
   158  	err = cmd.Execute()
   159  	assert.NilError(t, err)
   160  }
   161  
   162  func TestRunBuildWithSymlinkedContext(t *testing.T) {
   163  	defer env.Patch(t, "DOCKER_BUILDKIT", "0")()
   164  	dockerfile := `
   165  FROM alpine:3.6
   166  RUN echo hello world
   167  `
   168  
   169  	tmpDir := fs.NewDir(t, t.Name(),
   170  		fs.WithDir("context",
   171  			fs.WithFile("Dockerfile", dockerfile)),
   172  		fs.WithSymlink("context-link", "context"))
   173  	defer tmpDir.Remove()
   174  
   175  	fakeBuild := newFakeBuild()
   176  	cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
   177  	options := newBuildOptions()
   178  	options.context = tmpDir.Join("context-link")
   179  	options.untrusted = true
   180  	assert.NilError(t, runBuild(cli, options))
   181  
   182  	assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
   183  }
   184  
   185  func TestParseSecret(t *testing.T) {
   186  	type testcase struct {
   187  		value       string
   188  		errExpected bool
   189  		errMatch    string
   190  		source      *secretsprovider.Source
   191  	}
   192  	var testcases = []testcase{
   193  		{
   194  			value:       "",
   195  			errExpected: true,
   196  		}, {
   197  			value:       "foobar",
   198  			errExpected: true,
   199  			errMatch:    "must be a key=value pair",
   200  		}, {
   201  			value:       "foo,bar",
   202  			errExpected: true,
   203  			errMatch:    "must be a key=value pair",
   204  		}, {
   205  			value:       "foo=bar",
   206  			errExpected: true,
   207  			errMatch:    "unexpected key",
   208  		}, {
   209  			value:  "src=somefile",
   210  			source: &secretsprovider.Source{FilePath: "somefile"},
   211  		}, {
   212  			value:  "source=somefile",
   213  			source: &secretsprovider.Source{FilePath: "somefile"},
   214  		}, {
   215  			value:  "id=mysecret",
   216  			source: &secretsprovider.Source{ID: "mysecret"},
   217  		}, {
   218  			value:  "id=mysecret,src=somefile",
   219  			source: &secretsprovider.Source{ID: "mysecret", FilePath: "somefile"},
   220  		}, {
   221  			value:  "id=mysecret,source=somefile,type=file",
   222  			source: &secretsprovider.Source{ID: "mysecret", FilePath: "somefile"},
   223  		}, {
   224  			value:  "id=mysecret,src=somefile,src=othersecretfile",
   225  			source: &secretsprovider.Source{ID: "mysecret", FilePath: "othersecretfile"},
   226  		}, {
   227  			value:  "id=mysecret,src=somefile,env=SECRET",
   228  			source: &secretsprovider.Source{ID: "mysecret", FilePath: "somefile", Env: "SECRET"},
   229  		}, {
   230  			value:  "type=file",
   231  			source: &secretsprovider.Source{},
   232  		}, {
   233  			value:  "type=env",
   234  			source: &secretsprovider.Source{},
   235  		}, {
   236  			value:       "type=invalid",
   237  			errExpected: true,
   238  			errMatch:    "unsupported secret type",
   239  		},
   240  	}
   241  
   242  	for _, tc := range testcases {
   243  		t.Run(tc.value, func(t *testing.T) {
   244  			secret, err := parseSecret(tc.value)
   245  			assert.Equal(t, err != nil, tc.errExpected, fmt.Sprintf("err=%v errExpected=%t", err, tc.errExpected))
   246  			if tc.errMatch != "" {
   247  				assert.ErrorContains(t, err, tc.errMatch)
   248  			}
   249  			assert.DeepEqual(t, secret, tc.source)
   250  		})
   251  	}
   252  }
   253  
   254  type fakeBuild struct {
   255  	context *tar.Reader
   256  	options types.ImageBuildOptions
   257  }
   258  
   259  func newFakeBuild() *fakeBuild {
   260  	return &fakeBuild{}
   261  }
   262  
   263  func (f *fakeBuild) build(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
   264  	f.context = tar.NewReader(context)
   265  	f.options = options
   266  	body := new(bytes.Buffer)
   267  	return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
   268  }
   269  
   270  func (f *fakeBuild) headers(t *testing.T) []*tar.Header {
   271  	t.Helper()
   272  	headers := []*tar.Header{}
   273  	for {
   274  		hdr, err := f.context.Next()
   275  		switch err {
   276  		case io.EOF:
   277  			return headers
   278  		case nil:
   279  			headers = append(headers, hdr)
   280  		default:
   281  			assert.NilError(t, err)
   282  		}
   283  	}
   284  }
   285  
   286  func (f *fakeBuild) filenames(t *testing.T) []string {
   287  	t.Helper()
   288  	names := []string{}
   289  	for _, header := range f.headers(t) {
   290  		names = append(names, header.Name)
   291  	}
   292  	sort.Strings(names)
   293  	return names
   294  }