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  }