github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/store/buildcontrols/reducers.go (about) 1 package buildcontrols 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/tilt-dev/tilt/internal/container" 9 "github.com/tilt-dev/tilt/internal/dockercompose" 10 "github.com/tilt-dev/tilt/internal/k8s" 11 "github.com/tilt-dev/tilt/internal/store" 12 "github.com/tilt-dev/tilt/internal/store/dockercomposeservices" 13 "github.com/tilt-dev/tilt/internal/timecmp" 14 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 15 "github.com/tilt-dev/tilt/pkg/logger" 16 "github.com/tilt-dev/tilt/pkg/model" 17 ) 18 19 const BuildControlSource = "buildcontrol" 20 21 func HandleBuildStarted(ctx context.Context, state *store.EngineState, action BuildStartedAction) { 22 if action.Source == BuildControlSource { 23 state.BuildControllerStartCount++ 24 } 25 26 mn := action.ManifestName 27 manifest, ok := state.Manifest(mn) 28 if !ok { 29 return 30 } 31 32 ms, ok := state.ManifestState(mn) 33 if !ok { 34 return 35 } 36 37 bs := model.BuildRecord{ 38 Edits: append([]string{}, action.FilesChanged...), 39 StartTime: action.StartTime, 40 Reason: action.Reason, 41 SpanID: action.SpanID, 42 } 43 ms.ConfigFilesThatCausedChange = []string{} 44 ms.CurrentBuilds[action.Source] = bs 45 46 if ms.IsK8s() { 47 krs := ms.K8sRuntimeState() 48 podIDSet := map[k8s.PodID]bool{} 49 for _, pod := range krs.GetPods() { 50 podIDSet[k8s.PodID(pod.Name)] = true 51 krs.UpdateStartTime[k8s.PodID(pod.Name)] = action.StartTime 52 } 53 // remove stale pods 54 for podID := range krs.UpdateStartTime { 55 if !podIDSet[podID] { 56 delete(krs.UpdateStartTime, podID) 57 } 58 } 59 } else if manifest.IsDC() { 60 // Attach the SpanID and initialize the runtime state if we haven't yet. 61 state, _ := ms.RuntimeState.(dockercompose.State) 62 state = state.WithSpanID(dockercomposeservices.SpanIDForDCService(mn)) 63 ms.RuntimeState = state 64 } 65 66 state.RemoveFromTriggerQueue(mn) 67 state.CurrentBuildSet[mn] = true 68 } 69 70 // When a Manifest build finishes, update the BuildStatus for all applicable 71 // targets in the engine state. 72 func handleBuildResults(engineState *store.EngineState, 73 mt *store.ManifestTarget, br model.BuildRecord, results store.BuildResultSet) { 74 isBuildSuccess := br.Error == nil 75 76 ms := mt.State 77 mn := mt.Manifest.Name 78 for id, result := range results { 79 ms.MutableBuildStatus(id).LastResult = result 80 } 81 82 // Remove pending file changes that were consumed by this build. 83 for _, status := range ms.BuildStatuses { 84 status.ClearPendingChangesBefore(br.StartTime) 85 } 86 87 if isBuildSuccess { 88 ms.LastSuccessfulDeployTime = br.FinishTime 89 } 90 91 // Update build statuses for duplicated image targets in other manifests. 92 // This ensures that those images targets aren't redundantly rebuilt. 93 for _, currentMT := range engineState.TargetsBesides(mn) { 94 // We only want to update image targets for Manifests that are already queued 95 // for rebuild and not currently building. This has two benefits: 96 // 97 // 1) If there's a bug in Tilt's image caching (e.g., 98 // https://github.com/tilt-dev/tilt/pull/3542), this prevents infinite 99 // builds. 100 // 101 // 2) If the current manifest build was kicked off by a trigger, we don't 102 // want to queue manifests with the same image. 103 if currentMT.NextBuildReason() == model.BuildReasonNone || 104 engineState.IsBuilding(currentMT.Manifest.Name) { 105 continue 106 } 107 108 currentMS := currentMT.State 109 idSet := currentMT.Manifest.TargetIDSet() 110 updatedIDSet := make(map[model.TargetID]bool) 111 112 for id, result := range results { 113 _, ok := idSet[id] 114 if !ok { 115 continue 116 } 117 118 // We can only reuse image update, not live-updates or other kinds of 119 // deploys. 120 _, isImageResult := result.(store.ImageBuildResult) 121 if !isImageResult { 122 continue 123 } 124 125 currentStatus := currentMS.MutableBuildStatus(id) 126 currentStatus.LastResult = result 127 currentStatus.ClearPendingChangesBefore(br.StartTime) 128 updatedIDSet[id] = true 129 } 130 131 if len(updatedIDSet) == 0 { 132 continue 133 } 134 135 // Suppose we built manifestA, which contains imageA depending on imageCommon. 136 // 137 // We also have manifestB, which contains imageB depending on imageCommon. 138 // 139 // We need to mark imageB as dirty, because it was not built in the manifestA 140 // build but its dependency was built. 141 // 142 // Note that this logic also applies to deploy targets depending on image 143 // targets. If we built manifestA, which contains imageX and deploy target 144 // k8sA, and manifestB contains imageX and deploy target k8sB, we need to mark 145 // target k8sB as dirty so that Tilt actually deploys the changes to imageX. 146 rDepsMap := currentMT.Manifest.ReverseDependencyIDs() 147 for updatedID := range updatedIDSet { 148 149 // Go through each target depending on an image we just built. 150 for _, rDepID := range rDepsMap[updatedID] { 151 152 // If that target was also built, it's up-to-date. 153 if updatedIDSet[rDepID] { 154 continue 155 } 156 157 // Otherwise, we need to mark it for rebuild to pick up the new image. 158 currentMS.MutableBuildStatus(rDepID).PendingDependencyChanges[updatedID] = br.StartTime 159 } 160 } 161 } 162 } 163 164 func HandleBuildCompleted(ctx context.Context, engineState *store.EngineState, cb BuildCompleteAction) { 165 mn := cb.ManifestName 166 defer func() { 167 if !engineState.IsBuilding(mn) { 168 delete(engineState.CurrentBuildSet, mn) 169 } 170 }() 171 172 engineState.CompletedBuildCount++ 173 174 mt, ok := engineState.ManifestTargets[mn] 175 if !ok { 176 return 177 } 178 179 err := cb.Error 180 if err != nil { 181 s := fmt.Sprintf("Build Failed: %v", err) 182 183 engineState.LogStore.Append( 184 store.NewLogAction(mt.Manifest.Name, cb.SpanID, logger.ErrorLvl, nil, []byte(s)), 185 engineState.Secrets) 186 } 187 188 ms := mt.State 189 bs := ms.CurrentBuilds[cb.Source] 190 bs.Error = err 191 bs.FinishTime = cb.FinishTime 192 bs.BuildTypes = cb.Result.BuildTypes() 193 if bs.SpanID != "" { 194 bs.WarningCount = len(engineState.LogStore.Warnings(bs.SpanID)) 195 } 196 197 ms.AddCompletedBuild(bs) 198 199 delete(ms.CurrentBuilds, cb.Source) 200 201 handleBuildResults(engineState, mt, bs, cb.Result) 202 203 if !ms.PendingManifestChange.IsZero() && 204 timecmp.BeforeOrEqual(ms.PendingManifestChange, bs.StartTime) { 205 ms.PendingManifestChange = time.Time{} 206 } 207 208 if err != nil { 209 if IsFatalError(err) { 210 engineState.FatalError = err 211 return 212 } 213 } 214 215 manifest := mt.Manifest 216 if manifest.IsK8s() { 217 state := ms.K8sRuntimeState() 218 219 applyFilter := cb.Result.ApplyFilter() 220 if applyFilter != nil && len(applyFilter.DeployedRefs) > 0 { 221 state.ApplyFilter = applyFilter 222 } 223 224 if err == nil { 225 state.HasEverDeployedSuccessfully = true 226 } 227 228 ms.RuntimeState = state 229 } 230 231 if mt.Manifest.IsDC() { 232 state, _ := ms.RuntimeState.(dockercompose.State) 233 234 result := cb.Result[mt.Manifest.DockerComposeTarget().ID()] 235 dcResult, _ := result.(store.DockerComposeBuildResult) 236 cid := dcResult.Status.ContainerID 237 if cid != "" { 238 state = state.WithContainerID(container.ID(cid)) 239 } 240 241 cState := dcResult.Status.ContainerState 242 if cState != nil { 243 state = state.WithContainerState(*cState) 244 state = state.WithPorts(dcResult.Status.PortBindings) 245 } 246 247 ms.RuntimeState = state 248 } 249 250 if mt.Manifest.IsLocal() { 251 lrs := ms.LocalRuntimeState() 252 if err == nil { 253 lt := mt.Manifest.LocalTarget() 254 if lt.ReadinessProbe == nil { 255 // only update the succeeded time if there's no readiness probe 256 lrs.LastReadyOrSucceededTime = time.Now() 257 } 258 if lt.ServeCmd.Empty() { 259 // local resources without a serve command are jobs that run and 260 // terminate; so there's no real runtime status 261 lrs.Status = v1alpha1.RuntimeStatusNotApplicable 262 } 263 } 264 ms.RuntimeState = lrs 265 } 266 }