github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/deploy/kubectl/kubectl_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 kubectl
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/label"
    32  	deployutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/util"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
    35  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext"
    37  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    38  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    39  	"github.com/GoogleContainerTools/skaffold/testutil"
    40  )
    41  
    42  func TestKubectlDeploy(t *testing.T) {
    43  	tests := []struct {
    44  		description                 string
    45  		kubectl                     latest.KubectlDeploy
    46  		builds                      []graph.Artifact
    47  		commands                    util.Command
    48  		shouldErr                   bool
    49  		forceDeploy                 bool
    50  		waitForDeletions            bool
    51  		skipSkaffoldNamespaceOption bool
    52  		envs                        map[string]string
    53  	}{
    54  		{
    55  			description:      "no manifest",
    56  			kubectl:          latest.KubectlDeploy{},
    57  			commands:         testutil.CmdRunOut("kubectl version --client -ojson", KubectlVersion112),
    58  			waitForDeletions: true,
    59  		},
    60  		{
    61  			description: "deploy success (disable validation)",
    62  			kubectl: latest.KubectlDeploy{
    63  				Manifests: []string{"deployment.yaml"},
    64  				Flags: latest.KubectlFlags{
    65  					DisableValidation: true,
    66  				},
    67  			},
    68  			commands: testutil.
    69  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
    70  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f deployment.yaml --validate=false", DeploymentWebYAML).
    71  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
    72  				AndRun("kubectl --context kubecontext --namespace testNamespace apply -f - --validate=false"),
    73  			builds: []graph.Artifact{{
    74  				ImageName: "leeroy-web",
    75  				Tag:       "leeroy-web:v1",
    76  			}},
    77  			waitForDeletions: true,
    78  		},
    79  		{
    80  			description: "deploy success (forced)",
    81  			kubectl: latest.KubectlDeploy{
    82  				Manifests: []string{"deployment.yaml"},
    83  			},
    84  			commands: testutil.
    85  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
    86  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f deployment.yaml", DeploymentWebYAML).
    87  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
    88  				AndRun("kubectl --context kubecontext --namespace testNamespace apply -f - --force --grace-period=0"),
    89  			builds: []graph.Artifact{{
    90  				ImageName: "leeroy-web",
    91  				Tag:       "leeroy-web:v1",
    92  			}},
    93  			forceDeploy:      true,
    94  			waitForDeletions: true,
    95  		},
    96  		{
    97  			description: "deploy success",
    98  			kubectl: latest.KubectlDeploy{
    99  				Manifests: []string{"deployment.yaml"},
   100  			},
   101  			commands: testutil.
   102  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   103  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f deployment.yaml", DeploymentWebYAML).
   104  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
   105  				AndRun("kubectl --context kubecontext --namespace testNamespace apply -f -"),
   106  			builds: []graph.Artifact{{
   107  				ImageName: "leeroy-web",
   108  				Tag:       "leeroy-web:v1",
   109  			}},
   110  			waitForDeletions: true,
   111  		},
   112  		{
   113  			description: "deploy success (kubectl v1.18)",
   114  			kubectl: latest.KubectlDeploy{
   115  				Manifests: []string{"deployment.yaml"},
   116  			},
   117  			commands: testutil.
   118  				CmdRunOut("kubectl version --client -ojson", KubectlVersion118).
   119  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run=client -oyaml -f deployment.yaml", DeploymentWebYAML).
   120  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
   121  				AndRun("kubectl --context kubecontext --namespace testNamespace apply -f -"),
   122  			builds: []graph.Artifact{{
   123  				ImageName: "leeroy-web",
   124  				Tag:       "leeroy-web:v1",
   125  			}},
   126  			waitForDeletions: true,
   127  		},
   128  		{
   129  			description: "deploy success (default namespace)",
   130  			kubectl: latest.KubectlDeploy{
   131  				Manifests:        []string{"deployment.yaml"},
   132  				DefaultNamespace: &TestNamespace2,
   133  			},
   134  			commands: testutil.
   135  				CmdRunOut("kubectl version --client -ojson", KubectlVersion118).
   136  				AndRunOut("kubectl --context kubecontext --namespace testNamespace2 create --dry-run=client -oyaml -f deployment.yaml", DeploymentWebYAML).
   137  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace2 get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
   138  				AndRun("kubectl --context kubecontext --namespace testNamespace2 apply -f -"),
   139  			builds: []graph.Artifact{{
   140  				ImageName: "leeroy-web",
   141  				Tag:       "leeroy-web:v1",
   142  			}},
   143  			waitForDeletions:            true,
   144  			skipSkaffoldNamespaceOption: true,
   145  		},
   146  		{
   147  			description: "deploy success (default namespace with env template)",
   148  			kubectl: latest.KubectlDeploy{
   149  				Manifests:        []string{"deployment.yaml"},
   150  				DefaultNamespace: &TestNamespace2FromEnvTemplate,
   151  			},
   152  			commands: testutil.
   153  				CmdRunOut("kubectl version --client -ojson", KubectlVersion118).
   154  				AndRunOut("kubectl --context kubecontext --namespace testNamespace2 create --dry-run=client -oyaml -f deployment.yaml", DeploymentWebYAML).
   155  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace2 get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
   156  				AndRun("kubectl --context kubecontext --namespace testNamespace2 apply -f -"),
   157  			builds: []graph.Artifact{{
   158  				ImageName: "leeroy-web",
   159  				Tag:       "leeroy-web:v1",
   160  			}},
   161  			waitForDeletions:            true,
   162  			skipSkaffoldNamespaceOption: true,
   163  			envs: map[string]string{
   164  				"MYENV": "Namesp",
   165  			},
   166  		},
   167  		{
   168  			description: "http manifest",
   169  			kubectl: latest.KubectlDeploy{
   170  				Manifests: []string{"deployment.yaml", "http://remote.yaml"},
   171  			},
   172  			commands: testutil.
   173  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   174  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f deployment.yaml -f http://remote.yaml", DeploymentWebYAML).
   175  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
   176  				AndRun("kubectl --context kubecontext --namespace testNamespace apply -f -"),
   177  			builds: []graph.Artifact{{
   178  				ImageName: "leeroy-web",
   179  				Tag:       "leeroy-web:v1",
   180  			}},
   181  			waitForDeletions: true,
   182  		},
   183  		{
   184  			description: "deploy command error",
   185  			kubectl: latest.KubectlDeploy{
   186  				Manifests: []string{"deployment.yaml"},
   187  			},
   188  			commands: testutil.
   189  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   190  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f deployment.yaml", DeploymentWebYAML).
   191  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
   192  				AndRunErr("kubectl --context kubecontext --namespace testNamespace apply -f -", fmt.Errorf("")),
   193  			builds: []graph.Artifact{{
   194  				ImageName: "leeroy-web",
   195  				Tag:       "leeroy-web:v1",
   196  			}},
   197  			shouldErr:        true,
   198  			waitForDeletions: true,
   199  		},
   200  		{
   201  			description: "additional flags",
   202  			kubectl: latest.KubectlDeploy{
   203  				Manifests: []string{"deployment.yaml"},
   204  				Flags: latest.KubectlFlags{
   205  					Global: []string{"-v=0"},
   206  					Apply:  []string{"--overwrite=true"},
   207  					Delete: []string{"ignored"},
   208  				},
   209  			},
   210  			commands: testutil.
   211  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   212  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create -v=0 --dry-run -oyaml -f deployment.yaml", DeploymentWebYAML).
   213  				AndRunInputOut("kubectl --context kubecontext --namespace testNamespace get -v=0 -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
   214  				AndRunErr("kubectl --context kubecontext --namespace testNamespace apply -v=0 --overwrite=true -f -", fmt.Errorf("")),
   215  			builds: []graph.Artifact{{
   216  				ImageName: "leeroy-web",
   217  				Tag:       "leeroy-web:v1",
   218  			}},
   219  			shouldErr:        true,
   220  			waitForDeletions: true,
   221  		},
   222  	}
   223  	for _, test := range tests {
   224  		testutil.Run(t, test.description, func(t *testutil.T) {
   225  			t.SetEnvs(test.envs)
   226  			t.Override(&util.DefaultExecCommand, test.commands)
   227  			t.Override(&client.Client, deployutil.MockK8sClient)
   228  			t.NewTempDir().
   229  				Write("deployment.yaml", DeploymentWebYAML).
   230  				Touch("empty.ignored").
   231  				Chdir()
   232  
   233  			skaffoldNamespaceOption := ""
   234  			if !test.skipSkaffoldNamespaceOption {
   235  				skaffoldNamespaceOption = TestNamespace
   236  			}
   237  
   238  			k, err := NewDeployer(&kubectlConfig{
   239  				workingDir: ".",
   240  				force:      test.forceDeploy,
   241  				waitForDeletions: config.WaitForDeletions{
   242  					Enabled: test.waitForDeletions,
   243  					Delay:   0 * time.Second,
   244  					Max:     10 * time.Second,
   245  				},
   246  				RunContext: runcontext.RunContext{Opts: config.SkaffoldOptions{
   247  					Namespace: skaffoldNamespaceOption}},
   248  			}, &label.DefaultLabeller{}, &test.kubectl)
   249  			t.RequireNoError(err)
   250  
   251  			err = k.Deploy(context.Background(), ioutil.Discard, test.builds)
   252  
   253  			t.CheckError(test.shouldErr, err)
   254  		})
   255  	}
   256  }
   257  
   258  func TestKubectlCleanup(t *testing.T) {
   259  	tests := []struct {
   260  		description string
   261  		kubectl     latest.KubectlDeploy
   262  		commands    util.Command
   263  		shouldErr   bool
   264  		dryRun      bool
   265  	}{
   266  		{
   267  			description: "cleanup dry-run",
   268  			kubectl: latest.KubectlDeploy{
   269  				Manifests: []string{"deployment.yaml"},
   270  			},
   271  			commands: testutil.
   272  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   273  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f deployment.yaml", DeploymentWebYAML).
   274  				AndRun("kubectl --context kubecontext --namespace testNamespace delete --dry-run --ignore-not-found=true --wait=false -f -"),
   275  			dryRun: true,
   276  		},
   277  		{
   278  			description: "cleanup success",
   279  			kubectl: latest.KubectlDeploy{
   280  				Manifests: []string{"deployment.yaml"},
   281  			},
   282  			commands: testutil.
   283  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   284  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f deployment.yaml", DeploymentWebYAML).
   285  				AndRun("kubectl --context kubecontext --namespace testNamespace delete --ignore-not-found=true --wait=false -f -"),
   286  		},
   287  		{
   288  			description: "cleanup success (kubectl v1.18)",
   289  			kubectl: latest.KubectlDeploy{
   290  				Manifests: []string{"deployment.yaml"},
   291  			},
   292  			commands: testutil.
   293  				CmdRunOut("kubectl version --client -ojson", KubectlVersion118).
   294  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run=client -oyaml -f deployment.yaml", DeploymentWebYAML).
   295  				AndRun("kubectl --context kubecontext --namespace testNamespace delete --ignore-not-found=true --wait=false -f -"),
   296  		},
   297  		{
   298  			description: "cleanup error",
   299  			kubectl: latest.KubectlDeploy{
   300  				Manifests: []string{"deployment.yaml"},
   301  			},
   302  			commands: testutil.
   303  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   304  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f deployment.yaml", DeploymentWebYAML).
   305  				AndRunErr("kubectl --context kubecontext --namespace testNamespace delete --ignore-not-found=true --wait=false -f -", errors.New("BUG")),
   306  			shouldErr: true,
   307  		},
   308  		{
   309  			description: "additional flags",
   310  			kubectl: latest.KubectlDeploy{
   311  				Manifests: []string{"deployment.yaml"},
   312  				Flags: latest.KubectlFlags{
   313  					Global: []string{"-v=0"},
   314  					Apply:  []string{"ignored"},
   315  					Delete: []string{"--grace-period=1"},
   316  				},
   317  			},
   318  			commands: testutil.
   319  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   320  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create -v=0 --dry-run -oyaml -f deployment.yaml", DeploymentWebYAML).
   321  				AndRun("kubectl --context kubecontext --namespace testNamespace delete -v=0 --grace-period=1 --ignore-not-found=true --wait=false -f -"),
   322  		},
   323  	}
   324  	for _, test := range tests {
   325  		testutil.Run(t, test.description, func(t *testutil.T) {
   326  			t.Override(&util.DefaultExecCommand, test.commands)
   327  			t.NewTempDir().
   328  				Write("deployment.yaml", DeploymentWebYAML).
   329  				Chdir()
   330  
   331  			k, err := NewDeployer(&kubectlConfig{
   332  				workingDir: ".",
   333  				RunContext: runcontext.RunContext{Opts: config.SkaffoldOptions{Namespace: TestNamespace}},
   334  			}, &label.DefaultLabeller{}, &test.kubectl)
   335  			t.RequireNoError(err)
   336  
   337  			err = k.Cleanup(context.Background(), ioutil.Discard, test.dryRun)
   338  
   339  			t.CheckError(test.shouldErr, err)
   340  		})
   341  	}
   342  }
   343  
   344  func TestKubectlDeployerRemoteCleanup(t *testing.T) {
   345  	tests := []struct {
   346  		description string
   347  		kubectl     latest.KubectlDeploy
   348  		commands    util.Command
   349  	}{
   350  		{
   351  			description: "cleanup success",
   352  			kubectl: latest.KubectlDeploy{
   353  				RemoteManifests: []string{"pod/leeroy-web"},
   354  			},
   355  			commands: testutil.
   356  				CmdRun("kubectl --context kubecontext --namespace testNamespace get pod/leeroy-web -o yaml").
   357  				AndRun("kubectl --context kubecontext --namespace testNamespace delete --ignore-not-found=true --wait=false -f -").
   358  				AndRunInput("kubectl --context kubecontext --namespace testNamespace apply -f -", DeploymentWebYAML),
   359  		},
   360  		{
   361  			description: "cleanup error",
   362  			kubectl: latest.KubectlDeploy{
   363  				RemoteManifests: []string{"anotherNamespace:pod/leeroy-web"},
   364  			},
   365  			commands: testutil.
   366  				CmdRun("kubectl --context kubecontext --namespace anotherNamespace get pod/leeroy-web -o yaml").
   367  				AndRun("kubectl --context kubecontext --namespace testNamespace delete --ignore-not-found=true --wait=false -f -").
   368  				AndRunInput("kubectl --context kubecontext --namespace anotherNamespace apply -f -", DeploymentWebYAML),
   369  		},
   370  	}
   371  	for _, test := range tests {
   372  		testutil.Run(t, "cleanup remote", func(t *testutil.T) {
   373  			t.Override(&util.DefaultExecCommand, test.commands)
   374  			t.NewTempDir().
   375  				Write("deployment.yaml", DeploymentWebYAML).
   376  				Chdir()
   377  
   378  			k, err := NewDeployer(&kubectlConfig{
   379  				workingDir: ".",
   380  				RunContext: runcontext.RunContext{Opts: config.SkaffoldOptions{Namespace: TestNamespace}},
   381  			}, &label.DefaultLabeller{}, &test.kubectl)
   382  			t.RequireNoError(err)
   383  
   384  			err = k.Cleanup(context.Background(), ioutil.Discard, false)
   385  
   386  			t.CheckNoError(err)
   387  		})
   388  	}
   389  }
   390  
   391  func TestKubectlRedeploy(t *testing.T) {
   392  	testutil.Run(t, "", func(t *testutil.T) {
   393  		t.Override(&client.Client, deployutil.MockK8sClient)
   394  		tmpDir := t.NewTempDir().
   395  			Write("deployment-web.yaml", DeploymentWebYAML).
   396  			Write("deployment-app.yaml", DeploymentAppYAML)
   397  
   398  		t.Override(&util.DefaultExecCommand, testutil.
   399  			CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   400  			AndRunOut("kubectl --context kubecontext create --dry-run -oyaml -f "+tmpDir.Path("deployment-app.yaml")+" -f "+tmpDir.Path("deployment-web.yaml"), DeploymentAppYAML+"\n"+DeploymentWebYAML).
   401  			AndRunInputOut("kubectl --context kubecontext get -f - --ignore-not-found -ojson", DeploymentAppYAMLv1+"\n---\n"+DeploymentWebYAMLv1, "").
   402  			AndRunInput("kubectl --context kubecontext apply -f -", DeploymentAppYAMLv1+"\n---\n"+DeploymentWebYAMLv1).
   403  			AndRunOut("kubectl --context kubecontext create --dry-run -oyaml -f "+tmpDir.Path("deployment-app.yaml")+" -f "+tmpDir.Path("deployment-web.yaml"), DeploymentAppYAML+"\n"+DeploymentWebYAML).
   404  			AndRunInputOut("kubectl --context kubecontext get -f - --ignore-not-found -ojson", DeploymentAppYAMLv2+"\n---\n"+DeploymentWebYAMLv1, "").
   405  			AndRunInput("kubectl --context kubecontext apply -f -", DeploymentAppYAMLv2).
   406  			AndRunOut("kubectl --context kubecontext create --dry-run -oyaml -f "+tmpDir.Path("deployment-app.yaml")+" -f "+tmpDir.Path("deployment-web.yaml"), DeploymentAppYAML+"\n"+DeploymentWebYAML).
   407  			AndRunInputOut("kubectl --context kubecontext get -f - --ignore-not-found -ojson", DeploymentAppYAMLv2+"\n---\n"+DeploymentWebYAMLv1, ""),
   408  		)
   409  
   410  		deployer, err := NewDeployer(&kubectlConfig{
   411  			workingDir: ".",
   412  			waitForDeletions: config.WaitForDeletions{
   413  				Enabled: true,
   414  				Delay:   0 * time.Millisecond,
   415  				Max:     10 * time.Second},
   416  		}, &label.DefaultLabeller{}, &latest.KubectlDeploy{Manifests: []string{tmpDir.Path("deployment-app.yaml"), tmpDir.Path("deployment-web.yaml")}})
   417  		t.RequireNoError(err)
   418  
   419  		// Deploy one manifest
   420  		err = deployer.Deploy(context.Background(), ioutil.Discard, []graph.Artifact{
   421  			{ImageName: "leeroy-web", Tag: "leeroy-web:v1"},
   422  			{ImageName: "leeroy-app", Tag: "leeroy-app:v1"},
   423  		})
   424  		t.CheckNoError(err)
   425  
   426  		// Deploy one manifest since only one image is updated
   427  		err = deployer.Deploy(context.Background(), ioutil.Discard, []graph.Artifact{
   428  			{ImageName: "leeroy-web", Tag: "leeroy-web:v1"},
   429  			{ImageName: "leeroy-app", Tag: "leeroy-app:v2"},
   430  		})
   431  		t.CheckNoError(err)
   432  
   433  		// Deploy zero manifest since no image is updated
   434  		err = deployer.Deploy(context.Background(), ioutil.Discard, []graph.Artifact{
   435  			{ImageName: "leeroy-web", Tag: "leeroy-web:v1"},
   436  			{ImageName: "leeroy-app", Tag: "leeroy-app:v2"},
   437  		})
   438  		t.CheckNoError(err)
   439  	})
   440  }
   441  
   442  func TestKubectlWaitForDeletions(t *testing.T) {
   443  	testutil.Run(t, "", func(t *testutil.T) {
   444  		t.Override(&client.Client, deployutil.MockK8sClient)
   445  		tmpDir := t.NewTempDir().Write("deployment-web.yaml", DeploymentWebYAML)
   446  
   447  		t.Override(&util.DefaultExecCommand, testutil.
   448  			CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   449  			AndRunOut("kubectl --context kubecontext create --dry-run -oyaml -f "+tmpDir.Path("deployment-web.yaml"), DeploymentWebYAML).
   450  			AndRunInputOut("kubectl --context kubecontext get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, `{
   451  				"items":[
   452  					{"metadata":{"deletionTimestamp":"2020-07-24T12:40:32Z","name":"leeroy-web"}},
   453  					{"metadata":{"deletionTimestamp":"2020-07-24T12:40:32Z","name":"leeroy-app"}},
   454  					{"metadata":{"name":"leeroy-front"}}
   455  				]
   456  			}`).
   457  			AndRunInputOut("kubectl --context kubecontext get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, `{
   458  				"items":[
   459  					{"metadata":{"deletionTimestamp":"2020-07-24T12:40:32Z","name":"leeroy-web"}},
   460  					{"metadata":{"deletionTimestamp":"2020-07-24T12:40:32Z","name":"leeroy-app"}},
   461  					{"metadata":{"name":"leeroy-front"}}
   462  				]
   463  			}`).
   464  			AndRunInputOut("kubectl --context kubecontext get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, `{
   465  				"items":[
   466  					{"metadata":{"deletionTimestamp":"2020-07-24T12:40:32Z","name":"leeroy-web"}},
   467  					{"metadata":{"name":"leeroy-front"}}
   468  				]
   469  			}`).
   470  			AndRunInputOut("kubectl --context kubecontext get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, "").
   471  			AndRunInput("kubectl --context kubecontext apply -f -", DeploymentWebYAMLv1),
   472  		)
   473  
   474  		deployer, err := NewDeployer(&kubectlConfig{
   475  			workingDir: tmpDir.Root(),
   476  			waitForDeletions: config.WaitForDeletions{
   477  				Enabled: true,
   478  				Delay:   0 * time.Millisecond,
   479  				Max:     10 * time.Second,
   480  			},
   481  		}, &label.DefaultLabeller{}, &latest.KubectlDeploy{Manifests: []string{tmpDir.Path("deployment-web.yaml")}})
   482  		t.RequireNoError(err)
   483  
   484  		var out bytes.Buffer
   485  		err = deployer.Deploy(context.Background(), &out, []graph.Artifact{
   486  			{ImageName: "leeroy-web", Tag: "leeroy-web:v1"},
   487  		})
   488  
   489  		t.CheckNoError(err)
   490  		t.CheckDeepEqual(` - 2 resources are marked for deletion, waiting for completion: "leeroy-web", "leeroy-app"
   491   - "leeroy-web" is marked for deletion, waiting for completion
   492  `, out.String())
   493  	})
   494  }
   495  
   496  func TestKubectlWaitForDeletionsFails(t *testing.T) {
   497  	testutil.Run(t, "", func(t *testutil.T) {
   498  		tmpDir := t.NewTempDir().Write("deployment-web.yaml", DeploymentWebYAML)
   499  
   500  		t.Override(&client.Client, deployutil.MockK8sClient)
   501  		t.Override(&util.DefaultExecCommand, testutil.
   502  			CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   503  			AndRunOut("kubectl --context kubecontext create --dry-run -oyaml -f "+tmpDir.Path("deployment-web.yaml"), DeploymentWebYAML).
   504  			AndRunInputOut("kubectl --context kubecontext get -f - --ignore-not-found -ojson", DeploymentWebYAMLv1, `{
   505  				"items":[
   506  					{"metadata":{"deletionTimestamp":"2020-07-24T12:40:32Z","name":"leeroy-web"}},
   507  					{"metadata":{"deletionTimestamp":"2020-07-24T12:40:32Z","name":"leeroy-app"}}
   508  				]
   509  			}`),
   510  		)
   511  
   512  		deployer, err := NewDeployer(&kubectlConfig{
   513  			workingDir: tmpDir.Root(),
   514  			waitForDeletions: config.WaitForDeletions{
   515  				Enabled: true,
   516  				Delay:   10 * time.Second,
   517  				Max:     100 * time.Millisecond,
   518  			},
   519  		}, &label.DefaultLabeller{}, &latest.KubectlDeploy{Manifests: []string{tmpDir.Path("deployment-web.yaml")}})
   520  		t.RequireNoError(err)
   521  
   522  		err = deployer.Deploy(context.Background(), ioutil.Discard, []graph.Artifact{
   523  			{ImageName: "leeroy-web", Tag: "leeroy-web:v1"},
   524  		})
   525  
   526  		t.CheckErrorContains(`2 resources failed to complete their deletion before a new deployment: "leeroy-web", "leeroy-app"`, err)
   527  	})
   528  }
   529  
   530  func TestDependencies(t *testing.T) {
   531  	tests := []struct {
   532  		description string
   533  		manifests   []string
   534  		expected    []string
   535  	}{
   536  		{
   537  			description: "no manifest",
   538  			manifests:   []string(nil),
   539  			expected:    []string(nil),
   540  		},
   541  		{
   542  			description: "missing manifest file",
   543  			manifests:   []string{"missing.yaml"},
   544  			expected:    []string(nil),
   545  		},
   546  		{
   547  			description: "ignore non-manifest",
   548  			manifests:   []string{"*.ignored"},
   549  			expected:    []string(nil),
   550  		},
   551  		{
   552  			description: "single manifest",
   553  			manifests:   []string{"deployment.yaml"},
   554  			expected:    []string{"deployment.yaml"},
   555  		},
   556  		{
   557  			description: "keep manifests order",
   558  			manifests:   []string{"01_name.yaml", "00_service.yaml"},
   559  			expected:    []string{"01_name.yaml", "00_service.yaml"},
   560  		},
   561  		{
   562  			description: "sort children",
   563  			manifests:   []string{"01/*.yaml", "00/*.yaml"},
   564  			expected:    []string{filepath.Join("01", "a.yaml"), filepath.Join("01", "b.yaml"), filepath.Join("00", "a.yaml"), filepath.Join("00", "b.yaml")},
   565  		},
   566  		{
   567  			description: "http manifest",
   568  			manifests:   []string{"deployment.yaml", "http://remote.yaml"},
   569  			expected:    []string{"deployment.yaml"},
   570  		},
   571  	}
   572  	for _, test := range tests {
   573  		testutil.Run(t, test.description, func(t *testutil.T) {
   574  			t.NewTempDir().
   575  				Touch("deployment.yaml", "01_name.yaml", "00_service.yaml", "empty.ignored").
   576  				Touch("01/a.yaml", "01/b.yaml").
   577  				Touch("00/b.yaml", "00/a.yaml").
   578  				Chdir()
   579  
   580  			k, err := NewDeployer(&kubectlConfig{}, &label.DefaultLabeller{}, &latest.KubectlDeploy{Manifests: test.manifests})
   581  			t.RequireNoError(err)
   582  
   583  			dependencies, err := k.Dependencies()
   584  
   585  			t.CheckNoError(err)
   586  			t.CheckDeepEqual(test.expected, dependencies)
   587  		})
   588  	}
   589  }
   590  
   591  func TestKubectlRender(t *testing.T) {
   592  	tests := []struct {
   593  		description string
   594  		builds      []graph.Artifact
   595  		input       string
   596  		expected    string
   597  	}{
   598  		{
   599  			description: "normal render",
   600  			builds: []graph.Artifact{
   601  				{
   602  					ImageName: "gcr.io/k8s-skaffold/skaffold",
   603  					Tag:       "gcr.io/k8s-skaffold/skaffold:test",
   604  				},
   605  			},
   606  			input: `apiVersion: v1
   607  kind: Pod
   608  metadata:
   609    namespace: default
   610  spec:
   611    containers:
   612    - image: gcr.io/k8s-skaffold/skaffold
   613      name: skaffold
   614  `,
   615  			expected: `apiVersion: v1
   616  kind: Pod
   617  metadata:
   618    namespace: default
   619  spec:
   620    containers:
   621    - image: gcr.io/k8s-skaffold/skaffold:test
   622      name: skaffold
   623  `,
   624  		},
   625  		{
   626  			description: "two artifacts",
   627  			builds: []graph.Artifact{
   628  				{
   629  					ImageName: "gcr.io/project/image1",
   630  					Tag:       "gcr.io/project/image1:tag1",
   631  				},
   632  				{
   633  					ImageName: "gcr.io/project/image2",
   634  					Tag:       "gcr.io/project/image2:tag2",
   635  				},
   636  			},
   637  			input: `apiVersion: v1
   638  kind: Pod
   639  metadata:
   640    namespace: default
   641  spec:
   642    containers:
   643    - image: gcr.io/project/image1
   644      name: image1
   645    - image: gcr.io/project/image2
   646      name: image2
   647  `,
   648  			expected: `apiVersion: v1
   649  kind: Pod
   650  metadata:
   651    namespace: default
   652  spec:
   653    containers:
   654    - image: gcr.io/project/image1:tag1
   655      name: image1
   656    - image: gcr.io/project/image2:tag2
   657      name: image2
   658  `,
   659  		},
   660  		{
   661  			description: "no artifacts",
   662  			builds:      nil,
   663  			input: `apiVersion: v1
   664  kind: Pod
   665  metadata:
   666    namespace: default
   667  spec:
   668    containers:
   669    - image: image1:tag1
   670      name: image1
   671    - image: image2:tag2
   672      name: image2
   673  `,
   674  			expected: `apiVersion: v1
   675  kind: Pod
   676  metadata:
   677    namespace: default
   678  spec:
   679    containers:
   680    - image: gcr.io/project/image1:tag1
   681      name: image1
   682    - image: gcr.io/project/image2:tag2
   683      name: image2
   684  `,
   685  		},
   686  	}
   687  	for _, test := range tests {
   688  		testutil.Run(t, test.description, func(t *testutil.T) {
   689  			tmpDir := t.NewTempDir().Write("deployment.yaml", test.input)
   690  			t.Override(&util.DefaultExecCommand, testutil.
   691  				CmdRunOut("kubectl version --client -ojson", KubectlVersion112).
   692  				AndRunOut("kubectl --context kubecontext create --dry-run -oyaml -f "+tmpDir.Path("deployment.yaml"), test.input))
   693  			deployer, err := NewDeployer(&kubectlConfig{
   694  				workingDir:     ".",
   695  				defaultRepo:    "gcr.io/project",
   696  				multiLevelRepo: util.BoolPtr(true),
   697  			}, &label.DefaultLabeller{}, &latest.KubectlDeploy{
   698  				Manifests: []string{tmpDir.Path("deployment.yaml")},
   699  			})
   700  			t.RequireNoError(err)
   701  			var b bytes.Buffer
   702  			err = deployer.Render(context.Background(), &b, test.builds, true, "")
   703  			t.CheckNoError(err)
   704  			t.CheckDeepEqual(test.expected, b.String(), testutil.YamlObj(t.T))
   705  		})
   706  	}
   707  }
   708  
   709  func TestGCSManifests(t *testing.T) {
   710  	tests := []struct {
   711  		description string
   712  		kubectl     latest.KubectlDeploy
   713  		commands    util.Command
   714  		shouldErr   bool
   715  		skipRender  bool
   716  	}{
   717  		{
   718  			description: "manifest from GCS",
   719  			kubectl: latest.KubectlDeploy{
   720  				Manifests: []string{"gs://dev/deployment.yaml"},
   721  			},
   722  			commands: testutil.
   723  				CmdRunOut(fmt.Sprintf("gsutil cp -r %s %s", "gs://dev/deployment.yaml", manifest.ManifestTmpDir), "log").
   724  				AndRunOut("kubectl version --client -ojson", KubectlVersion112).
   725  				AndRunOut("kubectl --context kubecontext --namespace testNamespace create --dry-run -oyaml -f "+filepath.Join(manifest.ManifestTmpDir, "deployment.yaml"), DeploymentWebYAML).
   726  				AndRun("kubectl --context kubecontext --namespace testNamespace apply -f -"),
   727  			skipRender: true,
   728  		}}
   729  	for _, test := range tests {
   730  		testutil.Run(t, test.description, func(t *testutil.T) {
   731  			t.Override(&client.Client, deployutil.MockK8sClient)
   732  			t.Override(&util.DefaultExecCommand, test.commands)
   733  			if err := os.MkdirAll(manifest.ManifestTmpDir, os.ModePerm); err != nil {
   734  				t.Fatal(err)
   735  			}
   736  			if err := ioutil.WriteFile(manifest.ManifestTmpDir+"/deployment.yaml", []byte(DeploymentWebYAML), os.ModePerm); err != nil {
   737  				t.Fatal(err)
   738  			}
   739  			k, err := NewDeployer(&kubectlConfig{
   740  				workingDir: ".",
   741  				skipRender: test.skipRender,
   742  				RunContext: runcontext.RunContext{Opts: config.SkaffoldOptions{Namespace: TestNamespace}},
   743  			}, &label.DefaultLabeller{}, &test.kubectl)
   744  			t.RequireNoError(err)
   745  
   746  			err = k.Deploy(context.Background(), ioutil.Discard, nil)
   747  
   748  			t.CheckError(test.shouldErr, err)
   749  		})
   750  	}
   751  }
   752  
   753  func TestHasRunnableHooks(t *testing.T) {
   754  	tests := []struct {
   755  		description string
   756  		cfg         latest.KubectlDeploy
   757  		expected    bool
   758  	}{
   759  		{
   760  			description: "no hooks defined",
   761  			cfg:         latest.KubectlDeploy{},
   762  		},
   763  		{
   764  			description: "has pre-deploy hook defined",
   765  			cfg: latest.KubectlDeploy{
   766  				LifecycleHooks: latest.DeployHooks{PreHooks: []latest.DeployHookItem{{}}},
   767  			},
   768  			expected: true,
   769  		},
   770  		{
   771  			description: "has post-deploy hook defined",
   772  			cfg: latest.KubectlDeploy{
   773  				LifecycleHooks: latest.DeployHooks{PostHooks: []latest.DeployHookItem{{}}},
   774  			},
   775  			expected: true,
   776  		},
   777  	}
   778  	for _, test := range tests {
   779  		testutil.Run(t, test.description, func(t *testutil.T) {
   780  			k, err := NewDeployer(&kubectlConfig{}, &label.DefaultLabeller{}, &test.cfg)
   781  			t.RequireNoError(err)
   782  			actual := k.HasRunnableHooks()
   783  			t.CheckDeepEqual(test.expected, actual)
   784  		})
   785  	}
   786  }
   787  
   788  type kubectlConfig struct {
   789  	runcontext.RunContext // Embedded to provide the default values.
   790  	workingDir            string
   791  	defaultRepo           string
   792  	multiLevelRepo        *bool
   793  	skipRender            bool
   794  	force                 bool
   795  	waitForDeletions      config.WaitForDeletions
   796  }
   797  
   798  func (c *kubectlConfig) GetKubeContext() string                              { return "kubecontext" }
   799  func (c *kubectlConfig) GetKubeNamespace() string                            { return c.Opts.Namespace }
   800  func (c *kubectlConfig) WorkingDir() string                                  { return c.workingDir }
   801  func (c *kubectlConfig) SkipRender() bool                                    { return c.skipRender }
   802  func (c *kubectlConfig) ForceDeploy() bool                                   { return c.force }
   803  func (c *kubectlConfig) DefaultRepo() *string                                { return &c.defaultRepo }
   804  func (c *kubectlConfig) MultiLevelRepo() *bool                               { return c.multiLevelRepo }
   805  func (c *kubectlConfig) WaitForDeletions() config.WaitForDeletions           { return c.waitForDeletions }
   806  func (c *kubectlConfig) PortForwardResources() []*latest.PortForwardResource { return nil }