github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/kubernetesdiscovery/portforwards.go (about) 1 package kubernetesdiscovery 2 3 import ( 4 "context" 5 "fmt" 6 7 v1 "k8s.io/api/core/v1" 8 apierrors "k8s.io/apimachinery/pkg/api/errors" 9 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 "k8s.io/apimachinery/pkg/types" 11 errorutil "k8s.io/apimachinery/pkg/util/errors" 12 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 13 14 "github.com/tilt-dev/tilt/internal/controllers/apicmp" 15 "github.com/tilt-dev/tilt/internal/controllers/indexer" 16 "github.com/tilt-dev/tilt/internal/store" 17 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 18 "github.com/tilt-dev/tilt/pkg/logger" 19 ) 20 21 // Reconcile all the port forwards owned by this KD. The KD may be nil if it's being deleted. 22 func (r *Reconciler) manageOwnedPortForwards(ctx context.Context, nn types.NamespacedName, kd *v1alpha1.KubernetesDiscovery) error { 23 var pfList v1alpha1.PortForwardList 24 err := indexer.ListOwnedBy(ctx, r.ctrlClient, &pfList, nn, apiType) 25 if err != nil { 26 return fmt.Errorf("failed to fetch managed PortForward objects for KubernetesDiscovery %s: %v", 27 nn.Name, err) 28 } 29 30 pf, err := r.toDesiredPortForward(kd) 31 if err != nil { 32 return fmt.Errorf("creating portforward: %v", err) 33 } 34 35 // Delete all the port-forwards that don't match this one. 36 errs := []error{} 37 foundDesired := false 38 for _, existingPF := range pfList.Items { 39 matchesPF := pf != nil && existingPF.Name == pf.Name 40 if matchesPF { 41 foundDesired = true 42 43 // If this PortForward is already in the APIServer, make sure it's up-to-date. 44 if apicmp.DeepEqual(pf.Spec, existingPF.Spec) { 45 continue 46 } 47 48 updatedPF := existingPF.DeepCopy() 49 updatedPF.Spec = pf.Spec 50 err := r.ctrlClient.Update(ctx, updatedPF) 51 if err != nil && !apierrors.IsNotFound(err) { 52 errs = append(errs, fmt.Errorf("updating portforward %s: %v", existingPF.Name, err)) 53 } else { 54 warnDeprecatedImplicitForwards(ctx, kd, pf) 55 } 56 continue 57 } 58 59 // If this does not match the desired PF, this PF needs to be garbage collected. 60 deletedPF := existingPF.DeepCopy() 61 err := r.ctrlClient.Delete(ctx, deletedPF) 62 if err != nil && !apierrors.IsNotFound(err) { 63 errs = append(errs, fmt.Errorf("deleting portforward %s: %v", existingPF.Name, err)) 64 } 65 } 66 67 if !foundDesired && pf != nil { 68 err := r.ctrlClient.Create(ctx, pf) 69 if err != nil && !apierrors.IsAlreadyExists(err) { 70 errs = append(errs, fmt.Errorf("creating portforward %s: %v", pf.Name, err)) 71 } else { 72 warnDeprecatedImplicitForwards(ctx, kd, pf) 73 } 74 } 75 76 return errorutil.NewAggregate(errs) 77 } 78 79 // Construct the desired port-forward. May be nil. 80 func (r *Reconciler) toDesiredPortForward(kd *v1alpha1.KubernetesDiscovery) (*v1alpha1.PortForward, error) { 81 if kd == nil { 82 return nil, nil 83 } 84 85 pfTemplate := kd.Spec.PortForwardTemplateSpec 86 if pfTemplate == nil { 87 return nil, nil 88 } 89 90 pod := PickBestPortForwardPod(kd) 91 if pod == nil { 92 return nil, nil 93 } 94 95 pf := &v1alpha1.PortForward{ 96 ObjectMeta: metav1.ObjectMeta{ 97 Name: fmt.Sprintf("%s-%s", kd.Name, pod.Name), 98 Namespace: kd.Namespace, 99 Annotations: map[string]string{ 100 v1alpha1.AnnotationManifest: kd.Annotations[v1alpha1.AnnotationManifest], 101 v1alpha1.AnnotationSpanID: kd.Annotations[v1alpha1.AnnotationSpanID], 102 }, 103 }, 104 Spec: v1alpha1.PortForwardSpec{ 105 PodName: pod.Name, 106 Namespace: pod.Namespace, 107 Forwards: populateContainerPorts(pfTemplate, pod), 108 Cluster: kd.Spec.Cluster, 109 }, 110 } 111 err := controllerutil.SetControllerReference(kd, pf, r.ctrlClient.Scheme()) 112 if err != nil { 113 return nil, err 114 } 115 return pf, nil 116 } 117 118 // If any of the port-forward specs have ContainerPort = 0, populate them with 119 // the documented ports on the pod. If there's no default documented ports for 120 // the pod, populate it with the local port. 121 // 122 // TODO(nick): This is old legacy behavior, and I'm not totally sure it even 123 // makes sense. I wonder if we should just insist that ContainerPort is populated. 124 func populateContainerPorts(pft *v1alpha1.PortForwardTemplateSpec, pod *v1alpha1.Pod) []v1alpha1.Forward { 125 result := make([]v1alpha1.Forward, len(pft.Forwards)) 126 127 cPorts := store.AllPodContainerPorts(*pod) 128 for i := range pft.Forwards { 129 forward := pft.Forwards[i].DeepCopy() 130 if forward.ContainerPort == 0 && len(cPorts) > 0 { 131 forward.ContainerPort = cPorts[0] 132 for _, cPort := range cPorts { 133 if int(forward.LocalPort) == int(cPort) { 134 forward.ContainerPort = cPort 135 break 136 } 137 } 138 } 139 if forward.ContainerPort == 0 { 140 forward.ContainerPort = forward.LocalPort 141 } 142 result[i] = *forward 143 } 144 return result 145 } 146 147 // We can only portforward to one pod at a time. 148 // So pick the "best" pod to portforward to. 149 // May be nil if there is no eligible pod. 150 func PickBestPortForwardPod(kd *v1alpha1.KubernetesDiscovery) *v1alpha1.Pod { 151 var bestPod *v1alpha1.Pod 152 for _, pod := range kd.Status.Pods { 153 pod := pod 154 if pod.Name == "" { 155 continue 156 } 157 158 // Only do port-forwarding if the pod is running or deleting. 159 isRunning := pod.Phase == string(v1.PodRunning) 160 isDeleting := pod.Deleting 161 if !isRunning && !isDeleting { 162 continue 163 } 164 165 // This pod is eligible! Now compare it to the existing candidate (if there is one). 166 if bestPod == nil || isBetterPortForwardPod(&pod, bestPod) { 167 bestPod = &pod 168 } 169 } 170 return bestPod 171 } 172 173 // Check if podA is better than podB for port-forwarding. 174 func isBetterPortForwardPod(podA, podB *v1alpha1.Pod) bool { 175 // A non-deleting pod is always better than a deleting pod. 176 if podB.Deleting && !podA.Deleting { 177 return true 178 } else if podA.Deleting && !podB.Deleting { 179 return false 180 } 181 182 // Otherwise, a more recent pod is better. 183 if podA.CreatedAt.After(podB.CreatedAt.Time) { 184 return true 185 } else if podB.CreatedAt.After(podA.CreatedAt.Time) { 186 return false 187 } 188 189 // Use the name as a tie-breaker. 190 return podA.Name > podB.Name 191 } 192 193 func warnDeprecatedImplicitForwards(ctx context.Context, kd *v1alpha1.KubernetesDiscovery, pf *v1alpha1.PortForward) { 194 if kd == nil || pf == nil { 195 return 196 } 197 resourceName := kd.Annotations[v1alpha1.AnnotationManifest] 198 if resourceName == "" { 199 return 200 } 201 202 for _, pft := range kd.Spec.PortForwardTemplateSpec.Forwards { 203 if pft.ContainerPort != 0 { 204 continue 205 } 206 207 for _, f := range pf.Spec.Forwards { 208 if pft.LocalPort == f.LocalPort && f.LocalPort != f.ContainerPort { 209 logger.Get(ctx).Warnf( 210 "k8s_resource(name='%s', port_forwards='%d') currently maps localhost:%d to port %d in your container.\n"+ 211 "A future version of Tilt will change this default and will map localhost:%d to port %d in your container.\n"+ 212 "To keep your project working, change your Tiltfile to k8s_resource(name='%s', port_forwards='%d:%d')", 213 resourceName, // name=%s 214 f.LocalPort, // port_forward=%d 215 f.LocalPort, // localhost:%d 216 f.ContainerPort, // to port %d (deprecated) 217 f.LocalPort, // localhost:%d 218 f.LocalPort, // to port %d (new) 219 resourceName, // name=%s 220 f.LocalPort, // port_forward='%d:x' 221 f.ContainerPort, // port_forward='x:%d' 222 ) 223 } 224 } 225 } 226 }