github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/togglebutton/reconciler.go (about) 1 package togglebutton 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "k8s.io/apimachinery/pkg/runtime" 9 10 "github.com/pkg/errors" 11 apierrors "k8s.io/apimachinery/pkg/api/errors" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/types" 14 ctrl "sigs.k8s.io/controller-runtime" 15 "sigs.k8s.io/controller-runtime/pkg/builder" 16 "sigs.k8s.io/controller-runtime/pkg/client" 17 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 18 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 19 "sigs.k8s.io/controller-runtime/pkg/handler" 20 "sigs.k8s.io/controller-runtime/pkg/reconcile" 21 22 "github.com/tilt-dev/tilt/internal/controllers/indexer" 23 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 24 ) 25 26 const ( 27 actionUIInputName = "action" 28 turnOnInputValue = "on" 29 turnOffInputValue = "off" 30 ) 31 32 type Reconciler struct { 33 ctrlClient ctrlclient.Client 34 indexer *indexer.Indexer 35 lastClickProcessTimes map[string]time.Time 36 } 37 38 func (r *Reconciler) CreateBuilder(mgr ctrl.Manager) (*builder.Builder, error) { 39 b := ctrl.NewControllerManagedBy(mgr). 40 For(&v1alpha1.ToggleButton{}). 41 Watches(&v1alpha1.ConfigMap{}, 42 handler.EnqueueRequestsFromMapFunc(r.indexer.Enqueue)). 43 Owns(&v1alpha1.UIButton{}) 44 45 return b, nil 46 } 47 48 func NewReconciler(ctrlClient ctrlclient.Client, scheme *runtime.Scheme) *Reconciler { 49 return &Reconciler{ 50 ctrlClient: ctrlClient, 51 indexer: indexer.NewIndexer(scheme, indexToggleButton), 52 lastClickProcessTimes: make(map[string]time.Time), 53 } 54 } 55 56 func indexToggleButton(obj client.Object) []indexer.Key { 57 var result []indexer.Key 58 toggleButton := obj.(*v1alpha1.ToggleButton) 59 bGVK := v1alpha1.SchemeGroupVersion.WithKind("ConfigMap") 60 61 if toggleButton != nil { 62 if toggleButton.Spec.StateSource.ConfigMap != nil { 63 result = append(result, indexer.Key{ 64 Name: types.NamespacedName{Name: toggleButton.Spec.StateSource.ConfigMap.Name}, 65 GVK: bGVK, 66 }) 67 } 68 } 69 return result 70 } 71 72 func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { 73 nn := request.NamespacedName 74 75 tb := &v1alpha1.ToggleButton{} 76 err := r.ctrlClient.Get(ctx, nn, tb) 77 r.indexer.OnReconcile(nn, tb) 78 if client.IgnoreNotFound(err) != nil { 79 return ctrl.Result{}, err 80 } 81 82 if apierrors.IsNotFound(err) || !tb.ObjectMeta.DeletionTimestamp.IsZero() { 83 err := r.managedOwnedUIButton(ctx, nn, nil, false) 84 return ctrl.Result{}, err 85 } 86 87 origError := tb.Status.Error 88 // clear the error - if its conditions still apply, it will get re-set 89 tb = tb.DeepCopy() 90 tb.Status.Error = "" 91 92 err = r.processClick(ctx, tb) 93 if err != nil { 94 return ctrl.Result{}, err 95 } 96 97 isOn, err := r.isOn(ctx, tb) 98 if err != nil { 99 return ctrl.Result{}, err 100 } 101 102 err = r.managedOwnedUIButton(ctx, nn, tb, isOn) 103 if err != nil { 104 return ctrl.Result{}, err 105 } 106 107 if origError != tb.Status.Error { 108 err := r.ctrlClient.Status().Update(ctx, tb) 109 if err != nil { 110 return ctrl.Result{}, err 111 } 112 } 113 114 return ctrl.Result{}, err 115 } 116 117 func (r *Reconciler) processClick(ctx context.Context, tb *v1alpha1.ToggleButton) error { 118 uiButton := v1alpha1.UIButton{} 119 err := r.ctrlClient.Get(ctx, types.NamespacedName{Name: uibuttonName(tb.Name)}, &uiButton) 120 if err != nil { 121 if apierrors.IsNotFound(err) { 122 return nil 123 } else { 124 return err 125 } 126 } 127 128 // if there's a new click, pass the new value through to the ConfigMap 129 if uiButton.Status.LastClickedAt.After(r.lastClickProcessTimes[tb.Name]) { 130 foundInput := false 131 var isOn bool 132 for _, input := range uiButton.Status.Inputs { 133 if input.Name == actionUIInputName { 134 if input.Hidden == nil { 135 tb.Status.Error = fmt.Sprintf( 136 "button %q input %q was not of type 'Hidden'", 137 uiButton.Name, 138 input.Name, 139 ) 140 return nil 141 } 142 switch input.Hidden.Value { 143 case turnOnInputValue: 144 isOn = true 145 case turnOffInputValue: 146 isOn = false 147 default: 148 tb.Status.Error = fmt.Sprintf("button %q input %q had unexpected value %q", uiButton.Name, input.Name, input.Hidden.Value) 149 return nil 150 } 151 foundInput = true 152 break 153 } 154 } 155 156 if !foundInput { 157 tb.Status.Error = fmt.Sprintf("UIButton %q does not have an input named %q", uiButton.Name, actionUIInputName) 158 return nil 159 } 160 161 ss := tb.Spec.StateSource.ConfigMap 162 if ss == nil { 163 tb.Status.Error = "Spec.StateSource.ConfigMap is nil" 164 return nil 165 } 166 var cm v1alpha1.ConfigMap 167 err := r.ctrlClient.Get(ctx, types.NamespacedName{Name: ss.Name}, &cm) 168 if err != nil { 169 return errors.Wrap(err, "fetching ToggleButton ConfigMap") 170 } 171 172 var newValue string 173 if isOn { 174 newValue = ss.OnValue 175 } else { 176 newValue = ss.OffValue 177 } 178 179 if cm.Data == nil { 180 cm.Data = make(map[string]string) 181 } 182 183 currentValue, ok := cm.Data[ss.Key] 184 185 if !ok || currentValue != newValue { 186 cm.Data[ss.Key] = newValue 187 err := r.ctrlClient.Update(ctx, &cm) 188 if err != nil { 189 return errors.Wrap(err, "updating ConfigMap with ToggleButton value") 190 } 191 } 192 193 r.lastClickProcessTimes[tb.Name] = time.Now() 194 } 195 196 return nil 197 } 198 199 func (r *Reconciler) isOn(ctx context.Context, tb *v1alpha1.ToggleButton) (bool, error) { 200 isOn := tb.Spec.DefaultOn 201 ss := tb.Spec.StateSource.ConfigMap 202 if ss == nil { 203 tb.Status.Error = "Spec.StateSource.ConfigMap is nil" 204 return isOn, nil 205 } 206 var cm v1alpha1.ConfigMap 207 err := r.ctrlClient.Get(ctx, types.NamespacedName{Name: ss.Name}, &cm) 208 if client.IgnoreNotFound(err) != nil { 209 return false, errors.Wrapf(err, "fetching ToggleButton %q ConfigMap %q", tb.Name, ss.Name) 210 } 211 212 if apierrors.IsNotFound(err) { 213 tb.Status.Error = fmt.Sprintf("no such ConfigMap %q", ss.Name) 214 return isOn, nil 215 } 216 217 if cm.Data != nil { 218 cmVal, ok := cm.Data[ss.Key] 219 if ok { 220 switch cmVal { 221 case ss.OnValue: 222 isOn = true 223 case ss.OffValue: 224 isOn = false 225 default: 226 tb.Status.Error = fmt.Sprintf( 227 "ConfigMap %q key %q has unknown value %q. expected %q or %q", 228 ss.Name, 229 ss.Key, 230 cmVal, 231 ss.OnValue, 232 ss.OffValue) 233 return isOn, nil 234 } 235 } 236 } 237 238 return isOn, nil 239 } 240 241 func (r *Reconciler) managedOwnedUIButton(ctx context.Context, nn types.NamespacedName, tb *v1alpha1.ToggleButton, isOn bool) error { 242 b := &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: uibuttonName(nn.Name), Namespace: nn.Namespace}} 243 244 if tb == nil { 245 err := r.ctrlClient.Delete(ctx, b) 246 return ctrlclient.IgnoreNotFound(err) 247 } 248 249 _, err := ctrl.CreateOrUpdate(ctx, r.ctrlClient, b, func() error { 250 return r.configureUIButton(b, isOn, tb) 251 }) 252 if err != nil { 253 return errors.Wrapf(err, "upserting ToggleButton %q's UIButton", tb.Name) 254 } 255 256 return nil 257 } 258 259 func uibuttonName(tbName string) string { 260 return fmt.Sprintf("toggle-%s", tbName) 261 } 262 263 func (r *Reconciler) configureUIButton(b *v1alpha1.UIButton, isOn bool, tb *v1alpha1.ToggleButton) error { 264 var stateSpec v1alpha1.ToggleButtonStateSpec 265 var value string 266 if isOn { 267 stateSpec = tb.Spec.On 268 value = turnOffInputValue 269 } else { 270 stateSpec = tb.Spec.Off 271 value = turnOnInputValue 272 } 273 274 if b.Annotations == nil { 275 b.Annotations = make(map[string]string) 276 } 277 b.Annotations[v1alpha1.AnnotationButtonType] = tb.Annotations[v1alpha1.AnnotationButtonType] 278 b.Spec = v1alpha1.UIButtonSpec{ 279 Location: tb.Spec.Location, 280 Text: stateSpec.Text, 281 IconName: stateSpec.IconName, 282 IconSVG: stateSpec.IconSVG, 283 RequiresConfirmation: stateSpec.RequiresConfirmation, 284 Inputs: []v1alpha1.UIInputSpec{ 285 { 286 Name: actionUIInputName, 287 Hidden: &v1alpha1.UIHiddenInputSpec{Value: value}, 288 }, 289 }, 290 } 291 292 err := controllerutil.SetControllerReference(tb, b, r.ctrlClient.Scheme()) 293 if err != nil { 294 return errors.Wrapf(err, "setting ToggleButton %q's UIButton's controller reference", tb.Name) 295 } 296 297 return nil 298 }