github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/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, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { 29 tee := io.TeeReader(context, 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:3.6 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(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:3.6 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(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(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: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 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, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { 188 f.context = tar.NewReader(context) 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 }