github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/store/build_result.go (about)

     1  package store
     2  
     3  import (
     4  	"sort"
     5  
     6  	"github.com/distribution/reference"
     7  
     8  	"github.com/tilt-dev/tilt/internal/container"
     9  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    10  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    11  	"github.com/tilt-dev/tilt/pkg/model"
    12  )
    13  
    14  // The results of a build.
    15  //
    16  // If a build is successful, the builder should always return a BuildResult to
    17  // indicate the output artifacts of the build (e.g., any new images).
    18  //
    19  // If a build is not successful, things get messier. Certain types of failure
    20  // may still return a result (e.g., a failed live update might return
    21  // the container IDs where the error happened).
    22  //
    23  // Long-term, we want this interface to be more like Bazel's SpawnResult
    24  // https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/actions/SpawnResult.java#L36
    25  // where a builder always returns a Result, but with a code for each
    26  // of the different failure types.
    27  type BuildResult interface {
    28  	TargetID() model.TargetID
    29  	BuildType() model.BuildType
    30  }
    31  
    32  type LocalBuildResult struct {
    33  	id model.TargetID
    34  }
    35  
    36  func (r LocalBuildResult) TargetID() model.TargetID   { return r.id }
    37  func (r LocalBuildResult) BuildType() model.BuildType { return model.BuildTypeLocal }
    38  
    39  func NewLocalBuildResult(id model.TargetID) LocalBuildResult {
    40  	return LocalBuildResult{
    41  		id: id,
    42  	}
    43  }
    44  
    45  type ImageBuildResult struct {
    46  	id             model.TargetID
    47  	ImageMapStatus v1alpha1.ImageMapStatus
    48  }
    49  
    50  func (r ImageBuildResult) TargetID() model.TargetID   { return r.id }
    51  func (r ImageBuildResult) BuildType() model.BuildType { return model.BuildTypeImage }
    52  
    53  // For image targets.
    54  func NewImageBuildResult(id model.TargetID, localRef, clusterRef reference.NamedTagged) ImageBuildResult {
    55  	return ImageBuildResult{
    56  		id: id,
    57  		ImageMapStatus: v1alpha1.ImageMapStatus{
    58  			Image:            container.FamiliarString(clusterRef),
    59  			ImageFromCluster: container.FamiliarString(clusterRef),
    60  			ImageFromLocal:   container.FamiliarString(localRef),
    61  		},
    62  	}
    63  }
    64  
    65  // When localRef == ClusterRef
    66  func NewImageBuildResultSingleRef(id model.TargetID, ref reference.NamedTagged) ImageBuildResult {
    67  	return NewImageBuildResult(id, ref, ref)
    68  }
    69  
    70  type DockerComposeBuildResult struct {
    71  	id model.TargetID
    72  
    73  	// When we deploy a Docker Compose service, we wait synchronously for the
    74  	// container to start. Note that this is a different concurrency model than
    75  	// we use for Kubernetes, where the pods appear some time later via an
    76  	// asynchronous event.
    77  	Status v1alpha1.DockerComposeServiceStatus
    78  }
    79  
    80  func (r DockerComposeBuildResult) TargetID() model.TargetID   { return r.id }
    81  func (r DockerComposeBuildResult) BuildType() model.BuildType { return model.BuildTypeDockerCompose }
    82  
    83  // For docker compose deploy targets.
    84  func NewDockerComposeDeployResult(id model.TargetID, status v1alpha1.DockerComposeServiceStatus) DockerComposeBuildResult {
    85  	return DockerComposeBuildResult{
    86  		id:     id,
    87  		Status: status,
    88  	}
    89  }
    90  
    91  type K8sBuildResult struct {
    92  	*k8sconv.KubernetesApplyFilter
    93  
    94  	id model.TargetID
    95  }
    96  
    97  func (r K8sBuildResult) TargetID() model.TargetID   { return r.id }
    98  func (r K8sBuildResult) BuildType() model.BuildType { return model.BuildTypeK8s }
    99  
   100  // NewK8sDeployResult creates a deploy result for Kubernetes deploy targets.
   101  func NewK8sDeployResult(id model.TargetID, filter *k8sconv.KubernetesApplyFilter) K8sBuildResult {
   102  	return K8sBuildResult{
   103  		id:                    id,
   104  		KubernetesApplyFilter: filter,
   105  	}
   106  }
   107  
   108  func LocalImageRefFromBuildResult(r BuildResult) string {
   109  	if r, ok := r.(ImageBuildResult); ok {
   110  		return r.ImageMapStatus.ImageFromLocal
   111  	}
   112  	return ""
   113  }
   114  
   115  func ClusterImageRefFromBuildResult(r BuildResult) string {
   116  	if r, ok := r.(ImageBuildResult); ok {
   117  		return r.ImageMapStatus.ImageFromCluster
   118  	}
   119  	return ""
   120  }
   121  
   122  type BuildResultSet map[model.TargetID]BuildResult
   123  
   124  func (set BuildResultSet) ApplyFilter() *k8sconv.KubernetesApplyFilter {
   125  	for _, r := range set {
   126  		r, ok := r.(K8sBuildResult)
   127  		if ok {
   128  			return r.KubernetesApplyFilter
   129  		}
   130  	}
   131  	return nil
   132  }
   133  
   134  func MergeBuildResultsSet(a, b BuildResultSet) BuildResultSet {
   135  	res := make(BuildResultSet)
   136  	for k, v := range a {
   137  		res[k] = v
   138  	}
   139  	for k, v := range b {
   140  		res[k] = v
   141  	}
   142  	return res
   143  }
   144  
   145  func (set BuildResultSet) BuildTypes() []model.BuildType {
   146  	btMap := make(map[model.BuildType]bool, len(set))
   147  	for _, br := range set {
   148  		if br != nil {
   149  			btMap[br.BuildType()] = true
   150  		}
   151  	}
   152  	result := make([]model.BuildType, 0, len(btMap))
   153  	for key := range btMap {
   154  		result = append(result, key)
   155  	}
   156  	return result
   157  }
   158  
   159  // A BuildResultSet that can only hold image build results.
   160  type ImageBuildResultSet map[model.TargetID]ImageBuildResult
   161  
   162  func (s ImageBuildResultSet) ToBuildResultSet() BuildResultSet {
   163  	result := BuildResultSet{}
   164  	for k, v := range s {
   165  		result[k] = v
   166  	}
   167  	return result
   168  }
   169  
   170  // The state of the system since the last successful build.
   171  // This data structure should be considered immutable.
   172  // All methods that return a new BuildState should first clone the existing build state.
   173  type BuildState struct {
   174  	// The last result.
   175  	LastResult BuildResult
   176  
   177  	// Files changed since the last result was build.
   178  	// This must be liberal: it's ok if this has too many files, but not ok if it has too few.
   179  	FilesChangedSet map[string]bool
   180  
   181  	// Dependencies changed since the last result was built
   182  	DepsChangedSet map[model.TargetID]bool
   183  
   184  	// There are three kinds of triggers:
   185  	//
   186  	// 1) If a resource is in trigger_mode=TRIGGER_MODE_AUTO, then the resource auto-builds.
   187  	//    Pressing the trigger will always do a full image build.
   188  	//
   189  	// 2) If a resource is in trigger_mode=TRIGGER_MODE_MANUAL and there are no pending changes,
   190  	//    then pressing the trigger will do a full image build.
   191  	//
   192  	// 3) If a resource is in trigger_mode=TRIGGER_MODE_MANUAL, and there are
   193  	//    pending changes, then pressing the trigger will do a live_update (if one
   194  	//    is configured; otherwise, will do an image build as normal)
   195  	//
   196  	// This field indicates case 1 || case 2 -- i.e. that we should skip
   197  	// live_update, and force an image build (even if there are no changed files)
   198  	FullBuildTriggered bool
   199  
   200  	// The default cluster.
   201  	Cluster *v1alpha1.Cluster
   202  }
   203  
   204  func NewBuildState(result BuildResult, files []string, pendingDeps []model.TargetID) BuildState {
   205  	set := make(map[string]bool, len(files))
   206  	for _, f := range files {
   207  		set[f] = true
   208  	}
   209  	depsSet := make(map[model.TargetID]bool, len(pendingDeps))
   210  	for _, d := range pendingDeps {
   211  		depsSet[d] = true
   212  	}
   213  	return BuildState{
   214  		LastResult:      result,
   215  		FilesChangedSet: set,
   216  		DepsChangedSet:  depsSet,
   217  	}
   218  }
   219  
   220  func (b BuildState) ClusterOrEmpty() *v1alpha1.Cluster {
   221  	if b.Cluster == nil {
   222  		return &v1alpha1.Cluster{}
   223  	}
   224  	return b.Cluster
   225  }
   226  
   227  func (b BuildState) WithFullBuildTriggered(isImageBuildTrigger bool) BuildState {
   228  	b.FullBuildTriggered = isImageBuildTrigger
   229  	return b
   230  }
   231  
   232  func (b BuildState) LastLocalImageAsString() string {
   233  	return LocalImageRefFromBuildResult(b.LastResult)
   234  }
   235  
   236  // Return the files changed since the last result in sorted order.
   237  // The sorting helps ensure that this is deterministic, both for testing
   238  // and for deterministic builds.
   239  func (b BuildState) FilesChanged() []string {
   240  	result := make([]string, 0, len(b.FilesChangedSet))
   241  	for file := range b.FilesChangedSet {
   242  		result = append(result, file)
   243  	}
   244  	sort.Strings(result)
   245  	return result
   246  }
   247  
   248  // A build state is empty if there are no previous results.
   249  func (b BuildState) IsEmpty() bool {
   250  	return b.LastResult == nil
   251  }
   252  
   253  func (b BuildState) HasLastResult() bool {
   254  	return b.LastResult != nil
   255  }
   256  
   257  // Whether the image represented by this state needs to be built.
   258  // If the image has already been built, and no files have been
   259  // changed since then, then we can re-use the previous result.
   260  func (b BuildState) NeedsImageBuild() bool {
   261  	lastBuildWasImgBuild := b.LastResult != nil &&
   262  		b.LastResult.BuildType() == model.BuildTypeImage
   263  	return !lastBuildWasImgBuild ||
   264  		len(b.FilesChangedSet) > 0 ||
   265  		len(b.DepsChangedSet) > 0 ||
   266  		b.FullBuildTriggered
   267  }
   268  
   269  type BuildStateSet map[model.TargetID]BuildState
   270  
   271  func (set BuildStateSet) FullBuildTriggered() bool {
   272  	for _, state := range set {
   273  		if state.FullBuildTriggered {
   274  			return true
   275  		}
   276  	}
   277  	return false
   278  }
   279  
   280  func (set BuildStateSet) Empty() bool {
   281  	return len(set) == 0
   282  }
   283  
   284  func (set BuildStateSet) FilesChanged() []string {
   285  	resultMap := map[string]bool{}
   286  	for _, state := range set {
   287  		for k := range state.FilesChangedSet {
   288  			resultMap[k] = true
   289  		}
   290  	}
   291  
   292  	result := make([]string, 0, len(resultMap))
   293  	for k := range resultMap {
   294  		result = append(result, k)
   295  	}
   296  	sort.Strings(result)
   297  	return result
   298  }
   299  
   300  var BuildStateClean = BuildState{}