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 }