github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/extension/tiltfiles.go (about)

     1  package extension
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	apierrors "k8s.io/apimachinery/pkg/api/errors"
     8  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     9  	"k8s.io/apimachinery/pkg/types"
    10  	errorutil "k8s.io/apimachinery/pkg/util/errors"
    11  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    12  
    13  	"github.com/tilt-dev/tilt/internal/controllers/apicmp"
    14  	"github.com/tilt-dev/tilt/internal/controllers/indexer"
    15  	"github.com/tilt-dev/tilt/pkg/apis"
    16  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    17  	"github.com/tilt-dev/tilt/pkg/model"
    18  )
    19  
    20  var (
    21  	apiGVStr = v1alpha1.SchemeGroupVersion.String()
    22  	apiKind  = "Extension"
    23  	apiType  = metav1.TypeMeta{Kind: apiKind, APIVersion: apiGVStr}
    24  )
    25  
    26  // Reconcile the Tiltfile owned by this Extension.
    27  //
    28  // TODO(nick): This is almost exactly the same as the code for managing PortForwards
    29  // from KubernetesDiscovery. I feel like we're missing some abstraction here.
    30  func (r *Reconciler) manageOwnedTiltfile(ctx context.Context, nn types.NamespacedName, owner *v1alpha1.Extension) error {
    31  	var childList v1alpha1.TiltfileList
    32  	err := indexer.ListOwnedBy(ctx, r.ctrlClient, &childList, nn, apiType)
    33  	if err != nil {
    34  		return fmt.Errorf("failed to fetch managed Tiltfile objects for Extension %s: %v",
    35  			owner.Name, err)
    36  	}
    37  
    38  	child, err := r.toDesiredTiltfile(owner)
    39  	if err != nil {
    40  		return fmt.Errorf("creating tiltfile: %v", err)
    41  	}
    42  
    43  	// Delete all the Tiltfiles that don't match this one.
    44  	errs := []error{}
    45  	foundDesired := false
    46  	for _, existingChild := range childList.Items {
    47  		matches := child != nil && existingChild.Name == child.Name
    48  		if matches {
    49  			foundDesired = true
    50  
    51  			if apicmp.DeepEqual(child.Spec, existingChild.Spec) {
    52  				continue
    53  			}
    54  
    55  			updated := existingChild.DeepCopy()
    56  			updated.Spec = child.Spec
    57  			err := r.ctrlClient.Update(ctx, updated)
    58  			if err != nil && !apierrors.IsNotFound(err) {
    59  				errs = append(errs, fmt.Errorf("updating tiltfile %s: %v", existingChild.Name, err))
    60  			}
    61  			continue
    62  		}
    63  
    64  		// If this does not match the desired child, this child needs to be garbage collected.
    65  		deletedChild := existingChild.DeepCopy()
    66  		err := r.ctrlClient.Delete(ctx, deletedChild)
    67  		if err != nil && !apierrors.IsNotFound(err) {
    68  			errs = append(errs, fmt.Errorf("deleting tiltfile %s: %v", existingChild.Name, err))
    69  		}
    70  	}
    71  
    72  	if !foundDesired && child != nil {
    73  		err := r.ctrlClient.Create(ctx, child)
    74  		if err != nil && !apierrors.IsAlreadyExists(err) {
    75  			errs = append(errs, fmt.Errorf("creating tiltfile %s: %v", child.Name, err))
    76  		}
    77  	}
    78  
    79  	return errorutil.NewAggregate(errs)
    80  }
    81  
    82  // Construct the desired tiltfile. May be nil.
    83  func (r *Reconciler) toDesiredTiltfile(owner *v1alpha1.Extension) (*v1alpha1.Tiltfile, error) {
    84  	if owner == nil {
    85  		return nil, nil
    86  	}
    87  
    88  	// Extensions can be loaded as an independent resource, or can be loaded
    89  	// for their symbols into a tiltfile. If they're loaded for their symbols,
    90  	// we mark them as managedby the tiltfile loader, and don't bother loading
    91  	// the tiltfile independently.
    92  	if owner.Annotations[v1alpha1.AnnotationManagedBy] != "" {
    93  		return nil, nil
    94  	}
    95  
    96  	path := owner.Status.Path
    97  	if path == "" {
    98  		return nil, nil
    99  	}
   100  
   101  	// Each extensions resources get their own group.
   102  	// We prefix it with 'extension' so that all extensions get put together.
   103  	// TODO(nick): Let the user choose the label when they register the extension.
   104  	label := apis.SanitizeLabel(fmt.Sprintf("extension.%s", owner.Name))
   105  	fwName := apis.SanitizeName(fmt.Sprintf("%s:%s", model.TargetTypeConfigs, owner.Name))
   106  	child := &v1alpha1.Tiltfile{
   107  		ObjectMeta: metav1.ObjectMeta{
   108  			Name: owner.Name,
   109  			Annotations: map[string]string{
   110  				v1alpha1.AnnotationManifest: owner.Name,
   111  			},
   112  		},
   113  		Spec: v1alpha1.TiltfileSpec{
   114  			Path: path,
   115  			Labels: map[string]string{
   116  				label: label,
   117  			},
   118  			Args: owner.Spec.Args,
   119  			RestartOn: &v1alpha1.RestartOnSpec{
   120  				// It's OK if this filewatch doesn't exist yet.
   121  				// (Tiltfiles are weird in that the Tiltfile reconciler manages the filewatch.)
   122  				FileWatches: []string{fwName},
   123  			},
   124  		},
   125  	}
   126  	err := controllerutil.SetControllerReference(owner, child, r.ctrlClient.Scheme())
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	return child, nil
   131  }