github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/cli/down_test.go (about)

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