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  }