github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/tiltfile/api_test.go (about) 1 package tiltfile 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 apierrors "k8s.io/apimachinery/pkg/api/errors" 11 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 "k8s.io/apimachinery/pkg/runtime/schema" 13 "k8s.io/apimachinery/pkg/types" 14 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 15 16 "github.com/tilt-dev/tilt/internal/controllers/fake" 17 "github.com/tilt-dev/tilt/internal/k8s/testyaml" 18 "github.com/tilt-dev/tilt/internal/store" 19 "github.com/tilt-dev/tilt/internal/testutils/configmap" 20 "github.com/tilt-dev/tilt/internal/testutils/manifestbuilder" 21 "github.com/tilt-dev/tilt/internal/testutils/tempdir" 22 "github.com/tilt-dev/tilt/internal/tiltfile" 23 "github.com/tilt-dev/tilt/pkg/apis" 24 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 25 "github.com/tilt-dev/tilt/pkg/model" 26 ) 27 28 func TestAPICreate(t *testing.T) { 29 f := newAPIFixture(t) 30 fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 31 nn := types.NamespacedName{Name: "tiltfile"} 32 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 33 err := f.updateOwnedObjects(nn, tf, 34 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 35 assert.NoError(t, err) 36 37 var ka v1alpha1.KubernetesApply 38 assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka)) 39 assert.Contains(t, ka.Spec.YAML, "name: sancho") 40 } 41 42 func TestAPIDelete(t *testing.T) { 43 f := newAPIFixture(t) 44 fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 45 nn := types.NamespacedName{Name: "tiltfile"} 46 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 47 err := f.updateOwnedObjects(nn, tf, 48 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 49 assert.NoError(t, err) 50 51 var ka1 v1alpha1.KubernetesApply 52 assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka1)) 53 54 err = f.updateOwnedObjects(nn, tf, 55 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{}}) 56 assert.NoError(t, err) 57 58 var ka2 v1alpha1.KubernetesApply 59 err = f.Get(types.NamespacedName{Name: "fe"}, &ka2) 60 if assert.Error(t, err) { 61 assert.True(t, apierrors.IsNotFound(err)) 62 } 63 } 64 65 func TestAPINoGarbageCollectOnError(t *testing.T) { 66 f := newAPIFixture(t) 67 fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 68 nn := types.NamespacedName{Name: "tiltfile"} 69 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 70 err := f.updateOwnedObjects(nn, tf, 71 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 72 assert.NoError(t, err) 73 74 var ka1 v1alpha1.KubernetesApply 75 assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka1)) 76 77 err = f.updateOwnedObjects(nn, tf, &tiltfile.TiltfileLoadResult{ 78 Error: fmt.Errorf("random failure"), 79 Manifests: []model.Manifest{}, 80 }) 81 assert.NoError(t, err) 82 83 var ka2 v1alpha1.KubernetesApply 84 assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka2)) 85 assert.Equal(t, ka1, ka2) 86 } 87 88 func TestAPIUpdate(t *testing.T) { 89 f := newAPIFixture(t) 90 fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 91 nn := types.NamespacedName{Name: "tiltfile"} 92 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 93 err := f.updateOwnedObjects(nn, tf, 94 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 95 assert.NoError(t, err) 96 97 var ka v1alpha1.KubernetesApply 98 assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka)) 99 assert.Contains(t, ka.Spec.YAML, "name: sancho") 100 assert.NotContains(t, ka.Spec.YAML, "sidecar") 101 102 fe = manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoSidecarYAML).Build() 103 err = f.updateOwnedObjects(nn, tf, 104 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 105 assert.NoError(t, err) 106 107 err = f.Get(types.NamespacedName{Name: "fe"}, &ka) 108 assert.NoError(t, err) 109 assert.Contains(t, ka.Spec.YAML, "sidecar") 110 } 111 112 func TestImageMapCreate(t *testing.T) { 113 f := newAPIFixture(t) 114 fe := manifestbuilder.New(f, "fe"). 115 WithImageTarget(NewSanchoDockerBuildImageTarget(f)). 116 WithK8sYAML(testyaml.SanchoYAML). 117 Build() 118 nn := types.NamespacedName{Name: "tiltfile"} 119 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 120 err := f.updateOwnedObjects(nn, tf, 121 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 122 assert.NoError(t, err) 123 124 name := apis.SanitizeName(SanchoRef.String()) 125 126 var im v1alpha1.ImageMap 127 assert.NoError(t, f.Get(types.NamespacedName{Name: name}, &im)) 128 assert.Contains(t, im.Spec.Selector, SanchoRef.String()) 129 130 diName := apis.SanitizeName(fmt.Sprintf("fe:%s", SanchoRef.String())) 131 var di v1alpha1.DockerImage 132 assert.NoError(t, f.Get(types.NamespacedName{Name: diName}, &di)) 133 assert.Contains(t, di.Spec.Ref, SanchoRef.String()) 134 } 135 136 func TestCmdImageCreate(t *testing.T) { 137 f := newAPIFixture(t) 138 target := model.MustNewImageTarget(SanchoRef). 139 WithBuildDetails(model.CustomBuild{ 140 CmdImageSpec: v1alpha1.CmdImageSpec{Args: []string{"echo"}}, 141 Deps: []string{f.Path()}, 142 }) 143 fe := manifestbuilder.New(f, "fe"). 144 WithImageTarget(target). 145 WithK8sYAML(testyaml.SanchoYAML). 146 Build() 147 nn := types.NamespacedName{Name: "tiltfile"} 148 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 149 err := f.updateOwnedObjects(nn, tf, 150 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 151 assert.NoError(t, err) 152 153 name := apis.SanitizeName(SanchoRef.String()) 154 155 var im v1alpha1.ImageMap 156 assert.NoError(t, f.Get(types.NamespacedName{Name: name}, &im)) 157 assert.Contains(t, im.Spec.Selector, SanchoRef.String()) 158 159 ciName := apis.SanitizeName(fmt.Sprintf("fe:%s", SanchoRef.String())) 160 var ci v1alpha1.CmdImage 161 assert.NoError(t, f.Get(types.NamespacedName{Name: ciName}, &ci)) 162 assert.Contains(t, ci.Spec.Ref, SanchoRef.String()) 163 } 164 165 func TestTwoManifestsShareImage(t *testing.T) { 166 f := newAPIFixture(t) 167 target := model.MustNewImageTarget(SanchoRef). 168 WithBuildDetails(model.CustomBuild{ 169 CmdImageSpec: v1alpha1.CmdImageSpec{Args: []string{"echo"}}, 170 Deps: []string{f.Path()}, 171 }) 172 fe1 := manifestbuilder.New(f, "fe1"). 173 WithImageTarget(target). 174 WithK8sYAML(testyaml.SanchoYAML). 175 Build() 176 fe2 := manifestbuilder.New(f, "fe2"). 177 WithImageTarget(target). 178 WithK8sYAML(testyaml.SanchoYAML). 179 Build() 180 nn := types.NamespacedName{Name: "tiltfile"} 181 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 182 err := f.updateOwnedObjects(nn, tf, 183 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe1, fe2}}) 184 assert.NoError(t, err) 185 186 name := apis.SanitizeName(fe1.ImageTargets[0].ID().String()) 187 188 var fw v1alpha1.FileWatch 189 assert.NoError(t, f.Get(types.NamespacedName{Name: name}, &fw)) 190 assert.Equal(t, fw.Spec.DisableSource, &v1alpha1.DisableSource{ 191 EveryConfigMap: []v1alpha1.ConfigMapDisableSource{ 192 {Name: "fe1-disable", Key: "isDisabled"}, 193 {Name: "fe2-disable", Key: "isDisabled"}, 194 }, 195 }) 196 } 197 198 func TestAPITwoTiltfiles(t *testing.T) { 199 f := newAPIFixture(t) 200 feA := manifestbuilder.New(f, "fe-a").WithK8sYAML(testyaml.SanchoYAML).Build() 201 nnA := types.NamespacedName{Name: "tiltfile-a"} 202 tfA := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile-a"}} 203 204 feB := manifestbuilder.New(f, "fe-b").WithK8sYAML(testyaml.SanchoYAML).Build() 205 nnB := types.NamespacedName{Name: "tiltfile-b"} 206 tfB := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile-b"}} 207 208 err := f.updateOwnedObjects(nnA, tfA, 209 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{feA}}) 210 assert.NoError(t, err) 211 212 err = f.updateOwnedObjects(nnB, tfB, 213 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{feB}}) 214 assert.NoError(t, err) 215 216 var ka v1alpha1.KubernetesApply 217 assert.NoError(t, f.Get(types.NamespacedName{Name: "fe-a"}, &ka)) 218 assert.Contains(t, ka.Name, "fe-a") 219 assert.NoError(t, f.Get(types.NamespacedName{Name: "fe-b"}, &ka)) 220 assert.Contains(t, ka.Name, "fe-b") 221 222 err = f.updateOwnedObjects(nnA, nil, nil) 223 assert.NoError(t, err) 224 225 // Assert that fe-a was deleted but fe-b was not. 226 assert.NoError(t, f.Get(types.NamespacedName{Name: "fe-b"}, &ka)) 227 assert.Contains(t, ka.Name, "fe-b") 228 229 err = f.Get(types.NamespacedName{Name: "fe-a"}, &ka) 230 if assert.Error(t, err) { 231 assert.True(t, apierrors.IsNotFound(err)) 232 } 233 } 234 235 func TestCreateUiResourceForTiltfile(t *testing.T) { 236 f := newAPIFixture(t) 237 fe := manifestbuilder.New(f, "fe"). 238 WithImageTarget(NewSanchoDockerBuildImageTarget(f)). 239 WithK8sYAML(testyaml.SanchoYAML). 240 Build() 241 lr := manifestbuilder.New(f, "be").WithLocalResource("ls", []string{"be"}).Build() 242 nn := types.NamespacedName{Name: "tiltfile"} 243 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile", Labels: map[string]string{"some": "sweet-label"}}} 244 err := f.updateOwnedObjects(nn, tf, 245 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe, lr}}) 246 assert.NoError(t, err) 247 248 var uir v1alpha1.UIResource 249 require.NoError(t, f.Get(types.NamespacedName{Name: "tiltfile"}, &uir)) 250 require.Equal(t, map[string]string{"some": "sweet-label"}, uir.ObjectMeta.Labels) 251 require.Equal(t, "tiltfile", uir.ObjectMeta.Name) 252 } 253 254 func TestCreateClusterDefaultRegistry(t *testing.T) { 255 f := newAPIFixture(t) 256 fe := manifestbuilder.New(f, "fe"). 257 WithImageTarget(NewSanchoDockerBuildImageTarget(f)). 258 WithK8sYAML(testyaml.SanchoYAML). 259 Build() 260 tf := &v1alpha1.Tiltfile{ 261 ObjectMeta: metav1.ObjectMeta{Name: model.MainTiltfileManifestName.String()}, 262 } 263 nn := apis.Key(tf) 264 reg := &v1alpha1.RegistryHosting{ 265 Host: "registry.example.com", 266 SingleName: "fake-repo", 267 } 268 tlr := &tiltfile.TiltfileLoadResult{ 269 Manifests: []model.Manifest{fe}, 270 DefaultRegistry: reg, 271 } 272 err := f.updateOwnedObjects(nn, tf, tlr) 273 assert.NoError(t, err) 274 275 var cluster v1alpha1.Cluster 276 require.NoError(t, f.Get(types.NamespacedName{Name: "default"}, &cluster)) 277 require.NotNil(t, cluster.Spec.DefaultRegistry, ".Spec.DefaultRegistry was nil") 278 require.Equal(t, "registry.example.com", cluster.Spec.DefaultRegistry.Host, "Default registry host") 279 require.Equal(t, "fake-repo", cluster.Spec.DefaultRegistry.SingleName, "Default registry single name") 280 } 281 282 // Ensure that we emit disable-related objects/field appropriately 283 func TestDisableObjects(t *testing.T) { 284 f := newAPIFixture(t) 285 fe := manifestbuilder.New(f, "fe"). 286 WithImageTarget(NewSanchoDockerBuildImageTarget(f)). 287 WithK8sYAML(testyaml.SanchoYAML). 288 Build() 289 lr := manifestbuilder.New(f, "be").WithLocalResource("ls", []string{"be"}).Build() 290 nn := types.NamespacedName{Name: "tiltfile"} 291 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 292 err := f.updateOwnedObjects(nn, tf, 293 &tiltfile.TiltfileLoadResult{ 294 Manifests: []model.Manifest{fe, lr}, 295 }) 296 assert.NoError(t, err) 297 298 feDisable := &v1alpha1.DisableSource{ 299 ConfigMap: &v1alpha1.ConfigMapDisableSource{ 300 Name: "fe-disable", 301 Key: "isDisabled", 302 }, 303 } 304 305 var cm v1alpha1.ConfigMap 306 require.NoError(t, f.Get(types.NamespacedName{Name: feDisable.ConfigMap.Name}, &cm)) 307 require.Equal(t, "true", cm.Data[feDisable.ConfigMap.Key]) 308 309 name := apis.SanitizeName(SanchoRef.String()) 310 var im v1alpha1.ImageMap 311 require.NoError(t, f.Get(types.NamespacedName{Name: name}, &im)) 312 313 var ka v1alpha1.KubernetesApply 314 require.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka)) 315 require.Equal(t, feDisable, ka.Spec.DisableSource) 316 317 beDisable := &v1alpha1.DisableSource{ 318 ConfigMap: &v1alpha1.ConfigMapDisableSource{ 319 Name: "be-disable", 320 Key: "isDisabled", 321 }, 322 } 323 324 var fw v1alpha1.FileWatch 325 require.NoError(t, f.Get(types.NamespacedName{Name: "local:be"}, &fw)) 326 require.Equal(t, beDisable, fw.Spec.DisableSource) 327 328 var cmd v1alpha1.Cmd 329 require.NoError(t, f.Get(types.NamespacedName{Name: "be:update"}, &cmd)) 330 require.Equal(t, beDisable, cmd.Spec.DisableSource) 331 332 var uir v1alpha1.UIResource 333 require.NoError(t, f.Get(types.NamespacedName{Name: "be"}, &uir)) 334 require.Equal(t, []v1alpha1.DisableSource{*beDisable}, uir.Status.DisableStatus.Sources) 335 336 var tb v1alpha1.ToggleButton 337 err = f.Get(types.NamespacedName{Name: "fe-disable"}, &tb) 338 require.NoError(t, err) 339 require.Equal(t, feDisable.ConfigMap.Name, tb.Spec.StateSource.ConfigMap.Name) 340 341 err = f.Get(types.NamespacedName{Name: "be-disable"}, &tb) 342 require.NoError(t, err) 343 require.Equal(t, beDisable.ConfigMap.Name, tb.Spec.StateSource.ConfigMap.Name) 344 } 345 346 // If a DisableSource ConfigMap already exists, don't replace its data 347 func TestUpdateDisableSource(t *testing.T) { 348 f := newAPIFixture(t) 349 fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 350 nn := types.NamespacedName{Name: "tiltfile"} 351 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 352 err := f.updateOwnedObjects(nn, tf, 353 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 354 assert.NoError(t, err) 355 356 err = configmap.UpsertDisableConfigMap(f.ctx, f.c, "fe-disable", "isDisabled", true) 357 require.NoError(t, err) 358 359 err = f.updateOwnedObjects(nn, tf, 360 &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}) 361 assert.NoError(t, err) 362 363 var cm v1alpha1.ConfigMap 364 require.NoError(t, f.Get(types.NamespacedName{Name: "fe-disable"}, &cm)) 365 require.Equal(t, "true", cm.Data["isDisabled"]) 366 } 367 368 // make sure that objects created by the Tiltfile are included in typesToReconcile, so that 369 // they get cleaned up when they go away 370 // note: this test is not exhaustive, since not all branches generate all types that are possibly 371 // generated by a Tiltfile, but hopefully it at least catches most common cases 372 func TestReconciledTypesCompleteness(t *testing.T) { 373 f := newAPIFixture(t) 374 nn := types.NamespacedName{Name: "tiltfile"} 375 tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}} 376 fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build() 377 tlr := &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}} 378 ds := toDisableSources(tlr) 379 objs := toAPIObjects(nn, tf, tlr, 0, store.EngineModeCI, &v1alpha1.KubernetesClusterConnection{}, ds) 380 381 reconciledTypes := make(map[schema.GroupVersionResource]bool) 382 for _, t := range typesToReconcile { 383 reconciledTypes[t.GetGroupVersionResource()] = true 384 } 385 386 for _, os := range objs { 387 for _, v := range os { 388 require.Truef(t, 389 reconciledTypes[v.GetGroupVersionResource()], 390 "object %q of type %q was generated by the Tiltfile, but is not listed in typesToReconcile.\n"+ 391 "either add the type to typesToReconcile or change the Tiltfile reconciler to not generate it.", 392 v.GetName(), 393 v.GetGroupVersionResource()) 394 } 395 } 396 } 397 398 type apiFixture struct { 399 ctx context.Context 400 c ctrlclient.Client 401 *tempdir.TempDirFixture 402 } 403 404 func newAPIFixture(t testing.TB) *apiFixture { 405 f := tempdir.NewTempDirFixture(t) 406 407 ctx := context.Background() 408 c := fake.NewFakeTiltClient() 409 return &apiFixture{ 410 ctx: ctx, 411 c: c, 412 TempDirFixture: f, 413 } 414 } 415 416 func (f *apiFixture) updateOwnedObjects(nn types.NamespacedName, tf *v1alpha1.Tiltfile, tlr *tiltfile.TiltfileLoadResult) error { 417 return updateOwnedObjects(f.ctx, f.c, nn, tf, tlr, false, 0, store.EngineModeUp, 418 &v1alpha1.KubernetesClusterConnection{}) 419 } 420 421 func (f *apiFixture) Get(nn types.NamespacedName, obj ctrlclient.Object) error { 422 return f.c.Get(f.ctx, nn, obj) 423 }