github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/buildpacks/build_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 buildpacks
    18  
    19  import (
    20  	"context"
    21  	"io"
    22  	"io/ioutil"
    23  	"testing"
    24  
    25  	pack "github.com/buildpacks/pack/pkg/client"
    26  	packcfg "github.com/buildpacks/pack/pkg/image"
    27  	"github.com/docker/docker/api/types"
    28  	"github.com/google/go-cmp/cmp"
    29  
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    35  	"github.com/GoogleContainerTools/skaffold/testutil"
    36  )
    37  
    38  type fakePack struct {
    39  	Opts pack.BuildOptions
    40  }
    41  
    42  func (f *fakePack) runPack(_ context.Context, _ io.Writer, _ docker.LocalDaemon, opts pack.BuildOptions) error {
    43  	f.Opts = opts
    44  	return nil
    45  }
    46  
    47  func TestBuild(t *testing.T) {
    48  	tests := []struct {
    49  		description     string
    50  		artifact        *latest.Artifact
    51  		tag             string
    52  		api             *testutil.FakeAPIClient
    53  		files           map[string]string
    54  		pushImages      bool
    55  		shouldErr       bool
    56  		mode            config.RunMode
    57  		resolver        ArtifactResolver
    58  		expectedOptions *pack.BuildOptions
    59  	}{
    60  		{
    61  			description: "success for debug",
    62  			artifact:    buildpacksArtifact("my/builder", "my/run"),
    63  			tag:         "img:tag",
    64  			mode:        config.RunModes.Debug,
    65  			api:         &testutil.FakeAPIClient{},
    66  			resolver:    mockArtifactResolver{},
    67  			expectedOptions: &pack.BuildOptions{
    68  				AppPath:  ".",
    69  				Builder:  "my/builder",
    70  				RunImage: "my/run",
    71  				Env:      debugModeArgs,
    72  				Image:    "img:latest",
    73  			},
    74  		},
    75  		{
    76  			description: "success for build",
    77  			artifact:    buildpacksArtifact("my/builder", "my/run"),
    78  			tag:         "img:tag",
    79  			mode:        config.RunModes.Build,
    80  			api:         &testutil.FakeAPIClient{},
    81  			resolver:    mockArtifactResolver{},
    82  			expectedOptions: &pack.BuildOptions{
    83  				AppPath:    ".",
    84  				Builder:    "my/builder",
    85  				RunImage:   "my/run",
    86  				PullPolicy: packcfg.PullNever,
    87  				Env:        nonDebugModeArgs,
    88  				Image:      "img:latest",
    89  			},
    90  		},
    91  
    92  		{
    93  			description: "success with buildpacks for debug",
    94  			artifact:    withTrustedBuilder(withBuildpacks([]string{"my/buildpack", "my/otherBuildpack"}, buildpacksArtifact("my/otherBuilder", "my/otherRun"))),
    95  			tag:         "img:tag",
    96  			api:         &testutil.FakeAPIClient{},
    97  			resolver:    mockArtifactResolver{},
    98  			mode:        config.RunModes.Debug,
    99  			expectedOptions: &pack.BuildOptions{
   100  				AppPath:    ".",
   101  				Builder:    "my/otherBuilder",
   102  				RunImage:   "my/otherRun",
   103  				Buildpacks: []string{"my/buildpack", "my/otherBuildpack"},
   104  				Env:        debugModeArgs,
   105  				Image:      "img:latest",
   106  			},
   107  		},
   108  		{
   109  			description: "success with buildpacks for build",
   110  			artifact:    withTrustedBuilder(withBuildpacks([]string{"my/buildpack", "my/otherBuildpack"}, buildpacksArtifact("my/otherBuilder", "my/otherRun"))),
   111  			tag:         "img:tag",
   112  			api:         &testutil.FakeAPIClient{},
   113  			resolver:    mockArtifactResolver{},
   114  			mode:        config.RunModes.Build,
   115  			expectedOptions: &pack.BuildOptions{
   116  				AppPath:    ".",
   117  				Builder:    "my/otherBuilder",
   118  				RunImage:   "my/otherRun",
   119  				Buildpacks: []string{"my/buildpack", "my/otherBuildpack"},
   120  				PullPolicy: packcfg.PullNever,
   121  				Env:        nonDebugModeArgs,
   122  				Image:      "img:latest",
   123  			},
   124  		},
   125  		{
   126  			description: "project.toml",
   127  			artifact:    buildpacksArtifact("my/builder2", "my/run2"),
   128  			tag:         "img:tag",
   129  			api:         &testutil.FakeAPIClient{},
   130  			resolver:    mockArtifactResolver{},
   131  			mode:        config.RunModes.Build,
   132  			files: map[string]string{
   133  				"project.toml": `[[build.env]]
   134  name = "GOOGLE_RUNTIME_VERSION"
   135  value = "14.3.0"
   136  [[build.buildpacks]]
   137  id = "my/buildpack"
   138  [[build.buildpacks]]
   139  id = "my/otherBuildpack"
   140  version = "1.0"
   141  `,
   142  			},
   143  			expectedOptions: &pack.BuildOptions{
   144  				AppPath:    ".",
   145  				Builder:    "my/builder2",
   146  				RunImage:   "my/run2",
   147  				Buildpacks: []string{"my/buildpack", "my/otherBuildpack@1.0"},
   148  				Env: addDefaultArgs(config.RunModes.Build, map[string]string{
   149  					"GOOGLE_RUNTIME_VERSION": "14.3.0",
   150  				}),
   151  				Image: "img:latest",
   152  			},
   153  		},
   154  		{
   155  			description: "Buildpacks in skaffold.yaml override those in project.toml",
   156  			artifact:    withBuildpacks([]string{"my/buildpack", "my/otherBuildpack"}, buildpacksArtifact("my/builder3", "my/run3")),
   157  			tag:         "img:tag",
   158  			api:         &testutil.FakeAPIClient{},
   159  			resolver:    mockArtifactResolver{},
   160  			files: map[string]string{
   161  				"project.toml": `[[build.buildpacks]]
   162  id = "my/ignored"
   163  `,
   164  			},
   165  			expectedOptions: &pack.BuildOptions{
   166  				AppPath:    ".",
   167  				Builder:    "my/builder3",
   168  				RunImage:   "my/run3",
   169  				Buildpacks: []string{"my/buildpack", "my/otherBuildpack"},
   170  				Env:        nonDebugModeArgs,
   171  				Image:      "img:latest",
   172  			},
   173  		},
   174  		{
   175  			description: "Combine env from skaffold.yaml and project.toml",
   176  			artifact:    withEnv([]string{"KEY1=VALUE1"}, buildpacksArtifact("my/builder4", "my/run4")),
   177  			tag:         "img:tag",
   178  			mode:        config.RunModes.Build,
   179  			api:         &testutil.FakeAPIClient{},
   180  			resolver:    mockArtifactResolver{},
   181  			files: map[string]string{
   182  				"project.toml": `[[build.env]]
   183  name = "KEY2"
   184  value = "VALUE2"
   185  `,
   186  			},
   187  			expectedOptions: &pack.BuildOptions{
   188  				AppPath:  ".",
   189  				Builder:  "my/builder4",
   190  				RunImage: "my/run4",
   191  				Env: addDefaultArgs(config.RunModes.Build, map[string]string{
   192  					"KEY1": "VALUE1",
   193  					"KEY2": "VALUE2",
   194  				}),
   195  				Image: "img:latest",
   196  			},
   197  		},
   198  		{
   199  			description: "dev mode",
   200  			artifact:    withSync(&latest.Sync{Auto: util.BoolPtr(true)}, buildpacksArtifact("another/builder", "another/run")),
   201  			tag:         "img:tag",
   202  			api:         &testutil.FakeAPIClient{},
   203  			resolver:    mockArtifactResolver{},
   204  			mode:        config.RunModes.Dev,
   205  			expectedOptions: &pack.BuildOptions{
   206  				AppPath:  ".",
   207  				Builder:  "another/builder",
   208  				RunImage: "another/run",
   209  				Env: addDefaultArgs(config.RunModes.Build, map[string]string{
   210  					"GOOGLE_DEVMODE": "1",
   211  				}),
   212  				Image: "img:latest",
   213  			},
   214  		},
   215  		{
   216  			description: "dev mode but no sync",
   217  			artifact:    buildpacksArtifact("my/other-builder", "my/run"),
   218  			tag:         "img:tag",
   219  			api:         &testutil.FakeAPIClient{},
   220  			resolver:    mockArtifactResolver{},
   221  			mode:        config.RunModes.Dev,
   222  			expectedOptions: &pack.BuildOptions{
   223  				AppPath:  ".",
   224  				Builder:  "my/other-builder",
   225  				RunImage: "my/run",
   226  				Env:      nonDebugModeArgs,
   227  				Image:    "img:latest",
   228  			},
   229  		},
   230  		{
   231  			description: "invalid ref",
   232  			artifact:    buildpacksArtifact("my/builder", "my/run"),
   233  			tag:         "in valid ref",
   234  			api:         &testutil.FakeAPIClient{},
   235  			resolver:    mockArtifactResolver{},
   236  			shouldErr:   true,
   237  		},
   238  		{
   239  			description: "push error",
   240  			artifact:    buildpacksArtifact("my/builder", "my/run"),
   241  			tag:         "img:tag",
   242  			pushImages:  true,
   243  			api: &testutil.FakeAPIClient{
   244  				ErrImagePush: true,
   245  			},
   246  			resolver:  mockArtifactResolver{},
   247  			shouldErr: true,
   248  		},
   249  		{
   250  			description: "invalid env",
   251  			artifact:    withEnv([]string{"INVALID"}, buildpacksArtifact("my/builder", "my/run")),
   252  			tag:         "img:tag",
   253  			api:         &testutil.FakeAPIClient{},
   254  			resolver:    mockArtifactResolver{},
   255  			shouldErr:   true,
   256  		},
   257  		{
   258  			description: "invalid project.toml",
   259  			artifact:    buildpacksArtifact("my/builder2", "my/run2"),
   260  			tag:         "img:tag",
   261  			api:         &testutil.FakeAPIClient{},
   262  			resolver:    mockArtifactResolver{},
   263  			files: map[string]string{
   264  				"project.toml": `INVALID`,
   265  			},
   266  			shouldErr: true,
   267  		},
   268  	}
   269  	for _, test := range tests {
   270  		testutil.Run(t, test.description, func(t *testutil.T) {
   271  			t.NewTempDir().Touch("file").WriteFiles(test.files).Chdir()
   272  			pack := &fakePack{}
   273  			t.Override(&runPackBuildFunc, pack.runPack)
   274  
   275  			test.api.
   276  				Add(test.artifact.BuildpackArtifact.Builder, "builderImageID").
   277  				Add(test.artifact.BuildpackArtifact.RunImage, "runImageID").
   278  				Add("img:latest", "builtImageID")
   279  			localDocker := fakeLocalDaemon(test.api)
   280  
   281  			builder := NewArtifactBuilder(localDocker, test.pushImages, test.mode, test.resolver)
   282  			_, err := builder.Build(context.Background(), ioutil.Discard, test.artifact, test.tag, platform.Matcher{})
   283  
   284  			t.CheckError(test.shouldErr, err)
   285  			if test.expectedOptions != nil {
   286  				t.CheckDeepEqual(*test.expectedOptions, pack.Opts, ignoreField("ProjectDescriptor.SchemaVersion"), ignoreField("TrustBuilder"))
   287  			}
   288  		})
   289  	}
   290  }
   291  
   292  func TestBuildWithArtifactDependencies(t *testing.T) {
   293  	tests := []struct {
   294  		description     string
   295  		artifact        *latest.Artifact
   296  		tag             string
   297  		api             *testutil.FakeAPIClient
   298  		files           map[string]string
   299  		pushImages      bool
   300  		shouldErr       bool
   301  		mode            config.RunMode
   302  		resolver        ArtifactResolver
   303  		expectedOptions *pack.BuildOptions
   304  	}{
   305  		{
   306  			description: "custom builder image only with no push",
   307  			artifact:    withRequiredArtifacts([]*latest.ArtifactDependency{{ImageName: "builder-image", Alias: "BUILDER_IMAGE"}}, buildpacksArtifact("BUILDER_IMAGE", "my/run")),
   308  			tag:         "img:tag",
   309  			pushImages:  false,
   310  			mode:        config.RunModes.Build,
   311  			api:         &testutil.FakeAPIClient{},
   312  			resolver:    mockArtifactResolver{m: map[string]string{"builder-image": "my/custom-builder"}},
   313  			expectedOptions: &pack.BuildOptions{
   314  				AppPath:    ".",
   315  				Builder:    "my/custom-builder",
   316  				RunImage:   "my/run",
   317  				PullPolicy: packcfg.PullIfNotPresent,
   318  				Env:        nonDebugModeArgs,
   319  				Image:      "img:latest",
   320  			},
   321  		},
   322  		{
   323  			description: "custom run image only with no push",
   324  			artifact:    withRequiredArtifacts([]*latest.ArtifactDependency{{ImageName: "run-image", Alias: "RUN_IMAGE"}}, buildpacksArtifact("my/builder", "RUN_IMAGE")),
   325  			tag:         "img:tag",
   326  			pushImages:  false,
   327  			mode:        config.RunModes.Build,
   328  			api:         &testutil.FakeAPIClient{},
   329  			resolver:    mockArtifactResolver{m: map[string]string{"run-image": "my/custom-run"}},
   330  			expectedOptions: &pack.BuildOptions{
   331  				AppPath:    ".",
   332  				Builder:    "my/builder",
   333  				RunImage:   "my/custom-run",
   334  				PullPolicy: packcfg.PullIfNotPresent,
   335  				Env:        nonDebugModeArgs,
   336  				Image:      "img:latest",
   337  			},
   338  		},
   339  		{
   340  			description: "custom builder image only with push",
   341  			artifact:    withRequiredArtifacts([]*latest.ArtifactDependency{{ImageName: "builder-image", Alias: "BUILDER_IMAGE"}}, buildpacksArtifact("BUILDER_IMAGE", "my/run")),
   342  			tag:         "img:tag",
   343  			pushImages:  true,
   344  			mode:        config.RunModes.Build,
   345  			api:         &testutil.FakeAPIClient{},
   346  			resolver:    mockArtifactResolver{m: map[string]string{"builder-image": "my/custom-builder"}},
   347  			expectedOptions: &pack.BuildOptions{
   348  				AppPath:    ".",
   349  				Builder:    "my/custom-builder",
   350  				RunImage:   "my/run",
   351  				PullPolicy: packcfg.PullIfNotPresent,
   352  				Env:        nonDebugModeArgs,
   353  				Image:      "img:latest",
   354  			},
   355  		},
   356  		{
   357  			description: "custom run image only with push",
   358  			artifact:    withRequiredArtifacts([]*latest.ArtifactDependency{{ImageName: "run-image", Alias: "RUN_IMAGE"}}, buildpacksArtifact("my/builder", "RUN_IMAGE")),
   359  			tag:         "img:tag",
   360  			pushImages:  true,
   361  			mode:        config.RunModes.Build,
   362  			api:         &testutil.FakeAPIClient{},
   363  			resolver:    mockArtifactResolver{m: map[string]string{"run-image": "my/custom-run"}},
   364  			expectedOptions: &pack.BuildOptions{
   365  				AppPath:    ".",
   366  				Builder:    "my/builder",
   367  				RunImage:   "my/custom-run",
   368  				PullPolicy: packcfg.PullIfNotPresent,
   369  				Env:        nonDebugModeArgs,
   370  				Image:      "img:latest",
   371  			},
   372  		},
   373  		{
   374  			description: "custom run image and custom builder image with push",
   375  			artifact:    withRequiredArtifacts([]*latest.ArtifactDependency{{ImageName: "run-image", Alias: "RUN_IMAGE"}, {ImageName: "builder-image", Alias: "BUILDER_IMAGE"}}, buildpacksArtifact("BUILDER_IMAGE", "RUN_IMAGE")),
   376  			tag:         "img:tag",
   377  			pushImages:  true,
   378  			mode:        config.RunModes.Build,
   379  			api:         &testutil.FakeAPIClient{},
   380  			resolver:    mockArtifactResolver{m: map[string]string{"builder-image": "my/custom-builder", "run-image": "my/custom-run"}},
   381  			expectedOptions: &pack.BuildOptions{
   382  				AppPath:    ".",
   383  				Builder:    "my/custom-builder",
   384  				RunImage:   "my/custom-run",
   385  				PullPolicy: packcfg.PullNever,
   386  				Env:        nonDebugModeArgs,
   387  				Image:      "img:latest",
   388  			},
   389  		},
   390  		{
   391  			description: "custom run image and custom builder image with no push",
   392  			artifact:    withRequiredArtifacts([]*latest.ArtifactDependency{{ImageName: "run-image", Alias: "RUN_IMAGE"}, {ImageName: "builder-image", Alias: "BUILDER_IMAGE"}}, buildpacksArtifact("BUILDER_IMAGE", "RUN_IMAGE")),
   393  			tag:         "img:tag",
   394  			pushImages:  false,
   395  			mode:        config.RunModes.Build,
   396  			api:         &testutil.FakeAPIClient{},
   397  			resolver:    mockArtifactResolver{m: map[string]string{"builder-image": "my/custom-builder", "run-image": "my/custom-run"}},
   398  			expectedOptions: &pack.BuildOptions{
   399  				AppPath:    ".",
   400  				Builder:    "my/custom-builder",
   401  				RunImage:   "my/custom-run",
   402  				PullPolicy: packcfg.PullNever,
   403  				Env:        nonDebugModeArgs,
   404  				Image:      "img:latest",
   405  			},
   406  		},
   407  	}
   408  	for _, test := range tests {
   409  		testutil.Run(t, test.description, func(t *testutil.T) {
   410  			t.NewTempDir().Touch("file").WriteFiles(test.files).Chdir()
   411  			pack := &fakePack{}
   412  			t.Override(&runPackBuildFunc, pack.runPack)
   413  			images.images = map[imageTuple]bool{}
   414  			test.api.
   415  				Add(test.artifact.BuildpackArtifact.Builder, "builderImageID").
   416  				Add(test.artifact.BuildpackArtifact.RunImage, "runImageID").
   417  				Add("img:latest", "builtImageID")
   418  			t.Override(&docker.DefaultAuthHelper, testAuthHelper{})
   419  
   420  			localDocker := fakeLocalDaemon(test.api)
   421  
   422  			builder := NewArtifactBuilder(localDocker, test.pushImages, test.mode, test.resolver)
   423  			_, err := builder.Build(context.Background(), ioutil.Discard, test.artifact, test.tag, platform.Matcher{})
   424  
   425  			t.CheckError(test.shouldErr, err)
   426  			if test.expectedOptions != nil {
   427  				t.CheckDeepEqual(*test.expectedOptions, pack.Opts, ignoreField("ProjectDescriptor.SchemaVersion"), ignoreField("TrustBuilder"))
   428  			}
   429  		})
   430  	}
   431  }
   432  func buildpacksArtifact(builder, runImage string) *latest.Artifact {
   433  	return &latest.Artifact{
   434  		Workspace: ".",
   435  		ArtifactType: latest.ArtifactType{
   436  			BuildpackArtifact: &latest.BuildpackArtifact{
   437  				Builder:           builder,
   438  				RunImage:          runImage,
   439  				ProjectDescriptor: "project.toml",
   440  				Dependencies: &latest.BuildpackDependencies{
   441  					Paths: []string{"."},
   442  				},
   443  			},
   444  		},
   445  	}
   446  }
   447  
   448  func withEnv(env []string, artifact *latest.Artifact) *latest.Artifact {
   449  	artifact.BuildpackArtifact.Env = env
   450  	return artifact
   451  }
   452  
   453  func withSync(sync *latest.Sync, artifact *latest.Artifact) *latest.Artifact {
   454  	artifact.Sync = sync
   455  	return artifact
   456  }
   457  
   458  func withTrustedBuilder(artifact *latest.Artifact) *latest.Artifact {
   459  	artifact.BuildpackArtifact.TrustBuilder = true
   460  	return artifact
   461  }
   462  
   463  func withRequiredArtifacts(deps []*latest.ArtifactDependency, artifact *latest.Artifact) *latest.Artifact {
   464  	artifact.Dependencies = deps
   465  	return artifact
   466  }
   467  
   468  func withBuildpacks(buildpacks []string, artifact *latest.Artifact) *latest.Artifact {
   469  	artifact.BuildpackArtifact.Buildpacks = buildpacks
   470  	return artifact
   471  }
   472  
   473  type mockArtifactResolver struct {
   474  	m map[string]string
   475  }
   476  
   477  func (r mockArtifactResolver) GetImageTag(imageName string) (string, bool) {
   478  	if r.m == nil {
   479  		return "", false
   480  	}
   481  	val, found := r.m[imageName]
   482  	return val, found
   483  }
   484  
   485  type testAuthHelper struct{}
   486  
   487  func (t testAuthHelper) GetAuthConfig(string) (types.AuthConfig, error) {
   488  	return types.AuthConfig{}, nil
   489  }
   490  func (t testAuthHelper) GetAllAuthConfigs(context.Context) (map[string]types.AuthConfig, error) {
   491  	return nil, nil
   492  }
   493  
   494  func ignoreField(path string) cmp.Option {
   495  	return cmp.FilterPath(func(p cmp.Path) bool {
   496  		return p.String() == path
   497  	}, cmp.Ignore())
   498  }