github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/runner/v1/runner_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 v1
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"testing"
    25  
    26  	"github.com/blang/semver"
    27  	"k8s.io/client-go/tools/clientcmd/api"
    28  
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/access"
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/cluster"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy"
    35  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/helm"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kubectl"
    37  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kustomize"
    38  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon"
    39  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
    40  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/log"
    41  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
    42  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner"
    43  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext"
    44  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/defaults"
    45  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    46  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/status"
    47  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync"
    48  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/tag"
    49  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/test"
    50  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    51  	"github.com/GoogleContainerTools/skaffold/testutil"
    52  )
    53  
    54  type Actions struct {
    55  	Built    []string
    56  	Synced   []string
    57  	Tested   []string
    58  	Deployed []string
    59  }
    60  
    61  type TestBench struct {
    62  	buildErrors   []error
    63  	syncErrors    []error
    64  	testErrors    []error
    65  	deployErrors  []error
    66  	namespaces    []string
    67  	userIntents   []func(*runner.Intents)
    68  	intents       *runner.Intents
    69  	intentTrigger bool
    70  
    71  	devLoop        func(context.Context, io.Writer, func() error) error
    72  	firstMonitor   func(bool) error
    73  	cycles         int
    74  	currentCycle   int
    75  	currentActions Actions
    76  	actions        []Actions
    77  	tag            int
    78  }
    79  
    80  func NewTestBench() *TestBench {
    81  	return &TestBench{}
    82  }
    83  
    84  func (t *TestBench) WithBuildErrors(buildErrors []error) *TestBench {
    85  	t.buildErrors = buildErrors
    86  	return t
    87  }
    88  
    89  func (t *TestBench) WithSyncErrors(syncErrors []error) *TestBench {
    90  	t.syncErrors = syncErrors
    91  	return t
    92  }
    93  
    94  func (t *TestBench) WithDeployErrors(deployErrors []error) *TestBench {
    95  	t.deployErrors = deployErrors
    96  	return t
    97  }
    98  
    99  func (t *TestBench) WithDeployNamespaces(ns []string) *TestBench {
   100  	t.namespaces = ns
   101  	return t
   102  }
   103  
   104  func (t *TestBench) WithTestErrors(testErrors []error) *TestBench {
   105  	t.testErrors = testErrors
   106  	return t
   107  }
   108  
   109  func (t *TestBench) GetAccessor() access.Accessor {
   110  	return &access.NoopAccessor{}
   111  }
   112  
   113  func (t *TestBench) GetDebugger() debug.Debugger {
   114  	return &debug.NoopDebugger{}
   115  }
   116  
   117  func (t *TestBench) GetLogger() log.Logger {
   118  	return &log.NoopLogger{}
   119  }
   120  
   121  func (t *TestBench) GetStatusMonitor() status.Monitor {
   122  	return &status.NoopMonitor{}
   123  }
   124  
   125  func (t *TestBench) GetSyncer() sync.Syncer {
   126  	return t
   127  }
   128  
   129  func (t *TestBench) RegisterLocalImages(_ []graph.Artifact) {}
   130  func (t *TestBench) TrackBuildArtifacts(_ []graph.Artifact) {}
   131  
   132  func (t *TestBench) TestDependencies(context.Context, *latest.Artifact) ([]string, error) {
   133  	return nil, nil
   134  }
   135  func (t *TestBench) Dependencies() ([]string, error)                               { return nil, nil }
   136  func (t *TestBench) Cleanup(ctx context.Context, out io.Writer, dryRun bool) error { return nil }
   137  func (t *TestBench) Prune(ctx context.Context, out io.Writer) error                { return nil }
   138  
   139  func (t *TestBench) enterNewCycle() {
   140  	t.actions = append(t.actions, t.currentActions)
   141  	t.currentActions = Actions{}
   142  }
   143  
   144  func (t *TestBench) Build(_ context.Context, _ io.Writer, _ tag.ImageTags, _ platform.Resolver, artifacts []*latest.Artifact) ([]graph.Artifact, error) {
   145  	if len(t.buildErrors) > 0 {
   146  		err := t.buildErrors[0]
   147  		t.buildErrors = t.buildErrors[1:]
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  	}
   152  
   153  	t.tag++
   154  
   155  	var builds []graph.Artifact
   156  	for _, artifact := range artifacts {
   157  		builds = append(builds, graph.Artifact{
   158  			ImageName: artifact.ImageName,
   159  			Tag:       fmt.Sprintf("%s:%d", artifact.ImageName, t.tag),
   160  		})
   161  	}
   162  
   163  	t.currentActions.Built = findTags(builds)
   164  	return builds, nil
   165  }
   166  
   167  func (t *TestBench) Sync(_ context.Context, _ io.Writer, item *sync.Item) error {
   168  	if len(t.syncErrors) > 0 {
   169  		err := t.syncErrors[0]
   170  		t.syncErrors = t.syncErrors[1:]
   171  		if err != nil {
   172  			return err
   173  		}
   174  	}
   175  
   176  	t.currentActions.Synced = []string{item.Image}
   177  	return nil
   178  }
   179  
   180  func (t *TestBench) Test(_ context.Context, _ io.Writer, artifacts []graph.Artifact) error {
   181  	if len(t.testErrors) > 0 {
   182  		err := t.testErrors[0]
   183  		t.testErrors = t.testErrors[1:]
   184  		if err != nil {
   185  			return err
   186  		}
   187  	}
   188  
   189  	t.currentActions.Tested = findTags(artifacts)
   190  	return nil
   191  }
   192  
   193  func (t *TestBench) Deploy(_ context.Context, _ io.Writer, artifacts []graph.Artifact) error {
   194  	if len(t.deployErrors) > 0 {
   195  		err := t.deployErrors[0]
   196  		t.deployErrors = t.deployErrors[1:]
   197  		if err != nil {
   198  			return err
   199  		}
   200  	}
   201  
   202  	t.currentActions.Deployed = findTags(artifacts)
   203  	return nil
   204  }
   205  
   206  func (t *TestBench) Render(context.Context, io.Writer, []graph.Artifact, bool, string) error {
   207  	return nil
   208  }
   209  
   210  func (t *TestBench) Actions() []Actions {
   211  	return append(t.actions, t.currentActions)
   212  }
   213  
   214  func (t *TestBench) WatchForChanges(ctx context.Context, out io.Writer, doDev func() error) error {
   215  	// don't actually call the monitor here, because extra actions would be added
   216  	if err := t.firstMonitor(true); err != nil {
   217  		return err
   218  	}
   219  
   220  	t.intentTrigger = true
   221  	for _, intent := range t.userIntents {
   222  		intent(t.intents)
   223  		if err := t.devLoop(ctx, out, doDev); err != nil {
   224  			return err
   225  		}
   226  	}
   227  
   228  	t.intentTrigger = false
   229  	for i := 0; i < t.cycles; i++ {
   230  		t.enterNewCycle()
   231  		t.currentCycle = i
   232  		if err := t.devLoop(ctx, out, doDev); err != nil {
   233  			return err
   234  		}
   235  	}
   236  	return nil
   237  }
   238  
   239  func (t *TestBench) LogWatchToUser(_ io.Writer) {}
   240  
   241  func findTags(artifacts []graph.Artifact) []string {
   242  	var tags []string
   243  	for _, artifact := range artifacts {
   244  		tags = append(tags, artifact.Tag)
   245  	}
   246  	return tags
   247  }
   248  
   249  func (r *SkaffoldRunner) WithMonitor(m filemon.Monitor) *SkaffoldRunner {
   250  	r.monitor = m
   251  	return r
   252  }
   253  
   254  type triggerState struct {
   255  	build  bool
   256  	sync   bool
   257  	deploy bool
   258  }
   259  
   260  func createRunner(t *testutil.T, testBench *TestBench, monitor filemon.Monitor, artifacts []*latest.Artifact, autoTriggers *triggerState) *SkaffoldRunner {
   261  	if autoTriggers == nil {
   262  		autoTriggers = &triggerState{true, true, true}
   263  	}
   264  	var tests []*latest.TestCase
   265  	for _, a := range artifacts {
   266  		tests = append(tests, &latest.TestCase{
   267  			ImageName: a.ImageName,
   268  		})
   269  	}
   270  	cfg := &latest.SkaffoldConfig{
   271  		Pipeline: latest.Pipeline{
   272  			Build: latest.BuildConfig{
   273  				TagPolicy: latest.TagPolicy{
   274  					// Use the fastest tagger
   275  					ShaTagger: &latest.ShaTagger{},
   276  				},
   277  				Artifacts: artifacts,
   278  			},
   279  			Test:   tests,
   280  			Deploy: latest.DeployConfig{StatusCheckDeadlineSeconds: 60},
   281  		},
   282  	}
   283  	defaults.Set(cfg)
   284  	defaults.SetDefaultDeployer(cfg)
   285  	runCtx := &runcontext.RunContext{
   286  		Pipelines: runcontext.NewPipelines([]latest.Pipeline{cfg.Pipeline}),
   287  		Opts: config.SkaffoldOptions{
   288  			Trigger:           "polling",
   289  			WatchPollInterval: 100,
   290  			AutoBuild:         autoTriggers.build,
   291  			AutoSync:          autoTriggers.sync,
   292  			AutoDeploy:        autoTriggers.deploy,
   293  		},
   294  	}
   295  	runner, err := NewForConfig(context.Background(), runCtx)
   296  	t.CheckNoError(err)
   297  
   298  	// TODO(yuwenma):builder.builder looks weird. Avoid the nested struct.
   299  	runner.Builder.Builder = testBench
   300  	runner.tester = testBench
   301  	runner.deployer = testBench
   302  	runner.listener = testBench
   303  	runner.monitor = monitor
   304  
   305  	testBench.devLoop = func(ctx context.Context, out io.Writer, doDev func() error) error {
   306  		if err := monitor.Run(true); err != nil {
   307  			return err
   308  		}
   309  		return doDev()
   310  	}
   311  
   312  	testBench.firstMonitor = func(bool) error {
   313  		// default to noop so we don't add extra actions
   314  		return nil
   315  	}
   316  
   317  	return runner
   318  }
   319  
   320  func TestNewForConfig(t *testing.T) {
   321  	tests := []struct {
   322  		description      string
   323  		pipeline         latest.Pipeline
   324  		shouldErr        bool
   325  		cacheArtifacts   bool
   326  		expectedBuilder  build.BuilderMux
   327  		expectedTester   test.Tester
   328  		expectedDeployer deploy.Deployer
   329  	}{
   330  		{
   331  			description: "local builder config",
   332  			pipeline: latest.Pipeline{
   333  				Build: latest.BuildConfig{
   334  					TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}},
   335  					BuildType: latest.BuildType{
   336  						LocalBuild: &latest.LocalBuild{},
   337  					},
   338  				},
   339  				Deploy: latest.DeployConfig{
   340  					DeployType: latest.DeployType{
   341  						KubectlDeploy: &latest.KubectlDeploy{},
   342  					},
   343  				},
   344  			},
   345  			expectedTester:   &test.FullTester{},
   346  			expectedDeployer: &kubectl.Deployer{},
   347  		},
   348  		{
   349  			description: "gcb config",
   350  			pipeline: latest.Pipeline{
   351  				Build: latest.BuildConfig{
   352  					TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}},
   353  					BuildType: latest.BuildType{
   354  						GoogleCloudBuild: &latest.GoogleCloudBuild{},
   355  					},
   356  				},
   357  				Deploy: latest.DeployConfig{
   358  					DeployType: latest.DeployType{
   359  						KubectlDeploy: &latest.KubectlDeploy{},
   360  					},
   361  				},
   362  			},
   363  			expectedTester:   &test.FullTester{},
   364  			expectedDeployer: &kubectl.Deployer{},
   365  		},
   366  		{
   367  			description: "cluster builder config",
   368  			pipeline: latest.Pipeline{
   369  				Build: latest.BuildConfig{
   370  					TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}},
   371  					BuildType: latest.BuildType{
   372  						Cluster: &latest.ClusterDetails{Timeout: "100s"},
   373  					},
   374  				},
   375  				Deploy: latest.DeployConfig{
   376  					DeployType: latest.DeployType{
   377  						KubectlDeploy: &latest.KubectlDeploy{},
   378  					},
   379  				},
   380  			},
   381  			expectedTester:   &test.FullTester{},
   382  			expectedDeployer: &kubectl.Deployer{},
   383  		},
   384  		{
   385  			description: "bad tagger config",
   386  			pipeline: latest.Pipeline{
   387  				Build: latest.BuildConfig{
   388  					TagPolicy: latest.TagPolicy{},
   389  					BuildType: latest.BuildType{
   390  						LocalBuild: &latest.LocalBuild{},
   391  					},
   392  				},
   393  				Deploy: latest.DeployConfig{
   394  					DeployType: latest.DeployType{
   395  						KubectlDeploy: &latest.KubectlDeploy{},
   396  					},
   397  				},
   398  			},
   399  			shouldErr: true,
   400  		},
   401  		{
   402  			description:      "unknown builder and tagger",
   403  			pipeline:         latest.Pipeline{},
   404  			shouldErr:        true,
   405  			expectedTester:   &test.FullTester{},
   406  			expectedDeployer: &kubectl.Deployer{},
   407  		},
   408  		{
   409  			description: "no artifacts, cache",
   410  			pipeline: latest.Pipeline{
   411  				Build: latest.BuildConfig{
   412  					TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}},
   413  					BuildType: latest.BuildType{
   414  						LocalBuild: &latest.LocalBuild{},
   415  					},
   416  				},
   417  				Deploy: latest.DeployConfig{
   418  					DeployType: latest.DeployType{
   419  						KubectlDeploy: &latest.KubectlDeploy{},
   420  					},
   421  				},
   422  			},
   423  			expectedTester:   &test.FullTester{},
   424  			expectedDeployer: &kubectl.Deployer{},
   425  			cacheArtifacts:   true,
   426  		},
   427  		{
   428  			description: "transformableAllowList",
   429  			pipeline: latest.Pipeline{
   430  				Build: latest.BuildConfig{
   431  					TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}},
   432  					BuildType: latest.BuildType{
   433  						LocalBuild: &latest.LocalBuild{},
   434  					},
   435  				},
   436  				Deploy: latest.DeployConfig{
   437  					DeployType: latest.DeployType{
   438  						KubectlDeploy: &latest.KubectlDeploy{},
   439  					},
   440  				},
   441  				ResourceSelector: latest.ResourceSelectorConfig{
   442  					Allow: []latest.ResourceFilter{
   443  						{
   444  							GroupKind: "example.com/Application",
   445  						},
   446  					},
   447  				},
   448  			},
   449  			expectedTester:   &test.FullTester{},
   450  			expectedDeployer: &kubectl.Deployer{},
   451  		},
   452  		{
   453  			description: "multiple deployers",
   454  			pipeline: latest.Pipeline{
   455  				Build: latest.BuildConfig{
   456  					TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}},
   457  					BuildType: latest.BuildType{
   458  						LocalBuild: &latest.LocalBuild{},
   459  					},
   460  				},
   461  				Deploy: latest.DeployConfig{
   462  					DeployType: latest.DeployType{
   463  						KubectlDeploy:   &latest.KubectlDeploy{},
   464  						KustomizeDeploy: &latest.KustomizeDeploy{},
   465  						HelmDeploy:      &latest.HelmDeploy{},
   466  					},
   467  				},
   468  			},
   469  			expectedTester: &test.FullTester{},
   470  			expectedDeployer: deploy.NewDeployerMux([]deploy.Deployer{
   471  				&helm.Deployer{},
   472  				&kubectl.Deployer{},
   473  				&kustomize.Deployer{},
   474  			}, false),
   475  		},
   476  	}
   477  	for _, test := range tests {
   478  		testutil.Run(t, test.description, func(t *testutil.T) {
   479  			t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"})
   480  			t.Override(&cluster.FindMinikubeBinary, func(context.Context) (string, semver.Version, error) {
   481  				return "", semver.Version{}, errors.New("not found")
   482  			})
   483  			t.Override(&util.DefaultExecCommand, testutil.CmdRunWithOutput(
   484  				"helm version --client", `version.BuildInfo{Version:"v3.0.0"}`).
   485  				AndRunWithOutput("kubectl version --client -ojson", "v1.5.6"))
   486  
   487  			runCtx := &runcontext.RunContext{
   488  				Pipelines: runcontext.NewPipelines([]latest.Pipeline{test.pipeline}),
   489  				Opts: config.SkaffoldOptions{
   490  					Trigger: "polling",
   491  				},
   492  			}
   493  			// Test transformableAllowList
   494  			filters := runCtx.TransformAllowList()
   495  			if test.pipeline.ResourceSelector.Allow != nil {
   496  				t.CheckDeepEqual(test.pipeline.ResourceSelector.Allow, filters)
   497  			} else {
   498  				t.CheckEmpty(filters)
   499  			}
   500  
   501  			cfg, err := NewForConfig(context.Background(), runCtx)
   502  			t.CheckError(test.shouldErr, err)
   503  			if cfg != nil {
   504  				b, _t, d := runner.WithTimings(&test.expectedBuilder, test.expectedTester, test.expectedDeployer, test.cacheArtifacts)
   505  				if test.shouldErr {
   506  					t.CheckError(true, err)
   507  				} else {
   508  					t.CheckNoError(err)
   509  					t.CheckTypeEquality(b, cfg.Pruner.Builder)
   510  					t.CheckTypeEquality(_t, cfg.tester)
   511  					t.CheckTypeEquality(d, cfg.deployer)
   512  				}
   513  			}
   514  		})
   515  	}
   516  }
   517  
   518  func TestTriggerCallbackAndIntents(t *testing.T) {
   519  	var tests = []struct {
   520  		description          string
   521  		autoBuild            bool
   522  		autoSync             bool
   523  		autoDeploy           bool
   524  		expectedBuildIntent  bool
   525  		expectedSyncIntent   bool
   526  		expectedDeployIntent bool
   527  	}{
   528  		{
   529  			description:          "default",
   530  			autoBuild:            true,
   531  			autoSync:             true,
   532  			autoDeploy:           true,
   533  			expectedBuildIntent:  true,
   534  			expectedSyncIntent:   true,
   535  			expectedDeployIntent: true,
   536  		},
   537  		{
   538  			description:          "build trigger in api mode",
   539  			autoBuild:            false,
   540  			autoSync:             true,
   541  			autoDeploy:           true,
   542  			expectedBuildIntent:  false,
   543  			expectedSyncIntent:   true,
   544  			expectedDeployIntent: true,
   545  		},
   546  		{
   547  			description:          "deploy trigger in api mode",
   548  			autoBuild:            true,
   549  			autoSync:             true,
   550  			autoDeploy:           false,
   551  			expectedBuildIntent:  true,
   552  			expectedSyncIntent:   true,
   553  			expectedDeployIntent: false,
   554  		},
   555  		{
   556  			description:          "sync trigger in api mode",
   557  			autoBuild:            true,
   558  			autoSync:             false,
   559  			autoDeploy:           true,
   560  			expectedBuildIntent:  true,
   561  			expectedSyncIntent:   false,
   562  			expectedDeployIntent: true,
   563  		},
   564  	}
   565  
   566  	for _, test := range tests {
   567  		testutil.Run(t, test.description, func(t *testutil.T) {
   568  			opts := config.SkaffoldOptions{
   569  				Trigger:           "polling",
   570  				WatchPollInterval: 100,
   571  				AutoBuild:         test.autoBuild,
   572  				AutoSync:          test.autoSync,
   573  				AutoDeploy:        test.autoDeploy,
   574  			}
   575  			pipeline := latest.Pipeline{
   576  				Build: latest.BuildConfig{
   577  					TagPolicy: latest.TagPolicy{ShaTagger: &latest.ShaTagger{}},
   578  					BuildType: latest.BuildType{
   579  						LocalBuild: &latest.LocalBuild{},
   580  					},
   581  				},
   582  				Deploy: latest.DeployConfig{
   583  					DeployType: latest.DeployType{
   584  						KubectlDeploy: &latest.KubectlDeploy{},
   585  					},
   586  				},
   587  			}
   588  			r, _ := NewForConfig(context.Background(), &runcontext.RunContext{
   589  				Opts:      opts,
   590  				Pipelines: runcontext.NewPipelines([]latest.Pipeline{pipeline}),
   591  			})
   592  
   593  			r.intents.ResetBuild()
   594  			r.intents.ResetSync()
   595  			r.intents.ResetDeploy()
   596  
   597  			build, sync, deploy := r.intents.GetIntentsAttrs()
   598  			t.CheckDeepEqual(test.expectedBuildIntent, build)
   599  			t.CheckDeepEqual(test.expectedSyncIntent, sync)
   600  			t.CheckDeepEqual(test.expectedDeployIntent, deploy)
   601  		})
   602  	}
   603  }