github.com/ali-iotechsys/cli@v20.10.0+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 }