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 }