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{}