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