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  }