github.com/buildtool/build-tools@v0.2.29-0.20240322150259-6a1d0a553c23/pkg/push/push_test.go (about) 1 // MIT License 2 // 3 // Copyright (c) 2018 buildtool 4 // 5 // Permission is hereby granted, free of charge, to any person obtaining a copy 6 // of this software and associated documentation files (the "Software"), to deal 7 // in the Software without restriction, including without limitation the rights 8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 // copies of the Software, and to permit persons to whom the Software is 10 // furnished to do so, subject to the following conditions: 11 // 12 // The above copyright notice and this permission notice shall be included in all 13 // copies or substantial portions of the Software. 14 // 15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 // SOFTWARE. 22 23 package push 24 25 import ( 26 "errors" 27 "fmt" 28 "os" 29 "path/filepath" 30 "strings" 31 "testing" 32 33 "github.com/apex/log" 34 mocks "gitlab.com/unboundsoftware/apex-mocks" 35 36 "github.com/buildtool/build-tools/pkg" 37 "github.com/buildtool/build-tools/pkg/config" 38 "github.com/buildtool/build-tools/pkg/docker" 39 "github.com/buildtool/build-tools/pkg/registry" 40 "github.com/buildtool/build-tools/pkg/vcs" 41 "github.com/buildtool/build-tools/pkg/version" 42 43 types "github.com/docker/docker/api/types/registry" 44 "github.com/stretchr/testify/assert" 45 ) 46 47 var name string 48 49 func TestMain(m *testing.M) { 50 tempDir := setup() 51 code := m.Run() 52 teardown(tempDir) 53 os.Exit(code) 54 } 55 56 func setup() string { 57 name, _ = os.MkdirTemp(os.TempDir(), "build-tools") 58 59 return name 60 } 61 62 func teardown(tempDir string) { 63 _ = os.RemoveAll(tempDir) 64 } 65 66 func TestPush_BadDockerHost(t *testing.T) { 67 defer func() { _ = os.RemoveAll(name) }() 68 69 defer pkg.SetEnv("DOCKER_HOST", "abc-123")() 70 code := Push(name, version.Info{}) 71 assert.Equal(t, -1, code) 72 } 73 74 func TestPush(t *testing.T) { 75 defer func() { _ = os.RemoveAll(name) }() 76 code := Push(name, version.Info{}) 77 assert.Equal(t, -5, code) 78 } 79 80 func TestPush_BrokenConfig(t *testing.T) { 81 defer func() { _ = os.RemoveAll(name) }() 82 yaml := `ci: [] ` 83 _ = write(name, ".buildtools.yaml", yaml) 84 85 logMock := mocks.New() 86 log.SetHandler(logMock) 87 log.SetLevel(log.DebugLevel) 88 exitCode := Push(name, version.Info{}) 89 90 assert.Equal(t, -2, exitCode) 91 logMock.Check(t, []string{ 92 fmt.Sprintf("debug: Parsing config from file: <green>'%s'</green>\n", filepath.Join(name, ".buildtools.yaml")), 93 "error: <red>yaml: unmarshal errors:\n line 1: cannot unmarshal !!seq into config.CIConfig</red>", 94 }) 95 } 96 97 func TestPush_NoRegistry(t *testing.T) { 98 defer func() { _ = os.RemoveAll(name) }() 99 _ = write(name, "Dockerfile", "FROM scratch") 100 101 logMock := mocks.New() 102 log.SetHandler(logMock) 103 log.SetLevel(log.DebugLevel) 104 client := &docker.MockDocker{} 105 cfg := config.InitEmptyConfig() 106 cfg.VCS.VCS = &no{} 107 108 exitCode := doPush(client, cfg, name, "Dockerfile") 109 110 assert.Equal(t, -6, exitCode) 111 logMock.Check(t, []string{ 112 "debug: Authentication <yellow>not supported</yellow> for registry <green>No docker registry</green>\n", 113 "error: Commit and/or branch information is <red>missing</red>. Perhaps your not in a Git repository or forgot to set environment variables?"}) 114 } 115 func TestPush_PushError(t *testing.T) { 116 defer func() { _ = os.RemoveAll(name) }() 117 _ = write(name, "Dockerfile", "FROM scratch") 118 119 logMock := mocks.New() 120 log.SetHandler(logMock) 121 log.SetLevel(log.DebugLevel) 122 client := &docker.MockDocker{PushError: fmt.Errorf("unable to push layer")} 123 cfg := config.InitEmptyConfig() 124 cfg.CI.Gitlab.CIBuildName = "project" 125 cfg.VCS.VCS = &no{} 126 cfg.Registry.Dockerhub.Namespace = "repo" 127 128 exitCode := doPush(client, cfg, name, "Dockerfile") 129 130 assert.NotNil(t, exitCode) 131 assert.Equal(t, -6, exitCode) 132 logMock.Check(t, []string{ 133 "debug: Logged in\n", 134 "error: Commit and/or branch information is <red>missing</red>. Perhaps your not in a Git repository or forgot to set environment variables?", 135 }) 136 } 137 138 func TestPush_PushFeatureBranch(t *testing.T) { 139 defer func() { _ = os.RemoveAll(name) }() 140 _ = write(name, "Dockerfile", "FROM scratch") 141 142 logMock := mocks.New() 143 log.SetHandler(logMock) 144 log.SetLevel(log.DebugLevel) 145 pushOut := `{"status":"Push successful"}` 146 client := &docker.MockDocker{PushOutput: &pushOut} 147 cfg := config.InitEmptyConfig() 148 cfg.CI.Gitlab.CIBuildName = "reponame" 149 cfg.CI.Gitlab.CICommit = "abc123" 150 cfg.CI.Gitlab.CIBranchName = "feature1" 151 cfg.Registry.Dockerhub.Namespace = "repo" 152 153 exitCode := doPush(client, cfg, name, "Dockerfile") 154 155 assert.Equal(t, 0, exitCode) 156 assert.Equal(t, []string{"repo/reponame:abc123", "repo/reponame:feature1"}, client.Images) 157 logMock.Check(t, []string{ 158 "debug: Logged in\n", 159 "info: Pushing tag '<green>repo/reponame:abc123</green>'\n", 160 "info: Pushing tag '<green>repo/reponame:feature1</green>'\n"}) 161 } 162 163 func TestPush_PushMasterBranch(t *testing.T) { 164 defer func() { _ = os.RemoveAll(name) }() 165 _ = write(name, "Dockerfile", "FROM scratch") 166 167 logMock := mocks.New() 168 log.SetHandler(logMock) 169 log.SetLevel(log.DebugLevel) 170 pushOut := `{"status":"Push successful"}` 171 client := &docker.MockDocker{PushOutput: &pushOut} 172 cfg := config.InitEmptyConfig() 173 cfg.CI.Gitlab.CIBuildName = "reponame" 174 cfg.CI.Gitlab.CICommit = "abc123" 175 cfg.CI.Gitlab.CIBranchName = "master" 176 cfg.Registry.Dockerhub.Namespace = "repo" 177 exitCode := doPush(client, cfg, name, "Dockerfile") 178 179 assert.Equal(t, 0, exitCode) 180 assert.Equal(t, []string{ 181 "repo/reponame:abc123", "repo/reponame:master", "repo/reponame:latest"}, client.Images) 182 logMock.Check(t, []string{"debug: Logged in\n", 183 "info: Pushing tag '<green>repo/reponame:abc123</green>'\n", 184 "info: Pushing tag '<green>repo/reponame:master</green>'\n", 185 "info: Pushing tag '<green>repo/reponame:latest</green>'\n"}) 186 } 187 func TestPush_PushMainBranch(t *testing.T) { 188 defer func() { _ = os.RemoveAll(name) }() 189 _ = write(name, "Dockerfile", "FROM scratch") 190 191 logMock := mocks.New() 192 log.SetHandler(logMock) 193 log.SetLevel(log.DebugLevel) 194 pushOut := `{"status":"Push successful"}` 195 client := &docker.MockDocker{PushOutput: &pushOut} 196 cfg := config.InitEmptyConfig() 197 cfg.CI.Gitlab.CIBuildName = "reponame" 198 cfg.CI.Gitlab.CICommit = "abc123" 199 cfg.CI.Gitlab.CIBranchName = "main" 200 cfg.Registry.Dockerhub.Namespace = "repo" 201 exitCode := doPush(client, cfg, name, "Dockerfile") 202 203 assert.Equal(t, 0, exitCode) 204 assert.Equal(t, []string{"repo/reponame:abc123", "repo/reponame:main", "repo/reponame:latest"}, client.Images) 205 logMock.Check(t, []string{"debug: Logged in\n", 206 "info: Pushing tag '<green>repo/reponame:abc123</green>'\n", 207 "info: Pushing tag '<green>repo/reponame:main</green>'\n", 208 "info: Pushing tag '<green>repo/reponame:latest</green>'\n"}) 209 } 210 211 func TestPush_Multistage(t *testing.T) { 212 defer func() { _ = os.RemoveAll(name) }() 213 dockerfile := ` 214 FROM scratch as build 215 RUN echo apa > file 216 FROM scratch as test 217 RUN echo cepa > file2 218 FROM scratch 219 COPY --from=build file . 220 COPY --from=test file2 . 221 ` 222 _ = write(name, "Dockerfile", dockerfile) 223 224 logMock := mocks.New() 225 log.SetHandler(logMock) 226 log.SetLevel(log.DebugLevel) 227 pushOut := `{"status":"Push successful"}` 228 client := &docker.MockDocker{PushOutput: &pushOut} 229 cfg := config.InitEmptyConfig() 230 cfg.CI.Gitlab.CIBuildName = "reponame" 231 cfg.CI.Gitlab.CICommit = "abc123" 232 cfg.CI.Gitlab.CIBranchName = "master" 233 cfg.Registry.Dockerhub.Namespace = "repo" 234 235 exitCode := doPush(client, cfg, name, "Dockerfile") 236 237 assert.Equal(t, 0, exitCode) 238 assert.Equal(t, []string{"repo/reponame:build", "repo/reponame:test", "repo/reponame:abc123", "repo/reponame:master", "repo/reponame:latest"}, client.Images) 239 logMock.Check(t, []string{"debug: Logged in\n", 240 "info: Pushing tag '<green>repo/reponame:build</green>'\n", 241 "info: Pushing tag '<green>repo/reponame:test</green>'\n", 242 "info: Pushing tag '<green>repo/reponame:abc123</green>'\n", 243 "info: Pushing tag '<green>repo/reponame:master</green>'\n", 244 "info: Pushing tag '<green>repo/reponame:latest</green>'\n"}) 245 } 246 247 func TestPush_Output(t *testing.T) { 248 defer func() { _ = os.RemoveAll(name) }() 249 _ = write(name, "Dockerfile", "FROM scratch") 250 251 logMock := mocks.New() 252 log.SetHandler(logMock) 253 log.SetLevel(log.DebugLevel) 254 pushOut := `{"status":"The push refers to repository [registry.gitlab.com/project/image]"} 255 {"status":"Preparing","progressDetail":{},"id":"c49bda176134"} 256 {"status":"Preparing","progressDetail":{},"id":"cb13bd9b95b6"} 257 {"status":"Preparing","progressDetail":{},"id":"5905e8d02856"} 258 {"status":"Preparing","progressDetail":{},"id":"e3ef84c7b541"} 259 {"status":"Preparing","progressDetail":{},"id":"6096558c3d50"} 260 {"status":"Preparing","progressDetail":{},"id":"3b12aae5d4ca"} 261 {"status":"Preparing","progressDetail":{},"id":"ac7b6b272904"} 262 {"status":"Preparing","progressDetail":{},"id":"5b1304247ae3"} 263 {"status":"Preparing","progressDetail":{},"id":"75e70aa52609"} 264 {"status":"Preparing","progressDetail":{},"id":"dda151859818"} 265 {"status":"Preparing","progressDetail":{},"id":"fbd2732ad777"} 266 {"status":"Preparing","progressDetail":{},"id":"ba9de9d8475e"} 267 {"status":"Waiting","progressDetail":{},"id":"dda151859818"} 268 {"status":"Waiting","progressDetail":{},"id":"3b12aae5d4ca"} 269 {"status":"Waiting","progressDetail":{},"id":"ac7b6b272904"} 270 {"status":"Waiting","progressDetail":{},"id":"ba9de9d8475e"} 271 {"status":"Waiting","progressDetail":{},"id":"5b1304247ae3"} 272 {"status":"Waiting","progressDetail":{},"id":"75e70aa52609"} 273 {"status":"Waiting","progressDetail":{},"id":"fbd2732ad777"} 274 {"status":"Layer already exists","progressDetail":{},"id":"6096558c3d50"} 275 {"status":"Layer already exists","progressDetail":{},"id":"c49bda176134"} 276 {"status":"Layer already exists","progressDetail":{},"id":"e3ef84c7b541"} 277 {"status":"Pushing","progressDetail":{"current":512,"total":13088},"progress":"[=\u003e ] 512B/13.09kB","id":"cb13bd9b95b6"} 278 {"status":"Pushing","progressDetail":{"current":16896,"total":13088},"progress":"[==================================================\u003e] 16.9kB","id":"cb13bd9b95b6"} 279 {"status":"Pushing","progressDetail":{"current":512,"total":3511},"progress":"[=======\u003e ] 512B/3.511kB","id":"5905e8d02856"} 280 {"status":"Pushing","progressDetail":{"current":6144,"total":3511},"progress":"[==================================================\u003e] 6.144kB","id":"5905e8d02856"} 281 {"status":"Layer already exists","progressDetail":{},"id":"ac7b6b272904"} 282 {"status":"Layer already exists","progressDetail":{},"id":"3b12aae5d4ca"} 283 {"status":"Layer already exists","progressDetail":{},"id":"5b1304247ae3"} 284 {"status":"Layer already exists","progressDetail":{},"id":"75e70aa52609"} 285 {"status":"Layer already exists","progressDetail":{},"id":"dda151859818"} 286 {"status":"Layer already exists","progressDetail":{},"id":"fbd2732ad777"} 287 {"status":"Layer already exists","progressDetail":{},"id":"ba9de9d8475e"} 288 {"status":"Pushed","progressDetail":{},"id":"5905e8d02856"} 289 {"status":"Pushed","progressDetail":{},"id":"cb13bd9b95b6"} 290 {"status":"cd38b8b25e3e62d05589ad6b4639e2e222086604: digest: sha256:af534ee896ce2ac80f3413318329e45e3b3e74b89eb337b9364b8ac1e83498b7 size: 2828"} 291 {"progressDetail":{},"aux":{"Tag":"cd38b8b25e3e62d05589ad6b4639e2e222086604","Digest":"sha256:af534ee896ce2ac80f3413318329e45e3b3e74b89eb337b9364b8ac1e83498b7","Size":2828}} 292 ` 293 client := &docker.MockDocker{PushOutput: &pushOut} 294 cfg := config.InitEmptyConfig() 295 cfg.CI.Gitlab.CIBuildName = "reponame" 296 cfg.CI.Gitlab.CICommit = "abc123" 297 cfg.CI.Gitlab.CIBranchName = "master" 298 cfg.Registry.Dockerhub.Namespace = "repo" 299 300 exitCode := doPush(client, cfg, name, "Dockerfile") 301 302 assert.Equal(t, 0, exitCode) 303 assert.Equal(t, []string{"repo/reponame:abc123", "repo/reponame:master", "repo/reponame:latest"}, client.Images) 304 logMock.Check(t, []string{"debug: Logged in\n", 305 "info: Pushing tag '<green>repo/reponame:abc123</green>'\n", 306 "info: Pushing tag '<green>repo/reponame:master</green>'\n", 307 "info: Pushing tag '<green>repo/reponame:latest</green>'\n"}) 308 } 309 310 func TestPush_BrokenOutput(t *testing.T) { 311 defer func() { _ = os.RemoveAll(name) }() 312 _ = write(name, "Dockerfile", "FROM scratch") 313 314 logMock := mocks.New() 315 log.SetHandler(logMock) 316 log.SetLevel(log.DebugLevel) 317 pushOut := `Broken output` 318 client := &docker.MockDocker{PushOutput: &pushOut} 319 cfg := config.InitEmptyConfig() 320 cfg.CI.Gitlab.CIBuildName = "reponame" 321 cfg.CI.Gitlab.CICommit = "abc123" 322 cfg.CI.Gitlab.CIBranchName = "master" 323 cfg.Registry.Dockerhub.Namespace = "repo" 324 exitCode := doPush(client, cfg, name, "Dockerfile") 325 326 assert.Equal(t, -7, exitCode) 327 logMock.Check(t, []string{ 328 "debug: Logged in\n", 329 "info: Pushing tag '<green>repo/reponame:abc123</green>'\n", 330 "error: Unable to parse response: Broken output, Error: invalid character 'B' looking for beginning of value\n", 331 "error: <red>invalid character 'B' looking for beginning of value</red>"}) 332 } 333 334 func TestPush_ErrorDetail(t *testing.T) { 335 defer func() { _ = os.RemoveAll(name) }() 336 _ = write(name, "Dockerfile", "FROM scratch") 337 338 logMock := mocks.New() 339 log.SetHandler(logMock) 340 log.SetLevel(log.DebugLevel) 341 pushOut := `{"status":"", "errorDetail":{"message":"error details"}}` 342 client := &docker.MockDocker{PushOutput: &pushOut} 343 cfg := config.InitEmptyConfig() 344 cfg.CI.Gitlab.CIBuildName = "reponame" 345 cfg.CI.Gitlab.CICommit = "abc123" 346 cfg.CI.Gitlab.CIBranchName = "master" 347 cfg.Registry.Dockerhub.Namespace = "repo" 348 exitCode := doPush(client, cfg, name, "Dockerfile") 349 350 assert.Equal(t, -7, exitCode) 351 logMock.Check(t, []string{ 352 "debug: Logged in\n", 353 "info: Pushing tag '<green>repo/reponame:abc123</green>'\n", 354 "error: <red>error details</red>"}) 355 } 356 357 func TestPush_Create_Error(t *testing.T) { 358 defer func() { _ = os.RemoveAll(name) }() 359 _ = write(name, "Dockerfile", "FROM scratch") 360 361 logMock := mocks.New() 362 log.SetHandler(logMock) 363 log.SetLevel(log.DebugLevel) 364 pushOut := `Broken output` 365 client := &docker.MockDocker{PushOutput: &pushOut} 366 cfg := config.InitEmptyConfig() 367 cfg.AvailableRegistries = []registry.Registry{&mockRegistry{}} 368 cfg.CI.Gitlab.CIBuildName = "reponame" 369 cfg.CI.Gitlab.CICommit = "abc123" 370 cfg.CI.Gitlab.CIBranchName = "master" 371 cfg.Registry.Dockerhub.Namespace = "repo" 372 exitCode := doPush(client, cfg, name, "Dockerfile") 373 374 assert.Equal(t, -4, exitCode) 375 logMock.Check(t, []string{ 376 "error: <red>create error</red>"}) 377 } 378 379 func TestPush_UnreadableDockerfile(t *testing.T) { 380 defer func() { _ = os.RemoveAll(name) }() 381 dockerfile := filepath.Join(name, "Dockerfile") 382 _ = os.MkdirAll(dockerfile, 0777) 383 384 logMock := mocks.New() 385 log.SetHandler(logMock) 386 log.SetLevel(log.DebugLevel) 387 pushOut := `Broken output` 388 client := &docker.MockDocker{PushOutput: &pushOut} 389 cfg := config.InitEmptyConfig() 390 cfg.CI.Gitlab.CIBuildName = "reponame" 391 cfg.CI.Gitlab.CICommit = "abc123" 392 cfg.CI.Gitlab.CIBranchName = "master" 393 cfg.Registry.Dockerhub.Namespace = "repo" 394 exitCode := doPush(client, cfg, name, "Dockerfile") 395 396 assert.Equal(t, -5, exitCode) 397 logMock.Check(t, []string{ 398 "debug: Logged in\n", 399 fmt.Sprintf("error: <red>read %s: is a directory</red>", dockerfile)}) 400 } 401 402 type mockRegistry struct { 403 } 404 405 func (m mockRegistry) Configured() bool { 406 return true 407 } 408 409 func (m mockRegistry) Name() string { 410 panic("implement me") 411 } 412 413 func (m mockRegistry) Login(client docker.Client) error { 414 return nil 415 } 416 417 func (m mockRegistry) GetAuthConfig() types.AuthConfig { 418 return types.AuthConfig{} 419 } 420 421 func (m mockRegistry) GetAuthInfo() string { 422 return "" 423 } 424 425 func (m mockRegistry) RegistryUrl() string { 426 panic("implement me") 427 } 428 429 func (m mockRegistry) Create(repository string) error { 430 return errors.New("create error") 431 } 432 433 func (m mockRegistry) PushImage(client docker.Client, auth, image string) error { 434 panic("implement me") 435 } 436 437 var _ registry.Registry = &mockRegistry{} 438 439 type no struct { 440 vcs.CommonVCS 441 } 442 443 func (v no) Identify(dir string) bool { 444 v.CurrentCommit = "" 445 v.CurrentBranch = "" 446 447 return true 448 } 449 450 func (v no) Name() string { 451 return "none" 452 } 453 454 var _ vcs.VCS = &no{} 455 456 func write(dir, file, content string) error { 457 if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, file)), 0777); err != nil { 458 return err 459 } 460 return os.WriteFile(filepath.Join(dir, file), []byte(fmt.Sprintln(strings.TrimSpace(content))), 0666) 461 }