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