github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/kubernetesapply/disco.go (about) 1 package kubernetesapply 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 apierrors "k8s.io/apimachinery/pkg/api/errors" 9 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 "k8s.io/apimachinery/pkg/types" 11 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 12 "sigs.k8s.io/controller-runtime/pkg/reconcile" 13 14 "github.com/tilt-dev/tilt/internal/controllers/apicmp" 15 "github.com/tilt-dev/tilt/internal/k8s" 16 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 17 ) 18 19 // Each KubernetesApply object owns a KubernetesDiscovery object of the same name. 20 // 21 // After we reconcile a KubernetesApply, update the KubernetesDiscovery objects it owns. 22 // 23 // If the Apply has been deleted, any corresponding Disco objects should be deleted. 24 func (r *Reconciler) manageOwnedKubernetesDiscovery(ctx context.Context, nn types.NamespacedName, ka *v1alpha1.KubernetesApply) (reconcile.Result, error) { 25 if ka != nil && (ka.Status.Error != "" || ka.Status.ResultYAML == "") { 26 isDisabled := ka.Status.DisableStatus != nil && 27 ka.Status.DisableStatus.State == v1alpha1.DisableStateDisabled 28 if !isDisabled { 29 // If the KubernetesApply is in an error state or hasn't deployed anything, 30 // don't reconcile the discovery object. This prevents the reconcilers from 31 // tearing down all the discovery infra on a transient deploy error. 32 return reconcile.Result{}, nil 33 } 34 } 35 36 var existingKD v1alpha1.KubernetesDiscovery 37 err := r.ctrlClient.Get(ctx, nn, &existingKD) 38 isNotFound := apierrors.IsNotFound(err) 39 if err != nil && !isNotFound { 40 return reconcile.Result{}, 41 fmt.Errorf("failed to fetch managed KubernetesDiscovery objects for KubernetesApply %s: %v", 42 nn.Name, err) 43 } 44 45 kd, err := r.toDesiredKubernetesDiscovery(ka) 46 if err != nil { 47 return reconcile.Result{}, fmt.Errorf("generating kubernetesdiscovery: %v", err) 48 } 49 50 if isNotFound { 51 if kd == nil { 52 return reconcile.Result{}, nil // Nothing to do. 53 } 54 55 err := r.ctrlClient.Create(ctx, kd) 56 if err != nil { 57 if apierrors.IsAlreadyExists(err) { 58 return reconcile.Result{RequeueAfter: time.Second}, nil 59 } 60 return reconcile.Result{}, fmt.Errorf("creating kubernetesdiscovery: %v", err) 61 } 62 return reconcile.Result{}, nil 63 } 64 65 if kd == nil { 66 err := r.ctrlClient.Delete(ctx, &existingKD) 67 if err != nil && !apierrors.IsNotFound(err) { 68 return reconcile.Result{}, fmt.Errorf("deleting kubernetesdiscovery: %v", err) 69 } 70 return reconcile.Result{}, nil 71 } 72 73 if !apicmp.DeepEqual(existingKD.Spec, kd.Spec) { 74 existingKD.Spec = kd.Spec 75 err = r.ctrlClient.Update(ctx, &existingKD) 76 if err != nil { 77 if apierrors.IsConflict(err) { 78 return reconcile.Result{RequeueAfter: time.Second}, nil 79 } 80 return reconcile.Result{}, fmt.Errorf("updating kubernetesdiscovery: %v", err) 81 } 82 } 83 84 return reconcile.Result{}, nil 85 } 86 87 // Construct the desired KubernetesDiscovery 88 func (r *Reconciler) toDesiredKubernetesDiscovery(ka *v1alpha1.KubernetesApply) (*v1alpha1.KubernetesDiscovery, error) { 89 if ka == nil { 90 return nil, nil 91 } 92 93 if ka.Status.DisableStatus != nil && ka.Status.DisableStatus.State == v1alpha1.DisableStateDisabled { 94 return nil, nil 95 } 96 97 watchRefs, err := r.toWatchRefs(ka) 98 if err != nil { 99 return nil, err 100 } 101 102 if len(watchRefs) == 0 { 103 return nil, nil 104 } 105 106 kapp := ka.Spec 107 var extraSelectors []metav1.LabelSelector 108 if kapp.KubernetesDiscoveryTemplateSpec != nil { 109 extraSelectors = kapp.KubernetesDiscoveryTemplateSpec.ExtraSelectors 110 } 111 112 kd := &v1alpha1.KubernetesDiscovery{ 113 ObjectMeta: metav1.ObjectMeta{ 114 Name: ka.Name, 115 Annotations: map[string]string{ 116 v1alpha1.AnnotationManifest: ka.Annotations[v1alpha1.AnnotationManifest], 117 v1alpha1.AnnotationSpanID: ka.Annotations[v1alpha1.AnnotationSpanID], 118 }, 119 }, 120 Spec: v1alpha1.KubernetesDiscoverySpec{ 121 Cluster: ka.Spec.Cluster, 122 Watches: watchRefs, 123 ExtraSelectors: extraSelectors, 124 PodLogStreamTemplateSpec: kapp.PodLogStreamTemplateSpec.DeepCopy(), 125 PortForwardTemplateSpec: kapp.PortForwardTemplateSpec.DeepCopy(), 126 }, 127 } 128 129 err = controllerutil.SetControllerReference(ka, kd, r.ctrlClient.Scheme()) 130 if err != nil { 131 return nil, err 132 } 133 return kd, nil 134 } 135 136 // Based on the deployed UIDs, create the list of resources to watch. 137 // 138 // TODO(nick): This currently does a lot of YAML parsing, just to get a few small 139 // metadata fields. We should be able to do better here if it becomes a problem, by either 140 // 1) optimizing the parsing, or 141 // 2) memoizing the Apply -> Discovery function 142 func (r *Reconciler) toWatchRefs(ka *v1alpha1.KubernetesApply) ([]v1alpha1.KubernetesWatchRef, error) { 143 seenNamespaces := make(map[k8s.Namespace]bool) 144 var result []v1alpha1.KubernetesWatchRef 145 if ka.Status.ResultYAML != "" && ka.Spec.DiscoveryStrategy != v1alpha1.KubernetesDiscoveryStrategySelectorsOnly { 146 deployed, err := k8s.ParseYAMLFromString(ka.Status.ResultYAML) 147 if err != nil { 148 return nil, err 149 } 150 deployedRefs := k8s.ToRefList(deployed) 151 152 for _, ref := range deployedRefs { 153 ns := k8s.Namespace(ref.Namespace) 154 if ns == "" { 155 // since this entity is actually deployed, don't fallback to cfgNS 156 ns = k8s.DefaultNamespace 157 } 158 seenNamespaces[ns] = true 159 result = append(result, v1alpha1.KubernetesWatchRef{ 160 UID: string(ref.UID), 161 Namespace: ns.String(), 162 Name: ref.Name, 163 }) 164 } 165 } 166 167 yaml := ka.Spec.YAML 168 if yaml == "" { 169 // for KAs with ApplyCmds, there is no YAML in the Spec, so get it from the Status instead. 170 // We still prefer Spec YAML when available: 171 // 1. Using the spec YAML allows us to start connecting to pods before the image build starts. 172 // 2. If a deployment error clears the Status YAML, we'd lose all the watchers. 173 yaml = ka.Status.ResultYAML 174 } 175 entities, err := k8s.ParseYAMLFromString(yaml) 176 if err != nil { 177 return nil, err 178 } 179 180 for _, e := range entities { 181 ns := k8s.Namespace(e.Meta().GetNamespace()) 182 if ns == "" { 183 apiConfig := r.k8sClient.APIConfig() 184 context, ok := apiConfig.Contexts[apiConfig.CurrentContext] 185 if ok && context.Namespace != "" { 186 ns = k8s.Namespace(context.Namespace) 187 } 188 } 189 if ns == "" { 190 ns = k8s.DefaultNamespace 191 } 192 if !seenNamespaces[ns] { 193 seenNamespaces[ns] = true 194 result = append(result, v1alpha1.KubernetesWatchRef{ 195 Namespace: ns.String(), 196 }) 197 } 198 } 199 200 return result, nil 201 }