github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/cmdimage/reconciler.go (about)

     1  package cmdimage
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  
     7  	"k8s.io/apimachinery/pkg/runtime"
     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  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    14  	ctrl "sigs.k8s.io/controller-runtime"
    15  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    16  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    17  
    18  	"github.com/tilt-dev/tilt/internal/build"
    19  	"github.com/tilt-dev/tilt/internal/controllers/apicmp"
    20  	"github.com/tilt-dev/tilt/internal/controllers/core/dockerimage"
    21  	"github.com/tilt-dev/tilt/internal/controllers/indexer"
    22  	"github.com/tilt-dev/tilt/internal/docker"
    23  	"github.com/tilt-dev/tilt/internal/store"
    24  	"github.com/tilt-dev/tilt/internal/store/cmdimages"
    25  	"github.com/tilt-dev/tilt/pkg/apis"
    26  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    27  	"github.com/tilt-dev/tilt/pkg/model"
    28  )
    29  
    30  var clusterGVK = v1alpha1.SchemeGroupVersion.WithKind("Cluster")
    31  
    32  // Manages the CmdImage API object.
    33  type Reconciler struct {
    34  	client   ctrlclient.Client
    35  	st       store.RStore
    36  	indexer  *indexer.Indexer
    37  	docker   docker.Client
    38  	ib       *build.ImageBuilder
    39  	requeuer *indexer.Requeuer
    40  
    41  	mu      sync.Mutex
    42  	results map[types.NamespacedName]*result
    43  }
    44  
    45  var _ reconcile.Reconciler = &Reconciler{}
    46  
    47  func NewReconciler(client ctrlclient.Client, st store.RStore, scheme *runtime.Scheme, docker docker.Client, ib *build.ImageBuilder) *Reconciler {
    48  	return &Reconciler{
    49  		client:   client,
    50  		st:       st,
    51  		indexer:  indexer.NewIndexer(scheme, indexCmdImage),
    52  		docker:   docker,
    53  		ib:       ib,
    54  		requeuer: indexer.NewRequeuer(),
    55  		results:  make(map[types.NamespacedName]*result),
    56  	}
    57  }
    58  
    59  func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    60  	r.mu.Lock()
    61  	defer r.mu.Unlock()
    62  
    63  	nn := req.NamespacedName
    64  	obj := &v1alpha1.CmdImage{}
    65  	err := r.client.Get(ctx, nn, obj)
    66  	if err != nil && !apierrors.IsNotFound(err) {
    67  		return ctrl.Result{}, err
    68  	}
    69  	r.indexer.OnReconcile(nn, obj)
    70  
    71  	if apierrors.IsNotFound(err) || obj.ObjectMeta.DeletionTimestamp != nil {
    72  		delete(r.results, nn)
    73  		r.st.Dispatch(cmdimages.NewCmdImageDeleteAction(nn.Name))
    74  		return ctrl.Result{}, nil
    75  	}
    76  
    77  	r.st.Dispatch(cmdimages.NewCmdImageUpsertAction(obj))
    78  
    79  	err = r.maybeUpdateImageStatus(ctx, nn, obj)
    80  	if err != nil {
    81  		return ctrl.Result{}, err
    82  	}
    83  
    84  	err = r.maybeUpdateImageMapStatus(ctx, nn)
    85  	if err != nil {
    86  		return ctrl.Result{}, err
    87  	}
    88  
    89  	return ctrl.Result{}, nil
    90  }
    91  
    92  // Build the image, and push it if necessary.
    93  //
    94  // The error is simply the "main" build failure reason.
    95  func (r *Reconciler) ForceApply(
    96  	ctx context.Context,
    97  	iTarget model.ImageTarget,
    98  	customBuildCmd *v1alpha1.Cmd,
    99  	cluster *v1alpha1.Cluster,
   100  	imageMaps map[types.NamespacedName]*v1alpha1.ImageMap,
   101  	ps *build.PipelineState) (store.ImageBuildResult, error) {
   102  
   103  	// TODO(nick): It might make sense to reset the ImageMapStatus here
   104  	// to an empty image while the image is building. maybe?
   105  	// I guess it depends on how image reconciliation works, and
   106  	// if you want the live container to keep receiving updates
   107  	// while an image build is going on in parallel.
   108  	startTime := apis.NowMicro()
   109  	nn := types.NamespacedName{Name: iTarget.CmdImageName}
   110  	r.setImageStatus(nn, ToBuildingStatus(iTarget, startTime))
   111  
   112  	// Requeue the reconciler twice: once when the build has started and once
   113  	// after it has finished.
   114  	r.requeuer.Add(nn)
   115  	defer r.requeuer.Add(nn)
   116  
   117  	refs, _, err := r.ib.Build(ctx, iTarget, customBuildCmd, cluster, imageMaps, ps)
   118  	if err != nil {
   119  		r.setImageStatus(nn, ToCompletedFailStatus(iTarget, startTime, err))
   120  		return store.ImageBuildResult{}, err
   121  	}
   122  
   123  	r.setImageStatus(nn, ToCompletedSuccessStatus(iTarget, startTime, refs))
   124  
   125  	buildResult, err := dockerimage.UpdateImageMap(
   126  		ctx, r.docker,
   127  		iTarget, cluster, imageMaps, &startTime, refs)
   128  	if err != nil {
   129  		return store.ImageBuildResult{}, err
   130  	}
   131  	r.setImageMapStatus(nn, iTarget, buildResult.ImageMapStatus)
   132  	return buildResult, nil
   133  }
   134  
   135  func (r *Reconciler) ensureResult(nn types.NamespacedName) *result {
   136  	res, ok := r.results[nn]
   137  	if !ok {
   138  		res = &result{}
   139  		r.results[nn] = res
   140  	}
   141  	return res
   142  }
   143  
   144  func (r *Reconciler) setImageStatus(nn types.NamespacedName, status v1alpha1.CmdImageStatus) {
   145  	r.mu.Lock()
   146  	defer r.mu.Unlock()
   147  
   148  	result := r.ensureResult(nn)
   149  	result.image = status
   150  }
   151  
   152  func (r *Reconciler) setImageMapStatus(nn types.NamespacedName, iTarget model.ImageTarget, status v1alpha1.ImageMapStatus) {
   153  	r.mu.Lock()
   154  	defer r.mu.Unlock()
   155  
   156  	result := r.ensureResult(nn)
   157  	result.imageMapName = iTarget.ImageMapName()
   158  	result.imageMap = status
   159  }
   160  
   161  // Update the CmdImage status if necessary.
   162  func (r *Reconciler) maybeUpdateImageStatus(ctx context.Context, nn types.NamespacedName, obj *v1alpha1.CmdImage) error {
   163  	newStatus := v1alpha1.CmdImageStatus{}
   164  	existing, ok := r.results[nn]
   165  	if ok {
   166  		newStatus = existing.image
   167  	}
   168  
   169  	if apicmp.DeepEqual(obj.Status, newStatus) {
   170  		return nil
   171  	}
   172  
   173  	update := obj.DeepCopy()
   174  	update.Status = *(newStatus.DeepCopy())
   175  
   176  	return r.client.Status().Update(ctx, update)
   177  }
   178  
   179  // Update the ImageMap status if necessary.
   180  func (r *Reconciler) maybeUpdateImageMapStatus(ctx context.Context, nn types.NamespacedName) error {
   181  
   182  	existing, ok := r.results[nn]
   183  	if !ok || existing.imageMapName == "" {
   184  		return nil
   185  	}
   186  
   187  	var obj v1alpha1.ImageMap
   188  	imNN := types.NamespacedName{Name: existing.imageMapName}
   189  	err := r.client.Get(ctx, imNN, &obj)
   190  	if err != nil {
   191  		return client.IgnoreNotFound(err)
   192  	}
   193  
   194  	newStatus := existing.imageMap
   195  	if apicmp.DeepEqual(obj.Status, newStatus) {
   196  		return nil
   197  	}
   198  
   199  	update := obj.DeepCopy()
   200  	update.Status = *(newStatus.DeepCopy())
   201  
   202  	return r.client.Status().Update(ctx, update)
   203  }
   204  
   205  func (r *Reconciler) CreateBuilder(mgr ctrl.Manager) (*builder.Builder, error) {
   206  	b := ctrl.NewControllerManagedBy(mgr).
   207  		For(&v1alpha1.CmdImage{}).
   208  		WatchesRawSource(r.requeuer).
   209  		Watches(&v1alpha1.Cluster{},
   210  			handler.EnqueueRequestsFromMapFunc(r.indexer.Enqueue))
   211  
   212  	return b, nil
   213  }
   214  
   215  func indexCmdImage(obj ctrlclient.Object) []indexer.Key {
   216  	var keys []indexer.Key
   217  
   218  	ci := obj.(*v1alpha1.CmdImage)
   219  	if ci != nil && ci.Spec.Cluster != "" {
   220  		keys = append(keys, indexer.Key{
   221  			Name: types.NamespacedName{
   222  				Namespace: obj.GetNamespace(),
   223  				Name:      ci.Spec.Cluster,
   224  			},
   225  			GVK: clusterGVK,
   226  		})
   227  	}
   228  
   229  	return keys
   230  }
   231  
   232  type result struct {
   233  	image        v1alpha1.CmdImageStatus
   234  	imageMapName string
   235  	imageMap     v1alpha1.ImageMapStatus
   236  }