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

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