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  }