github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/tiltfile/api.go (about) 1 package tiltfile 2 3 import ( 4 "context" 5 "fmt" 6 "strconv" 7 "time" 8 9 "github.com/google/go-cmp/cmp" 10 "k8s.io/apimachinery/pkg/api/meta" 11 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 "k8s.io/apimachinery/pkg/runtime" 13 "k8s.io/apimachinery/pkg/types" 14 "k8s.io/apimachinery/pkg/util/errors" 15 "sigs.k8s.io/controller-runtime/pkg/cache" 16 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 17 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 18 19 "github.com/tilt-dev/tilt/internal/controllers/apicmp" 20 "github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate" 21 "github.com/tilt-dev/tilt/internal/controllers/apis/uibutton" 22 "github.com/tilt-dev/tilt/internal/controllers/apiset" 23 "github.com/tilt-dev/tilt/internal/controllers/indexer" 24 "github.com/tilt-dev/tilt/internal/feature" 25 "github.com/tilt-dev/tilt/internal/store" 26 "github.com/tilt-dev/tilt/internal/store/sessions" 27 "github.com/tilt-dev/tilt/internal/tiltfile" 28 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 29 "github.com/tilt-dev/tilt/pkg/model" 30 ) 31 32 var ( 33 apiGVStr = v1alpha1.SchemeGroupVersion.String() 34 apiKind = "Tiltfile" 35 apiType = metav1.TypeMeta{Kind: apiKind, APIVersion: apiGVStr} 36 ) 37 38 type disableSourceMap map[model.ManifestName]*v1alpha1.DisableSource 39 40 // Update all the objects in the apiserver that are owned by the Tiltfile. 41 // 42 // Here we have one big API object (the Tiltfile loader) create lots of 43 // API objects of different types. This is not a common pattern in Kubernetes-land 44 // (where often each type will only own one or two other types). But it's the best way 45 // to model how the Tiltfile works. 46 // 47 // For that reason, this code is much more generic than owned-object creation should be. 48 // 49 // In the future, anything that creates objects based on the Tiltfile (e.g., FileWatch specs, 50 // LocalServer specs) should go here. 51 func updateOwnedObjects( 52 ctx context.Context, 53 client ctrlclient.Client, 54 nn types.NamespacedName, 55 tf *v1alpha1.Tiltfile, 56 tlr *tiltfile.TiltfileLoadResult, 57 changeEnabledResources bool, 58 ciTimeoutFlag model.CITimeoutFlag, 59 mode store.EngineMode, 60 defaultK8sConnection *v1alpha1.KubernetesClusterConnection, 61 ) error { 62 disableSources := toDisableSources(tlr) 63 64 if tlr != nil { 65 for i, m := range tlr.Manifests { 66 // Apply the registry to the image refs. 67 err := m.InferImageProperties() 68 if err != nil { 69 return err 70 } 71 72 // Assemble the LiveUpdate selectors, connecting objects together. 73 err = m.InferLiveUpdateSelectors() 74 if err != nil { 75 return err 76 } 77 78 tlr.Manifests[i] = m.WithDisableSource(disableSources[m.Name]) 79 } 80 } 81 82 apiObjects := toAPIObjects(nn, tf, tlr, ciTimeoutFlag, mode, defaultK8sConnection, disableSources) 83 84 // Propagate labels and owner references from the parent tiltfile. 85 for _, objMap := range apiObjects { 86 for _, obj := range objMap { 87 err := controllerutil.SetControllerReference(tf, obj, client.Scheme()) 88 if err != nil { 89 return err 90 } 91 propagateLabels(tf, obj) 92 propagateAnnotations(tf, obj) 93 } 94 } 95 96 // Retry until the cache has started. 97 var retryCount = 0 98 var existingObjects apiset.ObjectSet 99 var err error 100 for { 101 existingObjects, err = getExistingAPIObjects(ctx, client, nn) 102 if err != nil { 103 if _, ok := err.(*cache.ErrCacheNotStarted); ok && retryCount < 5 { 104 retryCount++ 105 time.Sleep(200 * time.Millisecond) 106 continue 107 } 108 return err 109 } 110 break 111 } 112 113 if !changeEnabledResources { 114 // if we're not changing enabled resources, use existing values for disable configmaps 115 newConfigMaps := apiObjects.GetSetForType(&v1alpha1.ConfigMap{}) 116 oldConfigMaps := existingObjects.GetSetForType(&v1alpha1.ConfigMap{}) 117 for _, ds := range disableSources { 118 if old, ok := oldConfigMaps[ds.ConfigMap.Name]; ok { 119 newConfigMaps[ds.ConfigMap.Name] = old 120 } 121 } 122 } 123 124 err = updateNewObjects(ctx, client, apiObjects, existingObjects) 125 if err != nil { 126 return err 127 } 128 129 // If the tiltfile loader succeeded or if the tiltfile was deleted, 130 // garbage collect any old objects. 131 // 132 // If the tiltfile loader failed, we want to keep those objects around, in case 133 // the tiltfile was only partially evaluated and is missing objects. 134 if tlr == nil || tlr.Error == nil { 135 err := removeOrphanedObjects(ctx, client, apiObjects, existingObjects) 136 if err != nil { 137 return err 138 } 139 } 140 return nil 141 } 142 143 // Apply labels from the Tiltfile to all objects it creates. 144 func propagateLabels(tf *v1alpha1.Tiltfile, obj apiset.Object) { 145 if len(tf.Spec.Labels) > 0 { 146 labels := obj.GetLabels() 147 if labels == nil { 148 labels = make(map[string]string) 149 } 150 for k, v := range tf.Spec.Labels { 151 // Labels specified during tiltfile execution take precedence over 152 // labels specified in the tiltfile spec. 153 _, exists := labels[k] 154 if !exists { 155 labels[k] = v 156 } 157 } 158 obj.SetLabels(labels) 159 } 160 } 161 162 // We don't have a great strategy right now for assigning 163 // API object spec definitions to Manifests in the Tilt UI. 164 // 165 // For now, if an object doesn't have a Manifest annotation 166 // defined, we give it the same Manifest as the parent Tiltfile. 167 func propagateAnnotations(tf *v1alpha1.Tiltfile, obj apiset.Object) { 168 annos := obj.GetAnnotations() 169 if annos[v1alpha1.AnnotationManifest] == "" { 170 if annos == nil { 171 annos = make(map[string]string) 172 } 173 annos[v1alpha1.AnnotationManifest] = tf.Name 174 obj.SetAnnotations(annos) 175 } 176 } 177 178 var typesWithTiltfileBuiltins = []apiset.Object{ 179 &v1alpha1.ExtensionRepo{}, 180 &v1alpha1.Extension{}, 181 &v1alpha1.FileWatch{}, 182 &v1alpha1.Cmd{}, 183 &v1alpha1.KubernetesApply{}, 184 &v1alpha1.UIButton{}, 185 &v1alpha1.ConfigMap{}, 186 &v1alpha1.KubernetesDiscovery{}, 187 } 188 189 var typesToReconcile = append([]apiset.Object{ 190 &v1alpha1.ImageMap{}, 191 &v1alpha1.CmdImage{}, 192 &v1alpha1.DockerImage{}, 193 &v1alpha1.UIResource{}, 194 &v1alpha1.LiveUpdate{}, 195 &v1alpha1.ToggleButton{}, 196 &v1alpha1.Cluster{}, 197 &v1alpha1.DockerComposeService{}, 198 &v1alpha1.Session{}, 199 }, typesWithTiltfileBuiltins...) 200 201 // Fetch all the existing API objects that were generated from the Tiltfile. 202 func getExistingAPIObjects(ctx context.Context, client ctrlclient.Client, nn types.NamespacedName) (apiset.ObjectSet, error) { 203 result := apiset.ObjectSet{} 204 205 // TODO(nick): Parallelize this? 206 for _, obj := range typesToReconcile { 207 list := obj.NewList().(ctrlclient.ObjectList) 208 err := indexer.ListOwnedBy(ctx, client, list, nn, apiType) 209 if err != nil { 210 return nil, err 211 } 212 213 _ = meta.EachListItem(list, func(obj runtime.Object) error { 214 result.Add(obj.(apiset.Object)) 215 return nil 216 }) 217 } 218 219 return result, nil 220 } 221 222 // Pulls out all the API objects generated by the Tiltfile. 223 func toAPIObjects( 224 nn types.NamespacedName, 225 tf *v1alpha1.Tiltfile, 226 tlr *tiltfile.TiltfileLoadResult, 227 ciTimeoutFlag model.CITimeoutFlag, 228 mode store.EngineMode, 229 defaultK8sConnection *v1alpha1.KubernetesClusterConnection, 230 disableSources disableSourceMap, 231 ) apiset.ObjectSet { 232 result := apiset.ObjectSet{} 233 234 if tlr != nil { 235 result.AddSetForType(&v1alpha1.ImageMap{}, toImageMapObjects(tlr, disableSources)) 236 result.AddSetForType(&v1alpha1.LiveUpdate{}, toLiveUpdateObjects(tlr)) 237 result.AddSetForType(&v1alpha1.DockerImage{}, toDockerImageObjects(tlr, disableSources)) 238 result.AddSetForType(&v1alpha1.CmdImage{}, toCmdImageObjects(tlr, disableSources)) 239 240 for _, obj := range typesWithTiltfileBuiltins { 241 result.AddSetForType(obj, tlr.ObjectSet.GetSetForType(obj)) 242 } 243 244 result.AddSetForType(&v1alpha1.KubernetesApply{}, toKubernetesApplyObjects(tlr, disableSources)) 245 result.AddSetForType(&v1alpha1.DockerComposeService{}, toDockerComposeServiceObjects(tlr, disableSources)) 246 result.AddSetForType(&v1alpha1.ConfigMap{}, toDisableConfigMaps(disableSources, tlr.EnabledManifests)) 247 result.AddSetForType(&v1alpha1.Cmd{}, toCmdObjects(tlr, disableSources)) 248 result.AddSetForType(&v1alpha1.ToggleButton{}, toToggleButtons(disableSources)) 249 result.AddSetForType(&v1alpha1.Cluster{}, toClusterObjects(nn, tlr, defaultK8sConnection)) 250 result.AddSetForType(&v1alpha1.UIButton{}, toCancelButtons(tlr)) 251 } 252 253 result.AddSetForType(&v1alpha1.Session{}, toSessionObjects(nn, tf, tlr, ciTimeoutFlag, mode)) 254 result.AddSetForType(&v1alpha1.UIResource{}, toUIResourceObjects(tf, tlr, disableSources)) 255 256 watchInputs := WatchInputs{ 257 TiltfileManifestName: model.ManifestName(nn.Name), 258 EngineMode: mode, 259 } 260 261 if tlr != nil { 262 watchInputs.Manifests = tlr.Manifests 263 watchInputs.ConfigFiles = tlr.ConfigFiles 264 watchInputs.Tiltignore = tlr.Tiltignore 265 watchInputs.WatchSettings = tlr.WatchSettings 266 } 267 268 if tf != nil { 269 watchInputs.TiltfilePath = tf.Spec.Path 270 } 271 272 result.AddSetForType(&v1alpha1.FileWatch{}, ToFileWatchObjects(watchInputs, disableSources)) 273 274 return result 275 } 276 277 func disableConfigMapName(manifest model.Manifest) string { 278 return fmt.Sprintf("%s-disable", manifest.Name) 279 } 280 281 func toDisableSources(tlr *tiltfile.TiltfileLoadResult) disableSourceMap { 282 result := make(disableSourceMap) 283 if tlr != nil { 284 for _, m := range tlr.Manifests { 285 name := disableConfigMapName(m) 286 ds := &v1alpha1.DisableSource{ 287 ConfigMap: &v1alpha1.ConfigMapDisableSource{ 288 Name: name, 289 Key: "isDisabled", 290 }, 291 } 292 result[m.Name] = ds 293 } 294 } 295 return result 296 } 297 298 func appendCMDS(cms []v1alpha1.ConfigMapDisableSource, newCM v1alpha1.ConfigMapDisableSource) []v1alpha1.ConfigMapDisableSource { 299 for _, cm := range cms { 300 if apicmp.DeepEqual(cm, newCM) { 301 return cms 302 } 303 } 304 return append(cms, newCM) 305 } 306 307 func mergeDisableSource(existing *v1alpha1.DisableSource, toMerge *v1alpha1.DisableSource) *v1alpha1.DisableSource { 308 if toMerge == nil { 309 return existing 310 } 311 if apicmp.DeepEqual(existing, toMerge) { 312 return existing 313 } 314 315 cms := []v1alpha1.ConfigMapDisableSource{} 316 317 if existing.ConfigMap != nil { 318 cms = append(cms, *existing.ConfigMap) 319 } 320 cms = append(cms, existing.EveryConfigMap...) 321 if toMerge.ConfigMap != nil { 322 cms = appendCMDS(cms, *toMerge.ConfigMap) 323 } 324 for _, newCM := range toMerge.EveryConfigMap { 325 cms = appendCMDS(cms, newCM) 326 } 327 return &v1alpha1.DisableSource{EveryConfigMap: cms} 328 } 329 330 func toDisableConfigMaps(disableSources disableSourceMap, enabledResources []model.ManifestName) apiset.TypedObjectSet { 331 enabledResourceSet := make(map[model.ManifestName]bool) 332 for _, mn := range enabledResources { 333 enabledResourceSet[mn] = true 334 } 335 result := apiset.TypedObjectSet{} 336 for mn, ds := range disableSources { 337 isDisabled := !enabledResourceSet[mn] 338 cm := &v1alpha1.ConfigMap{ 339 ObjectMeta: metav1.ObjectMeta{ 340 Name: ds.ConfigMap.Name, 341 }, 342 Data: map[string]string{ds.ConfigMap.Key: strconv.FormatBool(isDisabled)}, 343 } 344 result[cm.Name] = cm 345 } 346 return result 347 } 348 349 func toToggleButtons(disableSources disableSourceMap) apiset.TypedObjectSet { 350 result := apiset.TypedObjectSet{} 351 for name, ds := range disableSources { 352 tb := &v1alpha1.ToggleButton{ 353 ObjectMeta: metav1.ObjectMeta{ 354 Name: fmt.Sprintf("%s-disable", name), 355 Annotations: map[string]string{ 356 v1alpha1.AnnotationButtonType: v1alpha1.ButtonTypeDisableToggle, 357 }, 358 }, 359 Spec: v1alpha1.ToggleButtonSpec{ 360 Location: v1alpha1.UIComponentLocation{ 361 ComponentID: string(name), 362 ComponentType: v1alpha1.ComponentTypeResource, 363 }, 364 On: v1alpha1.ToggleButtonStateSpec{ 365 Text: "Enable Resource", 366 }, 367 Off: v1alpha1.ToggleButtonStateSpec{ 368 Text: "Disable Resource", 369 RequiresConfirmation: true, 370 }, 371 StateSource: v1alpha1.StateSource{ 372 ConfigMap: &v1alpha1.ConfigMapStateSource{ 373 Name: ds.ConfigMap.Name, 374 Key: ds.ConfigMap.Key, 375 OnValue: "true", 376 OffValue: "false", 377 }, 378 }, 379 }, 380 } 381 result[tb.Name] = tb 382 } 383 return result 384 } 385 386 func toCancelButtons(tlr *tiltfile.TiltfileLoadResult) apiset.TypedObjectSet { 387 result := apiset.TypedObjectSet{} 388 for _, m := range tlr.Manifests { 389 button := uibutton.StopBuildButton(m.Name.String()) 390 result[button.Name] = button 391 } 392 return result 393 } 394 395 // Pulls out all the KubernetesApply objects generated by the Tiltfile. 396 func toKubernetesApplyObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet { 397 result := apiset.TypedObjectSet{} 398 for _, m := range tlr.Manifests { 399 if !m.IsK8s() { 400 continue 401 } 402 403 kTarget := m.K8sTarget() 404 name := m.Name.String() 405 ka := &v1alpha1.KubernetesApply{ 406 ObjectMeta: metav1.ObjectMeta{ 407 Name: name, 408 Annotations: map[string]string{ 409 v1alpha1.AnnotationManifest: name, 410 v1alpha1.AnnotationSpanID: fmt.Sprintf("kubernetesapply:%s", name), 411 v1alpha1.AnnotationManagedBy: "buildcontrol", 412 }, 413 }, 414 Spec: kTarget.KubernetesApplySpec, 415 } 416 ka.Spec.DisableSource = disableSources[m.Name] 417 result[name] = ka 418 } 419 return result 420 } 421 422 // Pulls out all the DockerComposeService objects generated by the Tiltfile. 423 func toDockerComposeServiceObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet { 424 result := apiset.TypedObjectSet{} 425 for _, m := range tlr.Manifests { 426 if !m.IsDC() { 427 continue 428 } 429 430 dcTarget := m.DockerComposeTarget() 431 name := m.Name.String() 432 obj := &v1alpha1.DockerComposeService{ 433 ObjectMeta: metav1.ObjectMeta{ 434 Name: name, 435 Annotations: map[string]string{ 436 v1alpha1.AnnotationManifest: name, 437 v1alpha1.AnnotationSpanID: fmt.Sprintf("dockercompose:%s", name), 438 v1alpha1.AnnotationManagedBy: "buildcontrol", 439 }, 440 }, 441 Spec: dcTarget.Spec, 442 } 443 obj.Spec.DisableSource = disableSources[m.Name] 444 result[name] = obj 445 } 446 return result 447 } 448 449 // Pulls out all the LiveUpdate objects generated by the Tiltfile. 450 func toLiveUpdateObjects(tlr *tiltfile.TiltfileLoadResult) apiset.TypedObjectSet { 451 result := apiset.TypedObjectSet{} 452 for _, m := range tlr.Manifests { 453 for _, iTarget := range m.ImageTargets { 454 luSpec := iTarget.LiveUpdateSpec 455 luName := iTarget.LiveUpdateName 456 if liveupdate.IsEmptySpec(luSpec) || luName == "" { 457 continue 458 } 459 460 managedBy := "" 461 if !iTarget.LiveUpdateReconciler { 462 managedBy = "buildcontrol" 463 } 464 465 updateMode := liveupdate.UpdateModeAuto 466 if !m.TriggerMode.AutoOnChange() { 467 updateMode = liveupdate.UpdateModeManual 468 } 469 470 obj := &v1alpha1.LiveUpdate{ 471 ObjectMeta: metav1.ObjectMeta{ 472 Name: luName, 473 Annotations: map[string]string{ 474 v1alpha1.AnnotationManifest: m.Name.String(), 475 v1alpha1.AnnotationSpanID: fmt.Sprintf("liveupdate:%s", luName), 476 v1alpha1.AnnotationManagedBy: managedBy, 477 liveupdate.AnnotationUpdateMode: updateMode, 478 }, 479 }, 480 Spec: luSpec, 481 } 482 result[luName] = obj 483 } 484 } 485 return result 486 } 487 488 // Pulls out all the DockerImage objects generated by the Tiltfile. 489 func toDockerImageObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet { 490 result := apiset.TypedObjectSet{} 491 492 for _, m := range tlr.Manifests { 493 for _, iTarget := range m.ImageTargets { 494 name := iTarget.DockerImageName 495 if name == "" { 496 continue 497 } 498 499 // Currently, if a DockerImage is in more than one manifest, 500 // we will create one per manifest. 501 // 502 // In the medium-term, we should try to annotate them in a way 503 // that allows manifests to share images. 504 di := &v1alpha1.DockerImage{ 505 ObjectMeta: metav1.ObjectMeta{ 506 Name: name, 507 Annotations: map[string]string{ 508 v1alpha1.AnnotationManifest: m.Name.String(), 509 v1alpha1.AnnotationSpanID: fmt.Sprintf("dockerimage:%s", name), 510 }, 511 }, 512 Spec: iTarget.DockerBuildInfo().DockerImageSpec, 513 } 514 515 // TODO(nick): Add DisableSource to image builds. 516 //di.Spec.DisableSource = disableSources[m.Name] 517 518 result[name] = di 519 } 520 } 521 return result 522 } 523 524 // Pulls out all the CmdImage objects generated by the Tiltfile. 525 func toCmdImageObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet { 526 result := apiset.TypedObjectSet{} 527 528 for _, m := range tlr.Manifests { 529 for _, iTarget := range m.ImageTargets { 530 name := iTarget.CmdImageName 531 if name == "" { 532 continue 533 } 534 535 // Currently, if a CmdImage is in more than one manifest, 536 // we will create one per manifest. 537 // 538 // In the medium-term, we should try to annotate them in a way 539 // that allows manifests to share images. 540 ci := &v1alpha1.CmdImage{ 541 ObjectMeta: metav1.ObjectMeta{ 542 Name: name, 543 Annotations: map[string]string{ 544 v1alpha1.AnnotationManifest: m.Name.String(), 545 v1alpha1.AnnotationSpanID: fmt.Sprintf("cmdimage:%s", name), 546 }, 547 }, 548 Spec: iTarget.CustomBuildInfo().CmdImageSpec, 549 } 550 551 // TODO(nick): Add DisableSource to image builds. 552 // di.Spec.DisableSource = disableSources[m.Name] 553 554 result[name] = ci 555 } 556 } 557 return result 558 } 559 560 // Pulls out all the ImageMap objects generated by the Tiltfile. 561 func toImageMapObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet { 562 result := apiset.TypedObjectSet{} 563 564 for _, m := range tlr.Manifests { 565 for _, iTarget := range m.ImageTargets { 566 if iTarget.IsLiveUpdateOnly { 567 continue 568 } 569 570 name := iTarget.ImageMapName() 571 _, ok := result[name] 572 if ok { 573 // Some ImageTargets are shared among multiple manifests. 574 continue 575 } 576 577 im := &v1alpha1.ImageMap{ 578 ObjectMeta: metav1.ObjectMeta{ 579 Name: name, 580 Annotations: map[string]string{ 581 v1alpha1.AnnotationManifest: m.Name.String(), 582 v1alpha1.AnnotationSpanID: fmt.Sprintf("imagemap:%s", name), 583 }, 584 }, 585 Spec: iTarget.ImageMapSpec, 586 } 587 result[name] = im 588 } 589 } 590 return result 591 } 592 593 // Creates an object representing the tilt session and exit conditions. 594 func toSessionObjects(nn types.NamespacedName, tf *v1alpha1.Tiltfile, tlr *tiltfile.TiltfileLoadResult, ciTimeoutFlag model.CITimeoutFlag, mode store.EngineMode) apiset.TypedObjectSet { 595 result := apiset.TypedObjectSet{} 596 if nn.Name != model.MainTiltfileManifestName.String() { 597 return result 598 } 599 result[sessions.DefaultSessionName] = sessions.FromTiltfile(tf, tlr, ciTimeoutFlag, mode) 600 return result 601 } 602 603 // Pulls out any cluster objects generated by the tiltfile. 604 func toClusterObjects(nn types.NamespacedName, tlr *tiltfile.TiltfileLoadResult, defaultK8sConnection *v1alpha1.KubernetesClusterConnection) apiset.TypedObjectSet { 605 result := apiset.TypedObjectSet{} 606 if nn.Name != model.MainTiltfileManifestName.String() { 607 return result 608 } 609 610 var annotations map[string]string 611 if tlr.FeatureFlags[feature.ClusterRefresh] { 612 annotations = map[string]string{ 613 "features.tilt.dev/cluster-refresh": "true", 614 } 615 } 616 617 if tlr.HasOrchestrator(model.OrchestratorK8s) { 618 name := v1alpha1.ClusterNameDefault 619 result[name] = &v1alpha1.Cluster{ 620 ObjectMeta: metav1.ObjectMeta{ 621 Name: name, 622 Annotations: annotations, 623 }, 624 Spec: v1alpha1.ClusterSpec{ 625 Connection: &v1alpha1.ClusterConnection{ 626 Kubernetes: defaultK8sConnection.DeepCopy(), 627 }, 628 DefaultRegistry: tlr.DefaultRegistry, 629 }, 630 } 631 } 632 633 if tlr.HasOrchestrator(model.OrchestratorDC) { 634 name := v1alpha1.ClusterNameDocker 635 result[name] = &v1alpha1.Cluster{ 636 ObjectMeta: metav1.ObjectMeta{ 637 Name: name, 638 Annotations: annotations, 639 }, 640 Spec: v1alpha1.ClusterSpec{ 641 Connection: &v1alpha1.ClusterConnection{ 642 Docker: &v1alpha1.DockerClusterConnection{}, 643 }, 644 }, 645 } 646 } 647 648 return result 649 } 650 651 // Pulls out all the Cmd objects generated by the Tiltfile. 652 func toCmdObjects(tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet { 653 result := apiset.TypedObjectSet{} 654 655 // Every local_resource's Update Cmd gets its own object. 656 for _, m := range tlr.Manifests { 657 if !m.IsLocal() { 658 continue 659 } 660 localTarget := m.LocalTarget() 661 cmdSpec := localTarget.UpdateCmdSpec 662 if cmdSpec == nil { 663 continue 664 } 665 666 name := localTarget.UpdateCmdName() 667 cmd := &v1alpha1.Cmd{ 668 ObjectMeta: metav1.ObjectMeta{ 669 Name: name, 670 Annotations: map[string]string{ 671 v1alpha1.AnnotationManifest: m.Name.String(), 672 v1alpha1.AnnotationSpanID: fmt.Sprintf("cmd:%s", name), 673 v1alpha1.AnnotationManagedBy: "local_resource", 674 }, 675 }, 676 Spec: *cmdSpec, 677 } 678 cmd.Spec.DisableSource = disableSources[m.Name] 679 result[name] = cmd 680 } 681 682 // Every custom_build Cmd gets its own Cmd object. 683 // It would be better for the CmdImage reconciler to create these 684 // and make them owned by the CmdImage. 685 for _, m := range tlr.Manifests { 686 for _, iTarget := range m.ImageTargets { 687 name := iTarget.CmdImageName 688 if name == "" { 689 continue 690 } 691 692 // Currently, if a CmdImage is in more than one manifest, 693 // we will create one per manifest. 694 // 695 // In the medium-term, we should try to annotate them in a way 696 // that allows manifests to share images. 697 cmdimageSpec := iTarget.CustomBuildInfo().CmdImageSpec 698 cmd := &v1alpha1.Cmd{ 699 ObjectMeta: metav1.ObjectMeta{ 700 Name: iTarget.CmdImageName, 701 Annotations: map[string]string{ 702 v1alpha1.AnnotationManifest: m.Name.String(), 703 v1alpha1.AnnotationSpanID: fmt.Sprintf("cmdimage:%s", name), 704 v1alpha1.AnnotationManagedBy: "cmd_image", 705 }, 706 }, 707 Spec: v1alpha1.CmdSpec{ 708 Args: cmdimageSpec.Args, 709 Dir: cmdimageSpec.Dir, 710 }, 711 } 712 713 // TODO(nick): Add DisableSource to image builds. 714 // cmd.Spec.DisableSource = disableSources[m.Name] 715 716 result[name] = cmd 717 } 718 } 719 return result 720 } 721 722 // Pulls out all the UIResource objects generated by the Tiltfile. 723 func toUIResourceObjects(tf *v1alpha1.Tiltfile, tlr *tiltfile.TiltfileLoadResult, disableSources disableSourceMap) apiset.TypedObjectSet { 724 result := apiset.TypedObjectSet{} 725 726 if tlr != nil { 727 for _, m := range tlr.Manifests { 728 name := string(m.Name) 729 730 r := &v1alpha1.UIResource{ 731 ObjectMeta: metav1.ObjectMeta{ 732 Name: name, 733 Labels: m.Labels, 734 Annotations: map[string]string{ 735 v1alpha1.AnnotationManifest: m.Name.String(), 736 }, 737 }, 738 } 739 740 ds := disableSources[m.Name] 741 if ds != nil { 742 r.Status.DisableStatus.State = v1alpha1.DisableStatePending 743 r.Status.DisableStatus.Sources = []v1alpha1.DisableSource{*ds} 744 } 745 746 result[name] = r 747 } 748 } 749 750 if tf != nil { 751 result[tf.Name] = &v1alpha1.UIResource{ 752 ObjectMeta: metav1.ObjectMeta{ 753 Name: tf.Name, 754 Labels: tf.Labels, 755 Annotations: map[string]string{ 756 v1alpha1.AnnotationManifest: tf.Name, 757 }, 758 }, 759 } 760 } 761 762 return result 763 } 764 765 func needsUpdate(old, obj apiset.Object) bool { 766 // Are there other fields here we should check? 767 specChanged := !apicmp.DeepEqual(old.GetSpec(), obj.GetSpec()) 768 labelsChanged := !apicmp.DeepEqual(old.GetLabels(), obj.GetLabels()) 769 annsChanged := !apicmp.DeepEqual(old.GetAnnotations(), obj.GetAnnotations()) 770 if specChanged || labelsChanged || annsChanged { 771 return true 772 } 773 774 // if this section ends up with more type-specific checks, we should probably move this 775 // to be a method on apiset.Object 776 if cm, ok := obj.(*v1alpha1.ConfigMap); ok { 777 if !cmp.Equal(cm.Data, old.(*v1alpha1.ConfigMap).Data) { 778 return true 779 } 780 } 781 782 return false 783 } 784 785 // Reconcile the new API objects against the existing API objects. 786 func updateNewObjects(ctx context.Context, client ctrlclient.Client, newObjects, oldObjects apiset.ObjectSet) error { 787 // TODO(nick): Does it make sense to parallelize the API calls? 788 errs := []error{} 789 790 // Upsert the new objects. 791 for t, s := range newObjects { 792 for name, obj := range s { 793 var old apiset.Object 794 oldSet, ok := oldObjects[t] 795 if ok { 796 old = oldSet[name] 797 } 798 799 if old == nil { 800 err := client.Create(ctx, obj) 801 if err != nil { 802 errs = append(errs, fmt.Errorf("create %s/%s: %v", obj.GetGroupVersionResource().Resource, obj.GetName(), err)) 803 } 804 continue 805 } 806 807 if needsUpdate(old, obj) { 808 obj.SetResourceVersion(old.GetResourceVersion()) 809 err := client.Update(ctx, obj) 810 if err != nil { 811 errs = append(errs, fmt.Errorf("update %s/%s: %v", obj.GetGroupVersionResource().Resource, obj.GetName(), err)) 812 } 813 continue 814 } 815 } 816 } 817 return errors.NewAggregate(errs) 818 } 819 820 // Garbage collect API objects that are no longer loaded. 821 func removeOrphanedObjects(ctx context.Context, client ctrlclient.Client, newObjects, oldObjects apiset.ObjectSet) error { 822 // Delete any objects that aren't in the new tiltfile. 823 errs := []error{} 824 for t, s := range oldObjects { 825 for name, obj := range s { 826 newSet, ok := newObjects[t] 827 if ok { 828 _, ok := newSet[name] 829 if ok { 830 continue 831 } 832 } 833 834 err := client.Delete(ctx, obj) 835 if err != nil { 836 errs = append(errs, fmt.Errorf("delete %s/%s: %v", obj.GetGroupVersionResource().Resource, obj.GetName(), err)) 837 } 838 } 839 } 840 return errors.NewAggregate(errs) 841 }