github.com/grahambrereton-form3/tilt@v0.10.18/internal/store/build_result.go (about) 1 package store 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/docker/distribution/reference" 8 "k8s.io/apimachinery/pkg/types" 9 10 "github.com/windmilleng/tilt/internal/container" 11 "github.com/windmilleng/tilt/internal/dockercompose" 12 "github.com/windmilleng/tilt/internal/k8s" 13 "github.com/windmilleng/tilt/pkg/model" 14 ) 15 16 // The results of a successful build. 17 type BuildResult interface { 18 TargetID() model.TargetID 19 BuildType() model.BuildType 20 Facets() []model.Facet 21 } 22 23 type LocalBuildResult struct { 24 id model.TargetID 25 } 26 27 func (r LocalBuildResult) TargetID() model.TargetID { return r.id } 28 func (r LocalBuildResult) BuildType() model.BuildType { return model.BuildTypeLocal } 29 func (r LocalBuildResult) Facets() []model.Facet { return nil } 30 31 func NewLocalBuildResult(id model.TargetID) LocalBuildResult { 32 return LocalBuildResult{ 33 id: id, 34 } 35 } 36 37 type ImageBuildResult struct { 38 id model.TargetID 39 40 // The name+tag of the image that the pod is running. 41 // 42 // The tag is derived from a content-addressable digest. 43 Image reference.NamedTagged 44 } 45 46 func (r ImageBuildResult) TargetID() model.TargetID { return r.id } 47 func (r ImageBuildResult) BuildType() model.BuildType { return model.BuildTypeImage } 48 func (r ImageBuildResult) Facets() []model.Facet { return nil } 49 50 // For image targets. 51 func NewImageBuildResult(id model.TargetID, image reference.NamedTagged) ImageBuildResult { 52 return ImageBuildResult{ 53 id: id, 54 Image: image, 55 } 56 } 57 58 type LiveUpdateBuildResult struct { 59 id model.TargetID 60 61 // The name+tag of the image that the pod is running. 62 Image reference.NamedTagged 63 64 // The ID of the container(s) that we live-updated in-place. 65 // 66 // The contents of the container have diverged from the image it's built on, 67 // so we need to keep track of that. 68 LiveUpdatedContainerIDs []container.ID 69 } 70 71 func (r LiveUpdateBuildResult) TargetID() model.TargetID { return r.id } 72 func (r LiveUpdateBuildResult) BuildType() model.BuildType { return model.BuildTypeLiveUpdate } 73 func (r LiveUpdateBuildResult) Facets() []model.Facet { return nil } 74 75 // For in-place container updates. 76 func NewLiveUpdateBuildResult(id model.TargetID, image reference.NamedTagged, containerIDs []container.ID) LiveUpdateBuildResult { 77 return LiveUpdateBuildResult{ 78 id: id, 79 Image: image, 80 LiveUpdatedContainerIDs: containerIDs, 81 } 82 } 83 84 type DockerComposeBuildResult struct { 85 id model.TargetID 86 87 // The ID of the container that Docker Compose created. 88 // 89 // When we deploy a Docker Compose service, we wait synchronously for the 90 // container to start. Note that this is a different concurrency model than 91 // we use for Kubernetes, where the pods appear some time later via an 92 // asynchronous event. 93 DockerComposeContainerID container.ID 94 } 95 96 func (r DockerComposeBuildResult) TargetID() model.TargetID { return r.id } 97 func (r DockerComposeBuildResult) BuildType() model.BuildType { return model.BuildTypeDockerCompose } 98 func (r DockerComposeBuildResult) Facets() []model.Facet { return nil } 99 100 // For docker compose deploy targets. 101 func NewDockerComposeDeployResult(id model.TargetID, containerID container.ID) DockerComposeBuildResult { 102 return DockerComposeBuildResult{ 103 id: id, 104 DockerComposeContainerID: containerID, 105 } 106 } 107 108 type K8sBuildResult struct { 109 id model.TargetID 110 111 // The UIDs that we deployed to a Kubernetes cluster. 112 DeployedUIDs []types.UID 113 114 AppliedEntitiesText string 115 } 116 117 func (r K8sBuildResult) TargetID() model.TargetID { return r.id } 118 func (r K8sBuildResult) BuildType() model.BuildType { return model.BuildTypeK8s } 119 func (r K8sBuildResult) Facets() []model.Facet { 120 121 return []model.Facet{ 122 { 123 Name: "applied yaml", 124 Value: r.AppliedEntitiesText, 125 }, 126 } 127 } 128 129 // For kubernetes deploy targets. 130 func NewK8sDeployResult(id model.TargetID, uids []types.UID, appliedEntities []k8s.K8sEntity) BuildResult { 131 appliedEntitiesText, err := k8s.SerializeSpecYAML(appliedEntities) 132 if err != nil { 133 appliedEntitiesText = fmt.Sprintf("unable to serialize entities to yaml: %s", err.Error()) 134 } 135 136 return K8sBuildResult{ 137 id: id, 138 DeployedUIDs: uids, 139 AppliedEntitiesText: appliedEntitiesText, 140 } 141 } 142 143 func ImageFromBuildResult(r BuildResult) reference.NamedTagged { 144 switch r := r.(type) { 145 case ImageBuildResult: 146 return r.Image 147 case LiveUpdateBuildResult: 148 return r.Image 149 } 150 return nil 151 } 152 153 type BuildResultSet map[model.TargetID]BuildResult 154 155 func (set BuildResultSet) LiveUpdatedContainerIDs() []container.ID { 156 result := []container.ID{} 157 for _, r := range set { 158 r, ok := r.(LiveUpdateBuildResult) 159 if ok { 160 result = append(result, r.LiveUpdatedContainerIDs...) 161 } 162 } 163 return result 164 } 165 166 func (set BuildResultSet) DeployedUIDSet() UIDSet { 167 result := NewUIDSet() 168 for _, r := range set { 169 r, ok := r.(K8sBuildResult) 170 if ok { 171 result.Add(r.DeployedUIDs...) 172 } 173 } 174 return result 175 } 176 177 func MergeBuildResultsSet(a, b BuildResultSet) BuildResultSet { 178 res := make(BuildResultSet) 179 for k, v := range a { 180 res[k] = v 181 } 182 for k, v := range b { 183 res[k] = v 184 } 185 return res 186 } 187 188 func (set BuildResultSet) BuildTypes() []model.BuildType { 189 btMap := make(map[model.BuildType]bool, len(set)) 190 for _, br := range set { 191 if br != nil { 192 btMap[br.BuildType()] = true 193 } 194 } 195 result := make([]model.BuildType, 0, len(btMap)) 196 for key := range btMap { 197 result = append(result, key) 198 } 199 return result 200 } 201 202 // Returns a container ID iff it's the only container ID in the result set. 203 // If there are multiple container IDs, we have to give up. 204 func (set BuildResultSet) OneAndOnlyLiveUpdatedContainerID() container.ID { 205 var id container.ID 206 for _, br := range set { 207 result, ok := br.(LiveUpdateBuildResult) 208 if !ok { 209 continue 210 } 211 212 if len(result.LiveUpdatedContainerIDs) == 0 { 213 continue 214 } 215 216 if len(result.LiveUpdatedContainerIDs) > 1 { 217 return "" 218 } 219 220 curID := result.LiveUpdatedContainerIDs[0] 221 if curID == "" { 222 continue 223 } 224 225 if id != "" && curID != id { 226 return "" 227 } 228 229 id = curID 230 } 231 return id 232 } 233 234 // The state of the system since the last successful build. 235 // This data structure should be considered immutable. 236 // All methods that return a new BuildState should first clone the existing build state. 237 type BuildState struct { 238 // The last successful build. 239 LastSuccessfulResult BuildResult 240 241 // Files changed since the last result was build. 242 // This must be liberal: it's ok if this has too many files, but not ok if it has too few. 243 FilesChangedSet map[string]bool 244 245 // Whether there was a manual trigger 246 NeedsForceUpdate bool 247 248 RunningContainers []ContainerInfo 249 250 // If we had an error retrieving running containers 251 RunningContainerError error 252 } 253 254 func NewBuildState(result BuildResult, files []string) BuildState { 255 set := make(map[string]bool, len(files)) 256 for _, f := range files { 257 set[f] = true 258 } 259 return BuildState{ 260 LastSuccessfulResult: result, 261 FilesChangedSet: set, 262 } 263 } 264 265 func (b BuildState) WithRunningContainers(cInfos []ContainerInfo) BuildState { 266 b.RunningContainers = cInfos 267 return b 268 } 269 270 func (b BuildState) WithRunningContainerError(err error) BuildState { 271 b.RunningContainerError = err 272 return b 273 } 274 275 func (b BuildState) WithNeedsForceUpdate(needsForceUpdate bool) BuildState { 276 b.NeedsForceUpdate = needsForceUpdate 277 return b 278 } 279 280 // NOTE(maia): Interim method to replicate old behavior where every 281 // BuildState had a single ContainerInfo 282 func (b BuildState) OneContainerInfo() ContainerInfo { 283 if len(b.RunningContainers) == 0 { 284 return ContainerInfo{} 285 } 286 return b.RunningContainers[0] 287 } 288 func (b BuildState) LastImageAsString() string { 289 img := ImageFromBuildResult(b.LastSuccessfulResult) 290 if img == nil { 291 return "" 292 } 293 return img.String() 294 } 295 296 // Return the files changed since the last result in sorted order. 297 // The sorting helps ensure that this is deterministic, both for testing 298 // and for deterministic builds. 299 func (b BuildState) FilesChanged() []string { 300 result := make([]string, 0, len(b.FilesChangedSet)) 301 for file, _ := range b.FilesChangedSet { 302 result = append(result, file) 303 } 304 sort.Strings(result) 305 return result 306 } 307 308 // A build state is empty if there are no previous results. 309 func (b BuildState) IsEmpty() bool { 310 return b.LastSuccessfulResult == nil 311 } 312 313 func (b BuildState) HasImage() bool { 314 return ImageFromBuildResult(b.LastSuccessfulResult) != nil 315 } 316 317 // Whether the image represented by this state needs to be built. 318 // If the image has already been built, and no files have been 319 // changed since then, then we can re-use the previous result. 320 func (b BuildState) NeedsImageBuild() bool { 321 lastBuildWasImgBuild := b.LastSuccessfulResult != nil && 322 b.LastSuccessfulResult.BuildType() == model.BuildTypeImage 323 return !lastBuildWasImgBuild || len(b.FilesChangedSet) > 0 || b.NeedsForceUpdate 324 } 325 326 type BuildStateSet map[model.TargetID]BuildState 327 328 func (set BuildStateSet) Empty() bool { 329 return len(set) == 0 330 } 331 332 func (set BuildStateSet) FilesChanged() []string { 333 resultMap := map[string]bool{} 334 for _, state := range set { 335 for k := range state.FilesChangedSet { 336 resultMap[k] = true 337 } 338 } 339 340 result := make([]string, 0, len(resultMap)) 341 for k := range resultMap { 342 result = append(result, k) 343 } 344 sort.Strings(result) 345 return result 346 } 347 348 // Information describing a single running & ready container 349 type ContainerInfo struct { 350 PodID k8s.PodID 351 ContainerID container.ID 352 ContainerName container.Name 353 Namespace k8s.Namespace 354 } 355 356 func (c ContainerInfo) Empty() bool { 357 return c == ContainerInfo{} 358 } 359 360 func IDsForInfos(infos []ContainerInfo) []container.ID { 361 ids := make([]container.ID, len(infos)) 362 for i, info := range infos { 363 ids[i] = info.ContainerID 364 } 365 return ids 366 } 367 368 func AllRunningContainers(mt *ManifestTarget) []ContainerInfo { 369 if mt.Manifest.IsDC() { 370 return RunningContainersForDC(mt.State.DCRuntimeState()) 371 } 372 373 var result []ContainerInfo 374 for _, iTarget := range mt.Manifest.ImageTargets { 375 cInfos, err := RunningContainersForTargetForOnePod(iTarget, mt.State.K8sRuntimeState()) 376 if err != nil { 377 // HACK(maia): just don't collect container info for targets running 378 // more than one pod -- we don't support LiveUpdating them anyway, 379 // so no need to monitor those containers for crashes. 380 continue 381 } 382 result = append(result, cInfos...) 383 } 384 return result 385 } 386 387 // If all containers running the given image are ready, returns info for them. 388 // (If this image is running on multiple pods, return an error.) 389 func RunningContainersForTargetForOnePod(iTarget model.ImageTarget, runtimeState K8sRuntimeState) ([]ContainerInfo, error) { 390 if runtimeState.PodLen() > 1 { 391 return nil, fmt.Errorf("can only get container info for a single pod; image target %s has %d pods", iTarget.ID(), runtimeState.PodLen()) 392 } 393 394 if runtimeState.PodLen() == 0 { 395 return nil, nil 396 } 397 398 pod := runtimeState.MostRecentPod() 399 if pod.PodID == "" { 400 return nil, nil 401 } 402 403 // If there was a recent deploy, the runtime state might not have the 404 // new pods yet. We check the PodAncestorID and see if it's in the most 405 // recent deploy set. If it's not, then we can should ignore these pods. 406 ancestorUID := runtimeState.PodAncestorUID 407 if ancestorUID != "" && !runtimeState.DeployedUIDSet.Contains(ancestorUID) { 408 return nil, nil 409 } 410 411 var containers []ContainerInfo 412 for _, c := range pod.Containers { 413 // Only return containers matching our image 414 if c.ImageRef == nil || iTarget.DeploymentRef.Name() != c.ImageRef.Name() { 415 continue 416 } 417 if c.ID == "" || c.Name == "" || !c.Ready { 418 // If we're missing any relevant info for this container, OR if the 419 // container isn't ready, we can't update it in place. 420 // (Since we'll need to fully rebuild this image, we shouldn't bother 421 // in-place updating ANY containers on this pod -- they'll all 422 // be recreated when we image build. So don't return ANY ContainerInfos.) 423 return nil, nil 424 } 425 containers = append(containers, ContainerInfo{ 426 PodID: pod.PodID, 427 ContainerID: c.ID, 428 ContainerName: c.Name, 429 Namespace: pod.Namespace, 430 }) 431 } 432 433 return containers, nil 434 } 435 436 func RunningContainersForDC(state dockercompose.State) []ContainerInfo { 437 return []ContainerInfo{ 438 ContainerInfo{ContainerID: state.ContainerID}, 439 } 440 } 441 442 var BuildStateClean = BuildState{}