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 }