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