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 }