github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/runner/v1/dev_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  	"io/ioutil"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	k8s "k8s.io/client-go/kubernetes"
    27  	fakekubeclientset "k8s.io/client-go/kubernetes/fake"
    28  	"k8s.io/client-go/tools/clientcmd/api"
    29  
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    35  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync"
    36  	"github.com/GoogleContainerTools/skaffold/testutil"
    37  )
    38  
    39  type NoopMonitor struct{}
    40  
    41  func (t *NoopMonitor) Register(func() ([]string, error), func(filemon.Events)) error {
    42  	return nil
    43  }
    44  
    45  func (t *NoopMonitor) Run(bool) error {
    46  	return nil
    47  }
    48  
    49  func (t *NoopMonitor) Reset() {}
    50  
    51  type FailMonitor struct{}
    52  
    53  func (t *FailMonitor) Register(func() ([]string, error), func(filemon.Events)) error {
    54  	return nil
    55  }
    56  
    57  func (t *FailMonitor) Run(bool) error {
    58  	return errors.New("BUG")
    59  }
    60  
    61  func (t *FailMonitor) Reset() {}
    62  
    63  type TestMonitor struct {
    64  	events    []filemon.Events
    65  	callbacks []func(filemon.Events)
    66  	testBench *TestBench
    67  }
    68  
    69  func (t *TestMonitor) Register(deps func() ([]string, error), onChange func(filemon.Events)) error {
    70  	t.callbacks = append(t.callbacks, onChange)
    71  	return nil
    72  }
    73  
    74  func (t *TestMonitor) Run(bool) error {
    75  	if t.testBench.intentTrigger {
    76  		return nil
    77  	}
    78  
    79  	evt := t.events[t.testBench.currentCycle]
    80  
    81  	for _, file := range evt.Modified {
    82  		switch file {
    83  		case "file1":
    84  			t.callbacks[0](evt) // 1st artifact changed
    85  		case "file2":
    86  			t.callbacks[1](evt) // 2nd artifact changed
    87  		// callbacks[2] and callbacks[3] are for `test` dependency triggers
    88  		case "manifest.yaml":
    89  			t.callbacks[4](evt) // deployment configuration changed
    90  		}
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  func (t *TestMonitor) Reset() {}
    97  
    98  func mockK8sClient(string) (k8s.Interface, error) {
    99  	return fakekubeclientset.NewSimpleClientset(), nil
   100  }
   101  
   102  func TestDevFailFirstCycle(t *testing.T) {
   103  	tests := []struct {
   104  		description     string
   105  		testBench       *TestBench
   106  		monitor         filemon.Monitor
   107  		expectedActions []Actions
   108  	}{
   109  		{
   110  			description:     "fails to build the first time",
   111  			testBench:       &TestBench{buildErrors: []error{errors.New("")}},
   112  			monitor:         &NoopMonitor{},
   113  			expectedActions: []Actions{{}},
   114  		},
   115  		{
   116  			description: "fails to test the first time",
   117  			testBench:   &TestBench{testErrors: []error{errors.New("")}},
   118  			monitor:     &NoopMonitor{},
   119  			expectedActions: []Actions{{
   120  				Built: []string{"img:1"},
   121  			}},
   122  		},
   123  		{
   124  			description: "fails to deploy the first time",
   125  			testBench:   &TestBench{deployErrors: []error{errors.New("")}},
   126  			monitor:     &NoopMonitor{},
   127  			expectedActions: []Actions{{
   128  				Built:  []string{"img:1"},
   129  				Tested: []string{"img:1"},
   130  			}},
   131  		},
   132  		{
   133  			description: "fails to watch after first cycle",
   134  			testBench:   &TestBench{},
   135  			monitor:     &FailMonitor{},
   136  			expectedActions: []Actions{{
   137  				Built:    []string{"img:1"},
   138  				Tested:   []string{"img:1"},
   139  				Deployed: []string{"img:1"},
   140  			}},
   141  		},
   142  	}
   143  	for _, test := range tests {
   144  		testutil.Run(t, test.description, func(t *testutil.T) {
   145  			t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"})
   146  			t.Override(&client.Client, mockK8sClient)
   147  			artifacts := []*latest.Artifact{{
   148  				ImageName: "img",
   149  			}}
   150  			r := createRunner(t, test.testBench, test.monitor, artifacts, nil)
   151  			test.testBench.firstMonitor = test.monitor.Run
   152  
   153  			err := r.Dev(context.Background(), ioutil.Discard, artifacts)
   154  
   155  			t.CheckErrorAndDeepEqual(true, err, test.expectedActions, test.testBench.Actions())
   156  		})
   157  	}
   158  }
   159  
   160  func TestDev(t *testing.T) {
   161  	tests := []struct {
   162  		description     string
   163  		testBench       *TestBench
   164  		watchEvents     []filemon.Events
   165  		expectedActions []Actions
   166  	}{
   167  		{
   168  			description: "ignore subsequent build errors",
   169  			testBench:   NewTestBench().WithBuildErrors([]error{nil, errors.New("")}),
   170  			watchEvents: []filemon.Events{
   171  				{Modified: []string{"file1", "file2"}},
   172  			},
   173  			expectedActions: []Actions{
   174  				{
   175  					Built:    []string{"img1:1", "img2:1"},
   176  					Tested:   []string{"img1:1", "img2:1"},
   177  					Deployed: []string{"img1:1", "img2:1"},
   178  				},
   179  				{},
   180  			},
   181  		},
   182  		{
   183  			description: "ignore subsequent test errors",
   184  			testBench:   &TestBench{testErrors: []error{nil, errors.New("")}},
   185  			watchEvents: []filemon.Events{
   186  				{Modified: []string{"file1", "file2"}},
   187  			},
   188  			expectedActions: []Actions{
   189  				{
   190  					Built:    []string{"img1:1", "img2:1"},
   191  					Tested:   []string{"img1:1", "img2:1"},
   192  					Deployed: []string{"img1:1", "img2:1"},
   193  				},
   194  				{
   195  					Built: []string{"img1:2", "img2:2"},
   196  				},
   197  			},
   198  		},
   199  		{
   200  			description: "ignore subsequent deploy errors",
   201  			testBench:   &TestBench{deployErrors: []error{nil, errors.New("")}},
   202  			watchEvents: []filemon.Events{
   203  				{Modified: []string{"file1", "file2"}},
   204  			},
   205  			expectedActions: []Actions{
   206  				{
   207  					Built:    []string{"img1:1", "img2:1"},
   208  					Tested:   []string{"img1:1", "img2:1"},
   209  					Deployed: []string{"img1:1", "img2:1"},
   210  				},
   211  				{
   212  					Built:  []string{"img1:2", "img2:2"},
   213  					Tested: []string{"img1:2", "img2:2"},
   214  				},
   215  			},
   216  		},
   217  		{
   218  			description: "full cycle twice",
   219  			testBench:   &TestBench{},
   220  			watchEvents: []filemon.Events{
   221  				{Modified: []string{"file1", "file2"}},
   222  			},
   223  			expectedActions: []Actions{
   224  				{
   225  					Built:    []string{"img1:1", "img2:1"},
   226  					Tested:   []string{"img1:1", "img2:1"},
   227  					Deployed: []string{"img1:1", "img2:1"},
   228  				},
   229  				{
   230  					Built:    []string{"img1:2", "img2:2"},
   231  					Tested:   []string{"img1:2", "img2:2"},
   232  					Deployed: []string{"img1:2", "img2:2"},
   233  				},
   234  			},
   235  		},
   236  		{
   237  			description: "only change second artifact",
   238  			testBench:   &TestBench{},
   239  			watchEvents: []filemon.Events{
   240  				{Modified: []string{"file2"}},
   241  			},
   242  			expectedActions: []Actions{
   243  				{
   244  					Built:    []string{"img1:1", "img2:1"},
   245  					Tested:   []string{"img1:1", "img2:1"},
   246  					Deployed: []string{"img1:1", "img2:1"},
   247  				},
   248  				{
   249  					Built:    []string{"img2:2"},
   250  					Tested:   []string{"img2:2"},
   251  					Deployed: []string{"img1:1", "img2:2"},
   252  				},
   253  			},
   254  		},
   255  		{
   256  			description: "redeploy",
   257  			testBench:   &TestBench{},
   258  			watchEvents: []filemon.Events{
   259  				{Modified: []string{"manifest.yaml"}},
   260  			},
   261  			expectedActions: []Actions{
   262  				{
   263  					Built:    []string{"img1:1", "img2:1"},
   264  					Tested:   []string{"img1:1", "img2:1"},
   265  					Deployed: []string{"img1:1", "img2:1"},
   266  				},
   267  				{
   268  					Deployed: []string{"img1:1", "img2:1"},
   269  				},
   270  			},
   271  		},
   272  	}
   273  	for _, test := range tests {
   274  		testutil.Run(t, test.description, func(t *testutil.T) {
   275  			t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"})
   276  			t.Override(&client.Client, mockK8sClient)
   277  			test.testBench.cycles = len(test.watchEvents)
   278  			artifacts := []*latest.Artifact{
   279  				{ImageName: "img1"},
   280  				{ImageName: "img2"},
   281  			}
   282  			r := createRunner(t, test.testBench, &TestMonitor{
   283  				events:    test.watchEvents,
   284  				testBench: test.testBench,
   285  			}, artifacts, nil)
   286  
   287  			err := r.Dev(context.Background(), ioutil.Discard, artifacts)
   288  
   289  			t.CheckNoError(err)
   290  			t.CheckDeepEqual(test.expectedActions, test.testBench.Actions())
   291  		})
   292  	}
   293  }
   294  
   295  func TestDevAutoTriggers(t *testing.T) {
   296  	tests := []struct {
   297  		description     string
   298  		watchEvents     []filemon.Events
   299  		expectedActions []Actions
   300  		autoTriggers    triggerState // the state of auto triggers
   301  		singleTriggers  triggerState // the state of single intent triggers at the end of dev loop
   302  		userIntents     []func(i *runner.Intents)
   303  	}{
   304  		{
   305  			description: "build on; sync on; deploy on",
   306  			watchEvents: []filemon.Events{
   307  				{Modified: []string{"file1"}},
   308  				{Modified: []string{"file2"}},
   309  			},
   310  			autoTriggers:   triggerState{true, true, true},
   311  			singleTriggers: triggerState{true, true, true},
   312  			expectedActions: []Actions{
   313  				{
   314  					Synced: []string{"img1:1"},
   315  				},
   316  				{
   317  					Built:    []string{"img2:2"},
   318  					Tested:   []string{"img2:2"},
   319  					Deployed: []string{"img1:1", "img2:2"},
   320  				},
   321  			},
   322  		},
   323  		{
   324  			description: "build off; sync off; deploy off",
   325  			watchEvents: []filemon.Events{
   326  				{Modified: []string{"file1"}},
   327  				{Modified: []string{"file2"}},
   328  			},
   329  			expectedActions: []Actions{{}, {}},
   330  		},
   331  		{
   332  			description: "build on; sync off; deploy off",
   333  			watchEvents: []filemon.Events{
   334  				{Modified: []string{"file1"}},
   335  				{Modified: []string{"file2"}},
   336  			},
   337  			autoTriggers:   triggerState{true, false, false},
   338  			singleTriggers: triggerState{true, false, false},
   339  			expectedActions: []Actions{{}, {
   340  				Built:  []string{"img2:2"},
   341  				Tested: []string{"img2:2"},
   342  			}},
   343  		},
   344  		{
   345  			description: "build off; sync on; deploy off",
   346  			watchEvents: []filemon.Events{
   347  				{Modified: []string{"file1"}},
   348  				{Modified: []string{"file2"}},
   349  			},
   350  			autoTriggers:   triggerState{false, true, false},
   351  			singleTriggers: triggerState{false, true, false},
   352  			expectedActions: []Actions{{
   353  				Synced: []string{"img1:1"},
   354  			}, {}},
   355  		},
   356  		{
   357  			description: "build off; sync off; deploy on",
   358  			watchEvents: []filemon.Events{
   359  				{Modified: []string{"file1"}},
   360  				{Modified: []string{"file2"}},
   361  			},
   362  			autoTriggers:    triggerState{false, false, true},
   363  			singleTriggers:  triggerState{false, false, true},
   364  			expectedActions: []Actions{{}, {}},
   365  		},
   366  		{
   367  			description:     "build off; sync off; deploy off; user requests build, but no change so intent is discarded",
   368  			watchEvents:     []filemon.Events{},
   369  			autoTriggers:    triggerState{false, false, false},
   370  			singleTriggers:  triggerState{false, false, false},
   371  			expectedActions: []Actions{},
   372  			userIntents: []func(i *runner.Intents){
   373  				func(i *runner.Intents) {
   374  					i.SetBuild(true)
   375  				},
   376  			},
   377  		},
   378  		{
   379  			description:     "build off; sync off; deploy off; user requests build, and then sync, but no change so both intents are discarded",
   380  			watchEvents:     []filemon.Events{},
   381  			autoTriggers:    triggerState{false, false, false},
   382  			singleTriggers:  triggerState{false, false, false},
   383  			expectedActions: []Actions{},
   384  			userIntents: []func(i *runner.Intents){
   385  				func(i *runner.Intents) {
   386  					i.SetBuild(true)
   387  					i.SetSync(true)
   388  				},
   389  			},
   390  		},
   391  		{
   392  			description:     "build off; sync off; deploy off; user requests build, and then sync, but no change so both intents are discarded",
   393  			watchEvents:     []filemon.Events{},
   394  			autoTriggers:    triggerState{false, false, false},
   395  			singleTriggers:  triggerState{false, false, false},
   396  			expectedActions: []Actions{},
   397  			userIntents: []func(i *runner.Intents){
   398  				func(i *runner.Intents) {
   399  					i.SetBuild(true)
   400  				},
   401  				func(i *runner.Intents) {
   402  					i.SetSync(true)
   403  				},
   404  			},
   405  		},
   406  	}
   407  	// first build-test-deploy sequence always happens
   408  	firstAction := Actions{
   409  		Built:    []string{"img1:1", "img2:1"},
   410  		Tested:   []string{"img1:1", "img2:1"},
   411  		Deployed: []string{"img1:1", "img2:1"},
   412  	}
   413  
   414  	for _, test := range tests {
   415  		testutil.Run(t, test.description, func(t *testutil.T) {
   416  			t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"})
   417  			t.Override(&client.Client, mockK8sClient)
   418  			t.Override(&sync.WorkingDir, func(context.Context, string, docker.Config) (string, error) { return "/", nil })
   419  			testBench := &TestBench{}
   420  			testBench.cycles = len(test.watchEvents)
   421  			testBench.userIntents = test.userIntents
   422  			artifacts := []*latest.Artifact{
   423  				{
   424  					ImageName: "img1",
   425  					Sync: &latest.Sync{
   426  						Manual: []*latest.SyncRule{{Src: "file1", Dest: "file1"}},
   427  					},
   428  				},
   429  				{
   430  					ImageName: "img2",
   431  				},
   432  			}
   433  			r := createRunner(t, testBench, &TestMonitor{
   434  				events:    test.watchEvents,
   435  				testBench: testBench,
   436  			}, artifacts, &test.autoTriggers)
   437  
   438  			testBench.intents = r.intents
   439  
   440  			err := r.Dev(context.Background(), ioutil.Discard, artifacts)
   441  
   442  			t.CheckNoError(err)
   443  			t.CheckDeepEqual(append([]Actions{firstAction}, test.expectedActions...), testBench.Actions())
   444  
   445  			build, _sync, deploy := r.intents.GetIntentsAttrs()
   446  			singleTriggers := triggerState{
   447  				build:  build,
   448  				sync:   _sync,
   449  				deploy: deploy,
   450  			}
   451  			t.CheckDeepEqual(test.singleTriggers, singleTriggers, cmp.AllowUnexported(triggerState{}))
   452  		})
   453  	}
   454  }
   455  
   456  func TestDevSync(t *testing.T) {
   457  	type fileSyncEventCalls struct {
   458  		InProgress int
   459  		Failed     int
   460  		Succeeded  int
   461  	}
   462  
   463  	tests := []struct {
   464  		description                string
   465  		testBench                  *TestBench
   466  		watchEvents                []filemon.Events
   467  		expectedActions            []Actions
   468  		expectedFileSyncEventCalls fileSyncEventCalls
   469  	}{
   470  		{
   471  			description: "sync",
   472  			testBench:   &TestBench{},
   473  			watchEvents: []filemon.Events{
   474  				{Modified: []string{"file1"}},
   475  			},
   476  			expectedActions: []Actions{
   477  				{
   478  					Built:    []string{"img1:1", "img2:1"},
   479  					Tested:   []string{"img1:1", "img2:1"},
   480  					Deployed: []string{"img1:1", "img2:1"},
   481  				},
   482  				{
   483  					Synced: []string{"img1:1"},
   484  				},
   485  			},
   486  			expectedFileSyncEventCalls: fileSyncEventCalls{
   487  				InProgress: 1,
   488  				Failed:     0,
   489  				Succeeded:  1,
   490  			},
   491  		},
   492  		{
   493  			description: "sync twice",
   494  			testBench:   &TestBench{},
   495  			watchEvents: []filemon.Events{
   496  				{Modified: []string{"file1"}},
   497  				{Modified: []string{"file1"}},
   498  			},
   499  			expectedActions: []Actions{
   500  				{
   501  					Built:    []string{"img1:1", "img2:1"},
   502  					Tested:   []string{"img1:1", "img2:1"},
   503  					Deployed: []string{"img1:1", "img2:1"},
   504  				},
   505  				{
   506  					Synced: []string{"img1:1"},
   507  				},
   508  				{
   509  					Synced: []string{"img1:1"},
   510  				},
   511  			},
   512  			expectedFileSyncEventCalls: fileSyncEventCalls{
   513  				InProgress: 2,
   514  				Failed:     0,
   515  				Succeeded:  2,
   516  			},
   517  		},
   518  	}
   519  	for _, test := range tests {
   520  		testutil.Run(t, test.description, func(t *testutil.T) {
   521  			var actualFileSyncEventCalls fileSyncEventCalls
   522  			t.SetupFakeKubernetesContext(api.Config{CurrentContext: "cluster1"})
   523  			t.Override(&client.Client, mockK8sClient)
   524  			t.Override(&fileSyncInProgress, func(int, string) { actualFileSyncEventCalls.InProgress++ })
   525  			t.Override(&fileSyncFailed, func(int, string, error) { actualFileSyncEventCalls.Failed++ })
   526  			t.Override(&fileSyncSucceeded, func(int, string) { actualFileSyncEventCalls.Succeeded++ })
   527  			t.Override(&sync.WorkingDir, func(context.Context, string, docker.Config) (string, error) { return "/", nil })
   528  			test.testBench.cycles = len(test.watchEvents)
   529  			artifacts := []*latest.Artifact{
   530  				{
   531  					ImageName: "img1",
   532  					Sync: &latest.Sync{
   533  						Manual: []*latest.SyncRule{{Src: "file1", Dest: "file1"}},
   534  					},
   535  				},
   536  				{
   537  					ImageName: "img2",
   538  				},
   539  			}
   540  			r := createRunner(t, test.testBench, &TestMonitor{
   541  				events:    test.watchEvents,
   542  				testBench: test.testBench,
   543  			}, artifacts, nil)
   544  
   545  			err := r.Dev(context.Background(), ioutil.Discard, artifacts)
   546  
   547  			t.CheckNoError(err)
   548  			t.CheckDeepEqual(test.expectedActions, test.testBench.Actions())
   549  			t.CheckDeepEqual(test.expectedFileSyncEventCalls, actualFileSyncEventCalls)
   550  		})
   551  	}
   552  }