github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/cli/command/image/build_test.go (about)

     1  package image
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"compress/gzip"
     7  	"context"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"testing"
    13  
    14  	"github.com/docker/cli/cli/streams"
    15  	"github.com/docker/cli/internal/test"
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/pkg/archive"
    18  	"github.com/google/go-cmp/cmp"
    19  	"gotest.tools/v3/assert"
    20  	"gotest.tools/v3/fs"
    21  	"gotest.tools/v3/skip"
    22  )
    23  
    24  func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
    25  	t.Setenv("DOCKER_BUILDKIT", "0")
    26  	buffer := new(bytes.Buffer)
    27  	fakeBuild := newFakeBuild()
    28  	fakeImageBuild := func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
    29  		tee := io.TeeReader(buildContext, buffer)
    30  		gzipReader, err := gzip.NewReader(tee)
    31  		assert.NilError(t, err)
    32  		return fakeBuild.build(ctx, gzipReader, options)
    33  	}
    34  
    35  	cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
    36  	dockerfile := bytes.NewBufferString(`
    37  		FROM alpine:frozen
    38  		COPY foo /
    39  	`)
    40  	cli.SetIn(streams.NewIn(io.NopCloser(dockerfile)))
    41  
    42  	dir := fs.NewDir(t, t.Name(),
    43  		fs.WithFile("foo", "some content"))
    44  	defer dir.Remove()
    45  
    46  	options := newBuildOptions()
    47  	options.compress = true
    48  	options.dockerfileName = "-"
    49  	options.context = dir.Path()
    50  	options.untrusted = true
    51  	assert.NilError(t, runBuild(context.TODO(), cli, options))
    52  
    53  	expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "foo"}
    54  	assert.DeepEqual(t, expected, fakeBuild.filenames(t))
    55  
    56  	header := buffer.Bytes()[:10]
    57  	assert.Equal(t, archive.Gzip, archive.DetectCompression(header))
    58  }
    59  
    60  func TestRunBuildResetsUidAndGidInContext(t *testing.T) {
    61  	skip.If(t, os.Getuid() != 0, "root is required to chown files")
    62  	t.Setenv("DOCKER_BUILDKIT", "0")
    63  	fakeBuild := newFakeBuild()
    64  	cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
    65  
    66  	dir := fs.NewDir(t, "test-build-context",
    67  		fs.WithFile("foo", "some content", fs.AsUser(65534, 65534)),
    68  		fs.WithFile("Dockerfile", `
    69  			FROM alpine:frozen
    70  			COPY foo bar /
    71  		`),
    72  	)
    73  	defer dir.Remove()
    74  
    75  	options := newBuildOptions()
    76  	options.context = dir.Path()
    77  	options.untrusted = true
    78  	assert.NilError(t, runBuild(context.TODO(), cli, options))
    79  
    80  	headers := fakeBuild.headers(t)
    81  	expected := []*tar.Header{
    82  		{Name: "Dockerfile"},
    83  		{Name: "foo"},
    84  	}
    85  	cmpTarHeaderNameAndOwner := cmp.Comparer(func(x, y tar.Header) bool {
    86  		return x.Name == y.Name && x.Uid == y.Uid && x.Gid == y.Gid
    87  	})
    88  	assert.DeepEqual(t, expected, headers, cmpTarHeaderNameAndOwner)
    89  }
    90  
    91  func TestRunBuildDockerfileOutsideContext(t *testing.T) {
    92  	t.Setenv("DOCKER_BUILDKIT", "0")
    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(context.TODO(), 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  	t.Setenv("DOCKER_BUILDKIT", "0")
   126  	cmd := NewBuildCommand(test.NewFakeCli(&fakeClient{}))
   127  	// Clone a small repo that exists so git doesn't prompt for credentials
   128  	cmd.SetArgs([]string{"github.com/docker/for-win"})
   129  	cmd.SetOut(io.Discard)
   130  	err := cmd.Execute()
   131  	assert.ErrorContains(t, err, "unable to prepare context")
   132  	assert.ErrorContains(t, err, "docker-build-git")
   133  }
   134  
   135  // TestRunBuildFromLocalGitHubDirNonExistingRepo tests that a local directory
   136  // starting with `github.com` takes precedence over the `github.com` special
   137  // case.
   138  func TestRunBuildFromLocalGitHubDir(t *testing.T) {
   139  	t.Setenv("DOCKER_BUILDKIT", "0")
   140  
   141  	buildDir := filepath.Join(t.TempDir(), "github.com", "docker", "no-such-repository")
   142  	err := os.MkdirAll(buildDir, 0o777)
   143  	assert.NilError(t, err)
   144  	err = os.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0o644)
   145  	assert.NilError(t, err)
   146  
   147  	client := test.NewFakeCli(&fakeClient{})
   148  	cmd := NewBuildCommand(client)
   149  	cmd.SetArgs([]string{buildDir})
   150  	cmd.SetOut(io.Discard)
   151  	err = cmd.Execute()
   152  	assert.NilError(t, err)
   153  }
   154  
   155  func TestRunBuildWithSymlinkedContext(t *testing.T) {
   156  	t.Setenv("DOCKER_BUILDKIT", "0")
   157  	dockerfile := `
   158  FROM alpine:frozen
   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(context.TODO(), cli, options))
   174  
   175  	assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
   176  }
   177  
   178  type fakeBuild struct {
   179  	context *tar.Reader
   180  	options types.ImageBuildOptions
   181  }
   182  
   183  func newFakeBuild() *fakeBuild {
   184  	return &fakeBuild{}
   185  }
   186  
   187  func (f *fakeBuild) build(_ context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
   188  	f.context = tar.NewReader(buildContext)
   189  	f.options = options
   190  	body := new(bytes.Buffer)
   191  	return types.ImageBuildResponse{Body: io.NopCloser(body)}, nil
   192  }
   193  
   194  func (f *fakeBuild) headers(t *testing.T) []*tar.Header {
   195  	t.Helper()
   196  	headers := []*tar.Header{}
   197  	for {
   198  		hdr, err := f.context.Next()
   199  		switch err {
   200  		case io.EOF:
   201  			return headers
   202  		case nil:
   203  			headers = append(headers, hdr)
   204  		default:
   205  			assert.NilError(t, err)
   206  		}
   207  	}
   208  }
   209  
   210  func (f *fakeBuild) filenames(t *testing.T) []string {
   211  	t.Helper()
   212  	names := []string{}
   213  	for _, header := range f.headers(t) {
   214  		names = append(names, header.Name)
   215  	}
   216  	sort.Strings(names)
   217  	return names
   218  }