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 }