github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/docker/image_test.go (about)

     1  /*
     2  Copyright 2019 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package docker
    18  
    19  import (
    20  	"context"
    21  	"io/ioutil"
    22  	"sync"
    23  	"sync/atomic"
    24  	"testing"
    25  
    26  	"github.com/docker/docker/api/types"
    27  	"github.com/docker/docker/client"
    28  
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
    30  	sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/errors"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    33  	"github.com/GoogleContainerTools/skaffold/proto/v1"
    34  	"github.com/GoogleContainerTools/skaffold/testutil"
    35  )
    36  
    37  func TestPush(t *testing.T) {
    38  	tests := []struct {
    39  		description    string
    40  		imageName      string
    41  		api            *testutil.FakeAPIClient
    42  		expectedDigest string
    43  		shouldErr      bool
    44  	}{
    45  		{
    46  			description:    "push",
    47  			imageName:      "gcr.io/scratchman",
    48  			api:            (&testutil.FakeAPIClient{}).Add("gcr.io/scratchman", "sha256:imageIDabcab"),
    49  			expectedDigest: "sha256:bb1f952848763dd1f8fcf14231d7a4557775abf3c95e588561bc7a478c94e7e0",
    50  		},
    51  		{
    52  			description: "stream error",
    53  			imageName:   "gcr.io/imthescratchman",
    54  			api: &testutil.FakeAPIClient{
    55  				ErrStream: true,
    56  			},
    57  			shouldErr: true,
    58  		},
    59  		{
    60  			description: "image push error",
    61  			imageName:   "gcr.io/skibabopbadopbop",
    62  			api: &testutil.FakeAPIClient{
    63  				ErrImagePush: true,
    64  			},
    65  			shouldErr: true,
    66  		},
    67  	}
    68  	for _, test := range tests {
    69  		testutil.Run(t, test.description, func(t *testutil.T) {
    70  			t.Override(&DefaultAuthHelper, testAuthHelper{})
    71  
    72  			localDocker := NewLocalDaemon(test.api, nil, false, nil)
    73  			digest, err := localDocker.Push(context.Background(), ioutil.Discard, test.imageName)
    74  
    75  			t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expectedDigest, digest)
    76  		})
    77  	}
    78  }
    79  
    80  func TestDoNotPushAlreadyPushed(t *testing.T) {
    81  	testutil.Run(t, "", func(t *testutil.T) {
    82  		t.Override(&DefaultAuthHelper, testAuthHelper{})
    83  
    84  		api := &testutil.FakeAPIClient{}
    85  		api.Add("image", "sha256:imageIDabcab")
    86  		localDocker := NewLocalDaemon(api, nil, false, nil)
    87  
    88  		digest, err := localDocker.Push(context.Background(), ioutil.Discard, "image")
    89  		t.CheckNoError(err)
    90  		t.CheckDeepEqual("sha256:bb1f952848763dd1f8fcf14231d7a4557775abf3c95e588561bc7a478c94e7e0", digest)
    91  
    92  		// Images already pushed don't need being pushed.
    93  		api.ErrImagePush = true
    94  
    95  		digest, err = localDocker.Push(context.Background(), ioutil.Discard, "image")
    96  		t.CheckNoError(err)
    97  		t.CheckDeepEqual("sha256:bb1f952848763dd1f8fcf14231d7a4557775abf3c95e588561bc7a478c94e7e0", digest)
    98  	})
    99  }
   100  
   101  func TestBuild(t *testing.T) {
   102  	tests := []struct {
   103  		description   string
   104  		env           map[string]string
   105  		api           *testutil.FakeAPIClient
   106  		workspace     string
   107  		artifact      *latest.DockerArtifact
   108  		expected      types.ImageBuildOptions
   109  		mode          config.RunMode
   110  		shouldErr     bool
   111  		expectedError string
   112  	}{
   113  		{
   114  			description: "build",
   115  			api:         &testutil.FakeAPIClient{},
   116  			workspace:   ".",
   117  			artifact:    &latest.DockerArtifact{},
   118  			expected: types.ImageBuildOptions{
   119  				Tags:        []string{"finalimage"},
   120  				AuthConfigs: allAuthConfig,
   121  			},
   122  			mode: config.RunModes.Dev,
   123  		},
   124  		{
   125  			description: "build with options",
   126  			api:         &testutil.FakeAPIClient{},
   127  			env: map[string]string{
   128  				"VALUE3": "value3",
   129  			},
   130  			workspace: ".",
   131  			artifact: &latest.DockerArtifact{
   132  				DockerfilePath: "Dockerfile",
   133  				BuildArgs: map[string]*string{
   134  					"k1": nil,
   135  					"k2": util.StringPtr("value2"),
   136  					"k3": util.StringPtr("{{.VALUE3}}"),
   137  				},
   138  				CacheFrom:   []string{"from-1"},
   139  				Target:      "target",
   140  				NetworkMode: "None",
   141  				NoCache:     true,
   142  				PullParent:  true,
   143  			},
   144  			mode: config.RunModes.Dev,
   145  			expected: types.ImageBuildOptions{
   146  				Tags:       []string{"finalimage"},
   147  				Dockerfile: "Dockerfile",
   148  				BuildArgs: map[string]*string{
   149  					"k1": nil,
   150  					"k2": util.StringPtr("value2"),
   151  					"k3": util.StringPtr("value3"),
   152  				},
   153  				CacheFrom:   []string{"from-1"},
   154  				AuthConfigs: allAuthConfig,
   155  				Target:      "target",
   156  				NetworkMode: "none",
   157  				NoCache:     true,
   158  				PullParent:  true,
   159  			},
   160  		},
   161  		{
   162  			description: "bad image build",
   163  			api: &testutil.FakeAPIClient{
   164  				ErrImageBuild: true,
   165  			},
   166  			mode:          config.RunModes.Dev,
   167  			workspace:     ".",
   168  			artifact:      &latest.DockerArtifact{},
   169  			shouldErr:     true,
   170  			expectedError: "docker build",
   171  		},
   172  		{
   173  			description: "bad return reader",
   174  			api: &testutil.FakeAPIClient{
   175  				ErrStream: true,
   176  			},
   177  			workspace:     ".",
   178  			mode:          config.RunModes.Dev,
   179  			artifact:      &latest.DockerArtifact{},
   180  			shouldErr:     true,
   181  			expectedError: "unable to stream build output",
   182  		},
   183  		{
   184  			description: "bad build arg template",
   185  			artifact: &latest.DockerArtifact{
   186  				BuildArgs: map[string]*string{
   187  					"key": util.StringPtr("{{INVALID"),
   188  				},
   189  			},
   190  			mode:          config.RunModes.Dev,
   191  			shouldErr:     true,
   192  			expectedError: `function "INVALID" not defined`,
   193  		},
   194  	}
   195  	for _, test := range tests {
   196  		testutil.Run(t, test.description, func(t *testutil.T) {
   197  			t.Override(&DefaultAuthHelper, testAuthHelper{})
   198  			t.Override(&EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
   199  				return util.EvaluateEnvTemplateMap(args)
   200  			})
   201  			t.SetEnvs(test.env)
   202  
   203  			localDocker := NewLocalDaemon(test.api, nil, false, nil)
   204  			opts := BuildOptions{Tag: "finalimage", Mode: test.mode}
   205  			_, err := localDocker.Build(context.Background(), ioutil.Discard, test.workspace, "final-image", test.artifact, opts)
   206  
   207  			if test.shouldErr {
   208  				t.CheckErrorContains(test.expectedError, err)
   209  			} else {
   210  				t.CheckNoError(err)
   211  				t.CheckDeepEqual(test.api.Built[0], test.expected)
   212  			}
   213  		})
   214  	}
   215  }
   216  
   217  func TestImageID(t *testing.T) {
   218  	tests := []struct {
   219  		description string
   220  		ref         string
   221  		api         *testutil.FakeAPIClient
   222  		expected    string
   223  		shouldErr   bool
   224  	}{
   225  		{
   226  			description: "find by tag",
   227  			ref:         "identifier:latest",
   228  			api:         (&testutil.FakeAPIClient{}).Add("identifier:latest", "sha256:123abc"),
   229  			expected:    "sha256:123abc",
   230  		},
   231  		{
   232  			description: "find by imageID",
   233  			ref:         "sha256:123abc",
   234  			api:         (&testutil.FakeAPIClient{}).Add("identifier:latest", "sha256:123abc"),
   235  			expected:    "sha256:123abc",
   236  		},
   237  		{
   238  			description: "image inspect error",
   239  			ref:         "test",
   240  			api: &testutil.FakeAPIClient{
   241  				ErrImageInspect: true,
   242  			},
   243  			shouldErr: true,
   244  		},
   245  		{
   246  			description: "not found",
   247  			ref:         "somethingelse",
   248  			api:         &testutil.FakeAPIClient{},
   249  			expected:    "",
   250  		},
   251  	}
   252  	for _, test := range tests {
   253  		testutil.Run(t, test.description, func(t *testutil.T) {
   254  			localDocker := NewLocalDaemon(test.api, nil, false, nil)
   255  
   256  			imageID, err := localDocker.ImageID(context.Background(), test.ref)
   257  
   258  			t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, imageID)
   259  			if test.shouldErr {
   260  				if e, ok := err.(sErrors.Error); ok {
   261  					t.CheckDeepEqual(e.StatusCode(), proto.StatusCode_BUILD_DOCKER_GET_DIGEST_ERR)
   262  				} else {
   263  					t.Error("expected to be of type actionable err not found")
   264  				}
   265  			}
   266  		})
   267  	}
   268  }
   269  
   270  func TestGetBuildArgs(t *testing.T) {
   271  	tests := []struct {
   272  		description string
   273  		artifact    *latest.DockerArtifact
   274  		env         []string
   275  		want        []string
   276  		shouldErr   bool
   277  	}{
   278  		{
   279  			description: "build args",
   280  			artifact: &latest.DockerArtifact{
   281  				BuildArgs: map[string]*string{
   282  					"key1": util.StringPtr("value1"),
   283  					"key2": nil,
   284  					"key3": util.StringPtr("{{.FOO}}"),
   285  				},
   286  			},
   287  			env:  []string{"FOO=bar"},
   288  			want: []string{"--build-arg", "key1=value1", "--build-arg", "key2", "--build-arg", "key3=bar"},
   289  		},
   290  		{
   291  			description: "invalid build arg",
   292  			artifact: &latest.DockerArtifact{
   293  				BuildArgs: map[string]*string{
   294  					"key": util.StringPtr("{{INVALID"),
   295  				},
   296  			},
   297  			shouldErr: true,
   298  		},
   299  		{
   300  			description: "add host",
   301  			artifact: &latest.DockerArtifact{
   302  				AddHost: []string{"1.gcr.io:127.0.0.1", "2.gcr.io:127.0.0.1"},
   303  			},
   304  			want: []string{"--add-host", "1.gcr.io:127.0.0.1", "--add-host", "2.gcr.io:127.0.0.1"},
   305  		},
   306  		{
   307  			description: "cache from",
   308  			artifact: &latest.DockerArtifact{
   309  				CacheFrom: []string{"gcr.io/foo/bar", "baz:latest"},
   310  			},
   311  			want: []string{"--cache-from", "gcr.io/foo/bar", "--cache-from", "baz:latest"},
   312  		},
   313  		{
   314  			description: "additional CLI flags",
   315  			artifact: &latest.DockerArtifact{
   316  				CliFlags: []string{"--foo", "--bar"},
   317  			},
   318  			want: []string{"--foo", "--bar"},
   319  		},
   320  		{
   321  			description: "target",
   322  			artifact: &latest.DockerArtifact{
   323  				Target: "stage1",
   324  			},
   325  			want: []string{"--target", "stage1"},
   326  		},
   327  		{
   328  			description: "network mode",
   329  			artifact: &latest.DockerArtifact{
   330  				NetworkMode: "Bridge",
   331  			},
   332  			want: []string{"--network", "bridge"},
   333  		},
   334  		{
   335  			description: "no-cache",
   336  			artifact: &latest.DockerArtifact{
   337  				NoCache: true,
   338  			},
   339  			want: []string{"--no-cache"},
   340  		},
   341  		{
   342  			description: "pullParent",
   343  			artifact: &latest.DockerArtifact{
   344  				PullParent: true,
   345  			},
   346  			want: []string{"--pull"},
   347  		},
   348  		{
   349  			description: "squash",
   350  			artifact: &latest.DockerArtifact{
   351  				Squash: true,
   352  			},
   353  			want: []string{"--squash"},
   354  		},
   355  		{
   356  			description: "secret with no source",
   357  			artifact: &latest.DockerArtifact{
   358  				Secrets: []*latest.DockerSecret{
   359  					{ID: "mysecret"},
   360  				},
   361  			},
   362  			want: []string{"--secret", "id=mysecret"},
   363  		},
   364  		{
   365  			description: "secret with file source",
   366  			artifact: &latest.DockerArtifact{
   367  				Secrets: []*latest.DockerSecret{
   368  					{ID: "mysecret", Source: "foo.src"},
   369  				},
   370  			},
   371  			want: []string{"--secret", "id=mysecret,src=foo.src"},
   372  		},
   373  		{
   374  			description: "secret with env source",
   375  			artifact: &latest.DockerArtifact{
   376  				Secrets: []*latest.DockerSecret{
   377  					{ID: "mysecret", Env: "FOO"},
   378  				},
   379  			},
   380  			want: []string{"--secret", "id=mysecret,env=FOO"},
   381  		},
   382  		{
   383  			description: "multiple secrets",
   384  			artifact: &latest.DockerArtifact{
   385  				Secrets: []*latest.DockerSecret{
   386  					{ID: "mysecret", Source: "foo.src"},
   387  					{ID: "anothersecret", Source: "bar.src"},
   388  				},
   389  			},
   390  			want: []string{"--secret", "id=mysecret,src=foo.src", "--secret", "id=anothersecret,src=bar.src"},
   391  		},
   392  		{
   393  			description: "ssh with no source",
   394  			artifact: &latest.DockerArtifact{
   395  				SSH: "default",
   396  			},
   397  			want: []string{"--ssh", "default"},
   398  		},
   399  		{
   400  			description: "all",
   401  			artifact: &latest.DockerArtifact{
   402  				BuildArgs: map[string]*string{
   403  					"key1": util.StringPtr("value1"),
   404  				},
   405  				CacheFrom:   []string{"foo"},
   406  				Target:      "stage1",
   407  				NetworkMode: "None",
   408  				CliFlags:    []string{"--foo", "--bar"},
   409  				PullParent:  true,
   410  			},
   411  			want: []string{"--build-arg", "key1=value1", "--cache-from", "foo", "--foo", "--bar", "--target", "stage1", "--network", "none", "--pull"},
   412  		},
   413  	}
   414  	for _, test := range tests {
   415  		testutil.Run(t, test.description, func(t *testutil.T) {
   416  			t.Override(&util.OSEnviron, func() []string { return test.env })
   417  			args, err := util.EvaluateEnvTemplateMap(test.artifact.BuildArgs)
   418  			t.CheckError(test.shouldErr, err)
   419  			if test.shouldErr {
   420  				return
   421  			}
   422  
   423  			result, err := ToCLIBuildArgs(test.artifact, args)
   424  
   425  			t.CheckError(test.shouldErr, err)
   426  			if !test.shouldErr {
   427  				t.CheckDeepEqual(test.want, result)
   428  			}
   429  		})
   430  	}
   431  }
   432  
   433  func TestImageExists(t *testing.T) {
   434  	tests := []struct {
   435  		description string
   436  		api         *testutil.FakeAPIClient
   437  		image       string
   438  		expected    bool
   439  	}{
   440  		{
   441  			description: "image exists",
   442  			image:       "image:tag",
   443  			api:         (&testutil.FakeAPIClient{}).Add("image:tag", "imageID"),
   444  			expected:    true,
   445  		}, {
   446  			description: "image does not exist",
   447  			image:       "dne",
   448  			api: (&testutil.FakeAPIClient{
   449  				ErrImageInspect: true,
   450  			}).Add("image:tag", "imageID"),
   451  		}, {
   452  			description: "error getting image",
   453  			api: (&testutil.FakeAPIClient{
   454  				ErrImageInspect: true,
   455  			}).Add("image:tag", "imageID"),
   456  		},
   457  	}
   458  	for _, test := range tests {
   459  		testutil.Run(t, test.description, func(t *testutil.T) {
   460  			localDocker := NewLocalDaemon(test.api, nil, false, nil)
   461  
   462  			actual := localDocker.ImageExists(context.Background(), test.image)
   463  
   464  			t.CheckDeepEqual(test.expected, actual)
   465  		})
   466  	}
   467  }
   468  
   469  func TestConfigFile(t *testing.T) {
   470  	api := (&testutil.FakeAPIClient{}).Add("gcr.io/image", "sha256:imageIDabcab")
   471  
   472  	localDocker := NewLocalDaemon(api, nil, false, nil)
   473  	cfg, err := localDocker.ConfigFile(context.Background(), "gcr.io/image")
   474  
   475  	testutil.CheckErrorAndDeepEqual(t, false, err, "sha256:imageIDabcab", cfg.Config.Image)
   476  }
   477  
   478  type APICallsCounter struct {
   479  	client.CommonAPIClient
   480  	calls int32
   481  }
   482  
   483  func (c *APICallsCounter) ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error) {
   484  	atomic.AddInt32(&c.calls, 1)
   485  	return c.CommonAPIClient.ImageInspectWithRaw(ctx, image)
   486  }
   487  
   488  func TestConfigFileConcurrentCalls(t *testing.T) {
   489  	api := &APICallsCounter{
   490  		CommonAPIClient: (&testutil.FakeAPIClient{}).Add("gcr.io/image", "sha256:imageIDabcab"),
   491  	}
   492  
   493  	localDocker := NewLocalDaemon(api, nil, false, nil)
   494  
   495  	var wg sync.WaitGroup
   496  	for i := 0; i < 100; i++ {
   497  		wg.Add(1)
   498  		go func() {
   499  			localDocker.ConfigFile(context.Background(), "gcr.io/image")
   500  			wg.Done()
   501  		}()
   502  	}
   503  	wg.Wait()
   504  
   505  	// Check that the APIClient was called only once
   506  	testutil.CheckDeepEqual(t, int32(1), atomic.LoadInt32(&api.calls))
   507  }
   508  
   509  func TestTagWithImageID(t *testing.T) {
   510  	tests := []struct {
   511  		description string
   512  		imageName   string
   513  		imageID     string
   514  		expected    string
   515  		shouldErr   bool
   516  	}{
   517  		{
   518  			description: "success",
   519  			imageName:   "ref",
   520  			imageID:     "sha256:imageID",
   521  			expected:    "ref:imageID",
   522  		},
   523  		{
   524  			description: "ignore tag",
   525  			imageName:   "ref:tag",
   526  			imageID:     "sha256:imageID",
   527  			expected:    "ref:imageID",
   528  		},
   529  		{
   530  			description: "not found",
   531  			imageName:   "ref",
   532  			imageID:     "sha256:unknownImageID",
   533  			shouldErr:   true,
   534  		},
   535  		{
   536  			description: "invalid",
   537  			imageName:   "!!invalid!!",
   538  			shouldErr:   true,
   539  		},
   540  		{
   541  			description: "empty image id",
   542  			imageName:   "ref",
   543  		},
   544  	}
   545  	for _, test := range tests {
   546  		testutil.Run(t, test.description, func(t *testutil.T) {
   547  			api := (&testutil.FakeAPIClient{}).Add("sha256:imageID", "sha256:imageID")
   548  
   549  			localDocker := NewLocalDaemon(api, nil, false, nil)
   550  			tag, err := localDocker.TagWithImageID(context.Background(), test.imageName, test.imageID)
   551  
   552  			t.CheckError(test.shouldErr, err)
   553  			t.CheckDeepEqual(test.expected, tag)
   554  		})
   555  	}
   556  }