github.com/tilt-dev/tilt@v0.36.0/internal/controllers/apis/restarton/restarton.go (about)

     1  package restarton
     2  
     3  import (
     4  	"context"
     5  	"time"
     6  
     7  	apierrors "k8s.io/apimachinery/pkg/api/errors"
     8  	"k8s.io/apimachinery/pkg/types"
     9  	"sigs.k8s.io/controller-runtime/pkg/builder"
    10  	"sigs.k8s.io/controller-runtime/pkg/client"
    11  	"sigs.k8s.io/controller-runtime/pkg/handler"
    12  
    13  	"github.com/tilt-dev/tilt/internal/controllers/indexer"
    14  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    15  )
    16  
    17  var fwGVK = v1alpha1.SchemeGroupVersion.WithKind("FileWatch")
    18  var btnGVK = v1alpha1.SchemeGroupVersion.WithKind("UIButton")
    19  
    20  var restartOnTypes = []client.Object{
    21  	&v1alpha1.FileWatch{},
    22  	&v1alpha1.UIButton{},
    23  }
    24  
    25  type ExtractFunc func(obj client.Object) (*v1alpha1.RestartOnSpec, *v1alpha1.StartOnSpec)
    26  
    27  // Objects is a container for objects referenced by a RestartOnSpec and/or StartOnSpec.
    28  type Objects struct {
    29  	UIButtons   map[string]*v1alpha1.UIButton
    30  	FileWatches map[string]*v1alpha1.FileWatch
    31  }
    32  
    33  // SetupController creates watches for types referenced by v1alpha1.RestartOnSpec & v1alpha1.StartOnSpec and registers
    34  // an index function for them.
    35  func SetupController(builder *builder.Builder, idxer *indexer.Indexer, extractFunc ExtractFunc) {
    36  	idxer.AddKeyFunc(
    37  		func(obj client.Object) []indexer.Key {
    38  			restartOn, startOn := extractFunc(obj)
    39  			return extractKeysForIndexer(obj.GetNamespace(), restartOn, startOn)
    40  		})
    41  
    42  	registerWatches(builder, idxer)
    43  }
    44  
    45  // FetchObjects retrieves all objects referenced in either the RestartOnSpec or StartOnSpec.
    46  func FetchObjects(ctx context.Context, client client.Reader, restartOn *v1alpha1.RestartOnSpec, startOn *v1alpha1.StartOnSpec) (Objects, error) {
    47  	buttons, err := Buttons(ctx, client, restartOn, startOn)
    48  	if err != nil {
    49  		return Objects{}, err
    50  	}
    51  
    52  	fileWatches, err := FileWatches(ctx, client, restartOn)
    53  	if err != nil {
    54  		return Objects{}, err
    55  	}
    56  
    57  	return Objects{
    58  		UIButtons:   buttons,
    59  		FileWatches: fileWatches,
    60  	}, nil
    61  }
    62  
    63  // Fetch all the buttons that this object depends on.
    64  //
    65  // If a button isn't in the API server yet, it will simply be missing from the map.
    66  //
    67  // Other errors reaching the API server will be returned to the caller.
    68  //
    69  // TODO(nick): If the user typos a button name, there's currently no feedback
    70  // that this is happening. This is probably the correct product behavior (in particular:
    71  // resources should still run if their restarton button has been deleted).
    72  // We might eventually need some sort of StartOnStatus/RestartOnStatus to express errors
    73  // in lookup.
    74  func Buttons(ctx context.Context, client client.Reader, restartOn *v1alpha1.RestartOnSpec, startOn *v1alpha1.StartOnSpec) (map[string]*v1alpha1.UIButton, error) {
    75  	buttonNames := []string{}
    76  	if startOn != nil {
    77  		buttonNames = append(buttonNames, startOn.UIButtons...)
    78  	}
    79  
    80  	if restartOn != nil {
    81  		buttonNames = append(buttonNames, restartOn.UIButtons...)
    82  	}
    83  
    84  	result := make(map[string]*v1alpha1.UIButton, len(buttonNames))
    85  	for _, n := range buttonNames {
    86  		_, exists := result[n]
    87  		if exists {
    88  			continue
    89  		}
    90  
    91  		b := &v1alpha1.UIButton{}
    92  		err := client.Get(ctx, types.NamespacedName{Name: n}, b)
    93  		if err != nil {
    94  			if apierrors.IsNotFound(err) {
    95  				continue
    96  			}
    97  			return nil, err
    98  		}
    99  		result[n] = b
   100  	}
   101  	return result, nil
   102  }
   103  
   104  // Fetch all the filewatches that this object depends on.
   105  //
   106  // If a filewatch isn't in the API server yet, it will simply be missing from the map.
   107  //
   108  // Other errors reaching the API server will be returned to the caller.
   109  //
   110  // TODO(nick): If the user typos a filewatch name, there's currently no feedback
   111  // that this is happening. This is probably the correct product behavior (in particular:
   112  // resources should still run if their restarton filewatch has been deleted).
   113  // We might eventually need some sort of RestartOnStatus to express errors
   114  // in lookup.
   115  func FileWatches(ctx context.Context, client client.Reader, restartOn *v1alpha1.RestartOnSpec) (map[string]*v1alpha1.FileWatch, error) {
   116  	if restartOn == nil {
   117  		return nil, nil
   118  	}
   119  
   120  	result := make(map[string]*v1alpha1.FileWatch, len(restartOn.FileWatches))
   121  	for _, n := range restartOn.FileWatches {
   122  		fw := &v1alpha1.FileWatch{}
   123  		err := client.Get(ctx, types.NamespacedName{Name: n}, fw)
   124  		if err != nil {
   125  			if apierrors.IsNotFound(err) {
   126  				continue
   127  			}
   128  			return nil, err
   129  		}
   130  		result[n] = fw
   131  	}
   132  	return result, nil
   133  }
   134  
   135  // Fetch the last time a start was requested from this target's dependencies.
   136  //
   137  // Returns the most recent trigger time. If the most recent trigger is a button,
   138  // return the button. Some consumers use the button for text inputs.
   139  func LastStartEvent(startOn *v1alpha1.StartOnSpec, triggerObjs Objects) (time.Time, *v1alpha1.UIButton) {
   140  	latestTime := time.Time{}
   141  	var latestButton *v1alpha1.UIButton
   142  	if startOn == nil {
   143  		return time.Time{}, nil
   144  	}
   145  
   146  	for _, bn := range startOn.UIButtons {
   147  		b, ok := triggerObjs.UIButtons[bn]
   148  		if !ok {
   149  			// ignore missing buttons
   150  			continue
   151  		}
   152  		lastEventTime := b.Status.LastClickedAt
   153  		if !lastEventTime.Time.Before(startOn.StartAfter.Time) && lastEventTime.Time.After(latestTime) {
   154  			latestTime = lastEventTime.Time
   155  			latestButton = b
   156  		}
   157  	}
   158  
   159  	return latestTime, latestButton
   160  }
   161  
   162  // Fetch the last time a restart was requested from this target's dependencies.
   163  //
   164  // Returns the most recent trigger time. If the most recent trigger is a button,
   165  // return the button. Some consumers use the button for text inputs.
   166  func LastRestartEvent(restartOn *v1alpha1.RestartOnSpec, triggerObjs Objects) (time.Time, *v1alpha1.UIButton) {
   167  	cur := time.Time{}
   168  	var latestButton *v1alpha1.UIButton
   169  	if restartOn == nil {
   170  		return cur, nil
   171  	}
   172  
   173  	for _, fwn := range restartOn.FileWatches {
   174  		fw, ok := triggerObjs.FileWatches[fwn]
   175  		if !ok {
   176  			// ignore missing filewatches
   177  			continue
   178  		}
   179  		lastEventTime := fw.Status.LastEventTime
   180  		if lastEventTime.Time.After(cur) {
   181  			cur = lastEventTime.Time
   182  		}
   183  	}
   184  
   185  	for _, bn := range restartOn.UIButtons {
   186  		b, ok := triggerObjs.UIButtons[bn]
   187  		if !ok {
   188  			// ignore missing buttons
   189  			continue
   190  		}
   191  		lastEventTime := b.Status.LastClickedAt
   192  		if lastEventTime.Time.After(cur) {
   193  			cur = lastEventTime.Time
   194  			latestButton = b
   195  		}
   196  	}
   197  
   198  	return cur, latestButton
   199  }
   200  
   201  // registerWatches ensures that reconciliation happens on changes to objects referenced by RestartOnSpec/StartOnSpec.
   202  func registerWatches(builder *builder.Builder, indexer *indexer.Indexer) {
   203  	for _, t := range restartOnTypes {
   204  		// this is arguably overly defensive, but a copy of the type object stub is made
   205  		// to avoid sharing references of it across different reconcilers
   206  		obj := t.DeepCopyObject().(client.Object)
   207  		builder.Watches(obj,
   208  			handler.EnqueueRequestsFromMapFunc(indexer.Enqueue))
   209  	}
   210  }
   211  
   212  // extractKeysForIndexer returns the keys of objects referenced in the RestartOnSpec and/or StartOnSpec.
   213  func extractKeysForIndexer(namespace string, restartOn *v1alpha1.RestartOnSpec, startOn *v1alpha1.StartOnSpec) []indexer.Key {
   214  	var keys []indexer.Key
   215  
   216  	if restartOn != nil {
   217  		for _, name := range restartOn.FileWatches {
   218  			keys = append(keys, indexer.Key{
   219  				Name: types.NamespacedName{Namespace: namespace, Name: name},
   220  				GVK:  fwGVK,
   221  			})
   222  		}
   223  
   224  		for _, name := range restartOn.UIButtons {
   225  			keys = append(keys, indexer.Key{
   226  				Name: types.NamespacedName{Namespace: namespace, Name: name},
   227  				GVK:  btnGVK,
   228  			})
   229  		}
   230  	}
   231  
   232  	if startOn != nil {
   233  		for _, name := range startOn.UIButtons {
   234  			keys = append(keys, indexer.Key{
   235  				Name: types.NamespacedName{Namespace: namespace, Name: name},
   236  				GVK:  btnGVK,
   237  			})
   238  		}
   239  	}
   240  
   241  	return keys
   242  }