github.com/tilt-dev/tilt@v0.36.0/internal/cli/down_test.go (about)

     1  package cli
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/pkg/errors"
     9  	"github.com/spf13/afero"
    10  	"github.com/spf13/cobra"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/tilt-dev/tilt/internal/analytics"
    15  	"github.com/tilt-dev/tilt/internal/dockercompose"
    16  	"github.com/tilt-dev/tilt/internal/k8s"
    17  	"github.com/tilt-dev/tilt/internal/k8s/kubeconfig"
    18  	"github.com/tilt-dev/tilt/internal/k8s/testyaml"
    19  	"github.com/tilt-dev/tilt/internal/localexec"
    20  	"github.com/tilt-dev/tilt/internal/testutils"
    21  	"github.com/tilt-dev/tilt/internal/tiltfile"
    22  	"github.com/tilt-dev/tilt/internal/xdg"
    23  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    24  	"github.com/tilt-dev/tilt/pkg/model"
    25  )
    26  
    27  func TestDownK8sYAML(t *testing.T) {
    28  	f := newDownFixture(t)
    29  
    30  	f.tfl.Result = newTiltfileLoadResult(newK8sManifest())
    31  	err := f.cmd.down(f.ctx, f.deps, nil)
    32  	assert.NoError(t, err)
    33  	assert.Contains(t, f.kCli.DeletedYaml, "sancho")
    34  }
    35  
    36  func TestDownIgnoresDisabled(t *testing.T) {
    37  	f := newDownFixture(t)
    38  
    39  	tlr := newTiltfileLoadResult(
    40  		newK8sConfigMapManifest("foo"),
    41  		newK8sConfigMapManifest("bar"),
    42  		newK8sConfigMapManifest("baz"))
    43  	tlr.EnabledManifests = []model.ManifestName{"bar"}
    44  	f.tfl.Result = tlr
    45  	err := f.cmd.down(f.ctx, f.deps, nil)
    46  	require.NoError(t, err)
    47  	require.NotContains(t, f.kCli.DeletedYaml, "foo")
    48  	require.Contains(t, f.kCli.DeletedYaml, "bar")
    49  	require.NotContains(t, f.kCli.DeletedYaml, "baz")
    50  }
    51  
    52  func TestDownPreservesEntitiesWithKeepLabel(t *testing.T) {
    53  	f := newDownFixture(t)
    54  
    55  	f.tfl.Result = newTiltfileLoadResult(newK8sPVCManifest("foo", "keep"), newK8sPVCManifest("bar", "delete"))
    56  	err := f.cmd.down(f.ctx, f.deps, nil)
    57  	require.NoError(t, err)
    58  	require.Contains(t, f.kCli.DeletedYaml, "bar")
    59  	require.NotContains(t, f.kCli.DeletedYaml, "foo")
    60  }
    61  
    62  func TestDownPreservesNamespacesByDefault(t *testing.T) {
    63  	f := newDownFixture(t)
    64  
    65  	f.tfl.Result = newTiltfileLoadResult(newK8sManifest(), newK8sNamespaceManifest("foo"), newK8sNamespaceManifest("bar"))
    66  	err := f.cmd.down(f.ctx, f.deps, nil)
    67  	require.NoError(t, err)
    68  	require.Contains(t, f.kCli.DeletedYaml, "sancho")
    69  	for _, ns := range []string{"foo", "bar"} {
    70  		require.NotContains(t, f.kCli.DeletedYaml, ns)
    71  	}
    72  }
    73  
    74  func TestDownDeletesNamespacesIfSpecified(t *testing.T) {
    75  	f := newDownFixture(t)
    76  
    77  	f.tfl.Result = newTiltfileLoadResult(
    78  		newK8sManifest(), newK8sNamespaceManifest("foo"), newK8sNamespaceManifest("bar"))
    79  	f.cmd.deleteNamespaces = true
    80  	err := f.cmd.down(f.ctx, f.deps, nil)
    81  	require.NoError(t, err)
    82  	for _, ns := range []string{"sancho", "foo", "bar"} {
    83  		require.Contains(t, f.kCli.DeletedYaml, ns)
    84  	}
    85  }
    86  
    87  func TestDownDeletesManifestsInReverseOrder(t *testing.T) {
    88  	f := newDownFixture(t)
    89  
    90  	f.tfl.Result = newTiltfileLoadResult(newK8sNamespaceManifest("foo"), newK8sManifest())
    91  	f.cmd.deleteNamespaces = true
    92  	err := f.cmd.down(f.ctx, f.deps, nil)
    93  	require.NoError(t, err)
    94  	require.Regexp(t, "(?s)name: sancho.*name: foo", f.kCli.DeletedYaml) // namespace comes after deployment
    95  }
    96  
    97  func TestDownDeletesEntitiesInReverseOrder(t *testing.T) {
    98  	f := newDownFixture(t)
    99  
   100  	f.tfl.Result = newTiltfileLoadResult(newK8sMultiEntityManifest())
   101  	f.cmd.deleteNamespaces = true
   102  	err := f.cmd.down(f.ctx, f.deps, nil)
   103  	require.NoError(t, err)
   104  
   105  	entities, err := k8s.ParseYAMLFromString(f.kCli.DeletedYaml)
   106  	require.NoError(t, err)
   107  	require.Equal(t, 2, len(entities))
   108  	require.Equal(t, "Secret", entities[0].GVK().Kind)
   109  	require.Equal(t, "Namespace", entities[1].GVK().Kind)
   110  }
   111  
   112  func TestDownDeletesInDependentOrder(t *testing.T) {
   113  	f := newDownFixture(t)
   114  
   115  	f.tfl.Result = newTiltfileLoadResult(newK8sDependentManifests()...)
   116  	err := f.cmd.down(f.ctx, f.deps, nil)
   117  	require.NoError(t, err)
   118  
   119  	entities, err := k8s.ParseYAMLFromString(f.kCli.DeletedYaml)
   120  	require.NoError(t, err)
   121  	require.Equal(t, 6, len(entities))
   122  
   123  	var names []string
   124  
   125  	for _, entity := range entities {
   126  		names = append(names, entity.Meta().GetName())
   127  	}
   128  
   129  	// For each name with dependencies, assert that its dependencies are deleted after it
   130  	for i, name := range names {
   131  		switch name {
   132  		case "mixed_dependent":
   133  			require.Contains(t, names[i:], "no_dependencies")
   134  			require.Contains(t, names[i:], "direct_dependent_1")
   135  			require.Contains(t, names[i:], "indirect_dependent_2")
   136  		case "indirect_dependent_1":
   137  			require.Contains(t, names[i:], "direct_dependent_2")
   138  		case "indirect_dependent_2":
   139  			require.Contains(t, names[i:], "direct_dependent_1")
   140  		case "direct_dependent_1":
   141  			require.Contains(t, names[i:], "no_dependencies")
   142  		case "direct_dependent_2":
   143  			require.Contains(t, names[i:], "no_dependencies")
   144  		}
   145  	}
   146  }
   147  
   148  func TestDownDeletesInDependentOrderReversed(t *testing.T) {
   149  	f := newDownFixture(t)
   150  
   151  	manifests := newK8sDependentManifests()
   152  
   153  	// Reverse the list of manifests to ensure delete order is dependent on manifest order
   154  	for i := 0; i < len(manifests)/2; i++ {
   155  		manifests[i], manifests[len(manifests)-i-1] = manifests[len(manifests)-i-1], manifests[i]
   156  	}
   157  
   158  	f.tfl.Result = newTiltfileLoadResult(manifests...)
   159  	err := f.cmd.down(f.ctx, f.deps, nil)
   160  	require.NoError(t, err)
   161  
   162  	entities, err := k8s.ParseYAMLFromString(f.kCli.DeletedYaml)
   163  	require.NoError(t, err)
   164  	require.Equal(t, 6, len(entities))
   165  
   166  	var names []string
   167  
   168  	for _, entity := range entities {
   169  		names = append(names, entity.Meta().GetName())
   170  	}
   171  
   172  	// For each name with dependencies, assert that its dependencies are deleted after it
   173  	for i, name := range names {
   174  		switch name {
   175  		case "mixed_dependent":
   176  			require.Contains(t, names[i:], "no_dependencies")
   177  			require.Contains(t, names[i:], "direct_dependent_1")
   178  			require.Contains(t, names[i:], "indirect_dependent_2")
   179  		case "indirect_dependent_1":
   180  			require.Contains(t, names[i:], "direct_dependent_2")
   181  		case "indirect_dependent_2":
   182  			require.Contains(t, names[i:], "direct_dependent_1")
   183  		case "direct_dependent_1":
   184  			require.Contains(t, names[i:], "no_dependencies")
   185  		case "direct_dependent_2":
   186  			require.Contains(t, names[i:], "no_dependencies")
   187  		}
   188  	}
   189  }
   190  
   191  func TestDownDeletesCyclicDependencies(t *testing.T) {
   192  	f := newDownFixture(t)
   193  
   194  	manifests := newK8sCyclicManifest()
   195  
   196  	f.tfl.Result = newTiltfileLoadResult(manifests...)
   197  	err := f.cmd.down(f.ctx, f.deps, nil)
   198  	require.NoError(t, err)
   199  
   200  	entities, err := k8s.ParseYAMLFromString(f.kCli.DeletedYaml)
   201  	require.NoError(t, err)
   202  
   203  	require.Equal(t, 2, len(entities))
   204  }
   205  
   206  func TestDownDeletesWithInvalidDependency(t *testing.T) {
   207  	f := newDownFixture(t)
   208  
   209  	manifests := newK8sInvalidDependencyManifests()
   210  
   211  	f.tfl.Result = newTiltfileLoadResult(manifests...)
   212  	err := f.cmd.down(f.ctx, f.deps, nil)
   213  	require.NoError(t, err)
   214  	require.Contains(t, f.kCli.DeletedYaml, "missing-dep")
   215  }
   216  
   217  func TestDownK8sFails(t *testing.T) {
   218  	f := newDownFixture(t)
   219  
   220  	f.tfl.Result = newTiltfileLoadResult(newK8sManifest())
   221  	f.kCli.DeleteError = fmt.Errorf("GARBLEGARBLE")
   222  	err := f.cmd.down(f.ctx, f.deps, nil)
   223  	if assert.Error(t, err) {
   224  		assert.Contains(t, err.Error(), "GARBLEGARBLE")
   225  	}
   226  }
   227  
   228  func TestDownK8sDeleteCmd(t *testing.T) {
   229  	f := newDownFixture(t)
   230  
   231  	kaSpec := v1alpha1.KubernetesApplySpec{
   232  		ApplyCmd:  &v1alpha1.KubernetesApplyCmd{Args: []string{"custom-deploy-cmd"}},
   233  		DeleteCmd: &v1alpha1.KubernetesApplyCmd{Args: []string{"custom-delete-cmd"}},
   234  	}
   235  
   236  	kt, err := k8s.NewTarget("fe", kaSpec, model.PodReadinessIgnore, nil)
   237  	require.NoError(t, err, "Failed to make KubernetesTarget")
   238  	m := model.Manifest{Name: "fe"}.WithDeployTarget(kt)
   239  
   240  	f.tfl.Result = newTiltfileLoadResult(m)
   241  	err = f.cmd.down(f.ctx, f.deps, nil)
   242  	require.NoError(t, err)
   243  
   244  	calls := f.execer.Calls()
   245  	if assert.Len(t, calls, 1, "Should have been exactly 1 exec call") {
   246  		assert.Equal(t, []string{"custom-delete-cmd"}, calls[0].Cmd.Argv)
   247  		if assert.Len(t, calls[0].Cmd.Env, 1, "Should have been exactly 1 env var") {
   248  			assert.Contains(t, calls[0].Cmd.Env[0], "KUBECONFIG=")
   249  		}
   250  	}
   251  }
   252  
   253  func TestDownK8sDeleteCmd_Error(t *testing.T) {
   254  	f := newDownFixture(t)
   255  
   256  	f.execer.RegisterCommand("custom-delete-cmd", 321, "", "delete failed")
   257  
   258  	kaSpec := v1alpha1.KubernetesApplySpec{
   259  		ApplyCmd:  &v1alpha1.KubernetesApplyCmd{Args: []string{"custom-deploy-cmd"}},
   260  		DeleteCmd: &v1alpha1.KubernetesApplyCmd{Args: []string{"custom-delete-cmd"}},
   261  	}
   262  
   263  	kt, err := k8s.NewTarget("fe", kaSpec, model.PodReadinessIgnore, nil)
   264  	require.NoError(t, err, "Failed to make KubernetesTarget")
   265  	m := model.Manifest{Name: "fe"}.WithDeployTarget(kt)
   266  
   267  	f.tfl.Result = newTiltfileLoadResult(m)
   268  	err = f.cmd.down(f.ctx, f.deps, nil)
   269  	assert.EqualError(t, err, "Deleting k8s entities for cmd: custom-delete-cmd: exit status 321")
   270  
   271  	calls := f.execer.Calls()
   272  	if assert.Len(t, calls, 1, "Should have been exactly 1 exec call") {
   273  		assert.Equal(t, []string{"custom-delete-cmd"}, calls[0].Cmd.Argv)
   274  	}
   275  }
   276  
   277  func TestDownDockerComposeWithExplodingKubeConfig(t *testing.T) {
   278  	f := newDownFixture(t)
   279  
   280  	f.deps.kClient = k8s.NewExplodingClient(errors.New("could not set up kubernetes client"))
   281  	f.tfl.Result = newTiltfileLoadResult(newDCManifest())
   282  	err := f.cmd.down(f.ctx, f.deps, nil)
   283  	assert.NoError(t, err)
   284  }
   285  
   286  func TestDownDCFails(t *testing.T) {
   287  	f := newDownFixture(t)
   288  
   289  	f.tfl.Result = newTiltfileLoadResult(newDCManifest())
   290  	f.dcc.DownError = fmt.Errorf("GARBLEGARBLE")
   291  	err := f.cmd.down(f.ctx, f.deps, nil)
   292  	if assert.Error(t, err) {
   293  		assert.Contains(t, err.Error(), "GARBLEGARBLE")
   294  	}
   295  }
   296  
   297  func TestDownArgs(t *testing.T) {
   298  	f := newDownFixture(t)
   299  
   300  	cmd := f.cmd.register()
   301  	cmd.SetArgs([]string{"foo", "bar"})
   302  	cmd.Run = func(cmd *cobra.Command, args []string) {
   303  		ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   304  		err := f.cmd.run(ctx, args)
   305  		require.NoError(t, err)
   306  	}
   307  	err := cmd.Execute()
   308  	require.NoError(t, err)
   309  
   310  	require.Equal(t, []string{"foo", "bar"}, f.tfl.PassedArgs())
   311  }
   312  
   313  func newK8sManifest() model.Manifest {
   314  	return model.Manifest{Name: "fe"}.WithDeployTarget(k8s.MustTarget("fe", testyaml.SanchoYAML))
   315  }
   316  
   317  func newK8sDependentManifests() []model.Manifest {
   318  	yamlTemplate := `
   319  apiVersion: v1
   320  kind: Secret
   321  metadata:
   322    name: %s
   323  data:
   324    mySecret: blah
   325  `
   326  
   327  	return []model.Manifest{
   328  		model.Manifest{
   329  			Name: "no_dependencies",
   330  		}.WithDeployTarget(k8s.MustTarget("no_dependencies", fmt.Sprintf(yamlTemplate, "no_dependencies"))),
   331  		model.Manifest{
   332  			Name:                 "direct_dependent_1",
   333  			ResourceDependencies: []model.ManifestName{"no_dependencies"},
   334  		}.WithDeployTarget(k8s.MustTarget("direct_dependent_1", fmt.Sprintf(yamlTemplate, "direct_dependent_1"))),
   335  		model.Manifest{
   336  			Name:                 "direct_dependent_2",
   337  			ResourceDependencies: []model.ManifestName{"no_dependencies"},
   338  		}.WithDeployTarget(k8s.MustTarget("direct_dependent_2", fmt.Sprintf(yamlTemplate, "direct_dependent_2"))),
   339  		model.Manifest{
   340  			Name:                 "indirect_dependent_1",
   341  			ResourceDependencies: []model.ManifestName{"direct_dependent_2"},
   342  		}.WithDeployTarget(k8s.MustTarget("indirect_dependent_1", fmt.Sprintf(yamlTemplate, "indirect_dependent_1"))),
   343  		model.Manifest{
   344  			Name:                 "indirect_dependent_2",
   345  			ResourceDependencies: []model.ManifestName{"direct_dependent_1"},
   346  		}.WithDeployTarget(k8s.MustTarget("indirect_dependent_2", fmt.Sprintf(yamlTemplate, "indirect_dependent_2"))),
   347  		model.Manifest{
   348  			Name:                 "mixed_dependent",
   349  			ResourceDependencies: []model.ManifestName{"no_dependencies", "direct_dependent_1", "indirect_dependent_2"},
   350  		}.WithDeployTarget(k8s.MustTarget("mixed_dependent", fmt.Sprintf(yamlTemplate, "mixed_dependent"))),
   351  	}
   352  }
   353  
   354  func newK8sCyclicManifest() []model.Manifest {
   355  	yamlTemplate := `
   356  apiVersion: v1
   357  kind: Secret
   358  metadata:
   359    name: %s
   360  data:
   361    mySecret: blah
   362  `
   363  
   364  	return []model.Manifest{
   365  		model.Manifest{
   366  			Name:                 "dep_1",
   367  			ResourceDependencies: []model.ManifestName{"dep_2"},
   368  		}.WithDeployTarget(k8s.MustTarget("dep_1", fmt.Sprintf(yamlTemplate, "dep_1"))),
   369  		model.Manifest{
   370  			Name:                 "dep_2",
   371  			ResourceDependencies: []model.ManifestName{"dep_1"},
   372  		}.WithDeployTarget(k8s.MustTarget("dep_2", fmt.Sprintf(yamlTemplate, "dep_2"))),
   373  	}
   374  }
   375  
   376  func newK8sInvalidDependencyManifests() []model.Manifest {
   377  	yaml := `
   378  apiVersion: v1
   379  kind: Secret
   380  metadata:
   381    name: missing-dep
   382  data:
   383    mySecret: blah
   384  `
   385  
   386  	return []model.Manifest{
   387  		model.Manifest{
   388  			Name:                 "missing-dep",
   389  			ResourceDependencies: []model.ManifestName{"nonexistent"},
   390  		}.WithDeployTarget(k8s.MustTarget("missing-dep", yaml)),
   391  	}
   392  
   393  }
   394  
   395  func newDCManifest() model.Manifest {
   396  	return model.Manifest{Name: "fe"}.WithDeployTarget(model.DockerComposeTarget{
   397  		Name: "fe",
   398  		Spec: v1alpha1.DockerComposeServiceSpec{
   399  			Service: "fe",
   400  			Project: v1alpha1.DockerComposeProject{
   401  				ConfigPaths: []string{"dc.yaml"},
   402  			},
   403  		},
   404  	})
   405  }
   406  
   407  func newK8sMultiEntityManifest() model.Manifest {
   408  	yaml := `
   409  apiVersion: v1
   410  kind: Namespace
   411  metadata:
   412    name: test-namespace
   413  ---
   414  apiVersion: v1
   415  kind: Secret
   416  metadata:
   417    name: test-secret
   418    namespace: test-namespace
   419  data:
   420    testSecret: blah
   421  `
   422  
   423  	return model.Manifest{Name: "test-secret"}.WithDeployTarget(k8s.MustTarget("test-secret", yaml))
   424  }
   425  
   426  func newK8sNamespaceManifest(name string) model.Manifest {
   427  	yaml := fmt.Sprintf(`
   428  apiVersion: v1
   429  kind: Namespace
   430  metadata:
   431    name: %s
   432  spec: {}
   433  status: {}`, name)
   434  	return model.Manifest{Name: model.ManifestName(name)}.WithDeployTarget(model.NewK8sTargetForTesting(yaml))
   435  }
   436  
   437  func newK8sConfigMapManifest(name string) model.Manifest {
   438  	yaml := fmt.Sprintf(`
   439  apiVersion: v1
   440  kind: ConfigMap
   441  metadata:
   442    name: %s
   443  data:
   444    hello: world`, name)
   445  	return model.Manifest{Name: model.ManifestName(name)}.WithDeployTarget(model.NewK8sTargetForTesting(yaml))
   446  }
   447  
   448  func newK8sPVCManifest(name string, downPolicy string) model.Manifest {
   449  	yaml := fmt.Sprintf(`
   450  apiVersion: v1
   451  kind: PersistentVolumeClaim
   452  metadata:
   453    name: %s
   454    annotations:
   455      tilt.dev/down-policy: %s
   456  spec: {}
   457  status: {}`, name, downPolicy)
   458  	return model.Manifest{Name: model.ManifestName(name)}.WithDeployTarget(model.NewK8sTargetForTesting(yaml))
   459  }
   460  
   461  type downFixture struct {
   462  	t      *testing.T
   463  	ctx    context.Context
   464  	cancel func()
   465  	cmd    *downCmd
   466  	deps   DownDeps
   467  	tfl    *tiltfile.FakeTiltfileLoader
   468  	dcc    *dockercompose.FakeDCClient
   469  	kCli   *k8s.FakeK8sClient
   470  	execer *localexec.FakeExecer
   471  }
   472  
   473  func newDownFixture(t *testing.T) downFixture {
   474  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   475  	ctx, cancel := context.WithCancel(ctx)
   476  	tfl := tiltfile.NewFakeTiltfileLoader()
   477  	dcc := dockercompose.NewFakeDockerComposeClient(t, ctx)
   478  	kCli := k8s.NewFakeK8sClient(t)
   479  	execer := localexec.NewFakeExecer(t)
   480  	fs := afero.NewMemMapFs()
   481  	xdgBase := xdg.NewFakeBase(t.TempDir(), fs)
   482  	writer := kubeconfig.NewWriter(xdgBase, fs, model.APIServerName("test"))
   483  
   484  	downDeps := DownDeps{
   485  		tfl:              tfl,
   486  		dcClient:         dcc,
   487  		kClient:          kCli,
   488  		execer:           execer,
   489  		kubeconfigWriter: writer,
   490  		fs:               fs,
   491  	}
   492  	cmd := &downCmd{downDepsProvider: func(ctx context.Context, tiltAnalytics *analytics.TiltAnalytics, subcommand model.TiltSubcommand) (deps DownDeps, err error) {
   493  		return downDeps, nil
   494  	}}
   495  	ret := downFixture{
   496  		t:      t,
   497  		ctx:    ctx,
   498  		cancel: cancel,
   499  		cmd:    cmd,
   500  		deps:   downDeps,
   501  		tfl:    tfl,
   502  		dcc:    dcc,
   503  		kCli:   kCli,
   504  		execer: execer,
   505  	}
   506  
   507  	t.Cleanup(ret.TearDown)
   508  
   509  	return ret
   510  }
   511  
   512  func (f *downFixture) TearDown() {
   513  	f.cancel()
   514  }
   515  
   516  func newTiltfileLoadResult(manifests ...model.Manifest) tiltfile.TiltfileLoadResult {
   517  	tlr := tiltfile.TiltfileLoadResult{Manifests: manifests}
   518  	for _, m := range manifests {
   519  		tlr.EnabledManifests = append(tlr.EnabledManifests, m.Name)
   520  	}
   521  	return tlr
   522  }