github.com/grahambrereton-form3/tilt@v0.10.18/internal/store/engine_state.go (about)

     1  package store
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"sort"
     8  	"time"
     9  
    10  	"github.com/windmilleng/wmclient/pkg/analytics"
    11  
    12  	tiltanalytics "github.com/windmilleng/tilt/internal/analytics"
    13  	"github.com/windmilleng/tilt/internal/container"
    14  	"github.com/windmilleng/tilt/internal/dockercompose"
    15  	"github.com/windmilleng/tilt/internal/hud/view"
    16  	"github.com/windmilleng/tilt/internal/ospath"
    17  	"github.com/windmilleng/tilt/internal/token"
    18  	"github.com/windmilleng/tilt/pkg/model"
    19  )
    20  
    21  type EngineState struct {
    22  	TiltBuildInfo model.TiltBuild
    23  	TiltStartTime time.Time
    24  
    25  	// saved so that we can render in order
    26  	ManifestDefinitionOrder []model.ManifestName
    27  
    28  	// TODO(nick): This will eventually be a general Target index.
    29  	ManifestTargets map[model.ManifestName]*ManifestTarget
    30  
    31  	CurrentlyBuilding model.ManifestName
    32  	WatchFiles        bool
    33  
    34  	// How many builds have been completed (pass or fail) since starting tilt
    35  	CompletedBuildCount int
    36  
    37  	// For synchronizing BuildController so that it's only
    38  	// doing one action at a time. In the future, we might
    39  	// want to allow it to parallelize builds better, but that
    40  	// would require better tools for triaging output to different streams.
    41  	BuildControllerActionCount int
    42  
    43  	FatalError error
    44  	HUDEnabled bool
    45  
    46  	// The user has indicated they want to exit
    47  	UserExited bool
    48  
    49  	// The full log stream for tilt. This might deserve gc or file storage at some point.
    50  	Log model.Log `testdiff:"ignore"`
    51  
    52  	TiltfilePath             string
    53  	ConfigFiles              []string
    54  	TiltIgnoreContents       string
    55  	PendingConfigFileChanges map[string]time.Time
    56  
    57  	// InitManifests is the list of manifest names that we were told to init from the CLI.
    58  	InitManifests []model.ManifestName
    59  
    60  	TriggerQueue []model.ManifestName
    61  
    62  	LogTimestamps bool
    63  	IsProfiling   bool
    64  
    65  	TiltfileState ManifestState
    66  
    67  	// from GitHub
    68  	LatestTiltBuild model.TiltBuild
    69  
    70  	// Analytics Info
    71  
    72  	AnalyticsUserOpt       analytics.Opt // changes to this field will propagate into the TiltAnalytics subscriber + we'll record them as user choice
    73  	AnalyticsTiltfileOpt   analytics.Opt // Set by the Tiltfile. Overrides the UserOpt.
    74  	AnalyticsNudgeSurfaced bool          // this flag is set the first time we show the analytics nudge to the user.
    75  
    76  	Features map[string]bool
    77  
    78  	Secrets model.SecretSet
    79  
    80  	CloudAddress string
    81  	Token        token.Token
    82  	TeamName     string
    83  
    84  	TiltCloudUsername                           string
    85  	TokenKnownUnregistered                      bool // to distinguish whether an empty TiltCloudUsername means "we haven't checked" or "we checked and the token isn't registered"
    86  	WaitingForTiltCloudUsernamePostRegistration bool
    87  
    88  	DockerPruneSettings model.DockerPruneSettings
    89  }
    90  
    91  // Merge analytics opt-in status from different sources.
    92  // The Tiltfile opt-in takes precedence over the user opt-in.
    93  func (e *EngineState) AnalyticsEffectiveOpt() analytics.Opt {
    94  	if tiltanalytics.IsAnalyticsDisabledFromEnv() {
    95  		return analytics.OptOut
    96  	}
    97  	if e.AnalyticsTiltfileOpt != analytics.OptDefault {
    98  		return e.AnalyticsTiltfileOpt
    99  	}
   100  	return e.AnalyticsUserOpt
   101  }
   102  
   103  func (e *EngineState) ManifestNamesForTargetID(id model.TargetID) []model.ManifestName {
   104  	result := make([]model.ManifestName, 0)
   105  	for mn, mt := range e.ManifestTargets {
   106  		manifest := mt.Manifest
   107  		for _, iTarget := range manifest.ImageTargets {
   108  			if iTarget.ID() == id {
   109  				result = append(result, mn)
   110  			}
   111  		}
   112  		if manifest.K8sTarget().ID() == id {
   113  			result = append(result, mn)
   114  		}
   115  		if manifest.DockerComposeTarget().ID() == id {
   116  			result = append(result, mn)
   117  		}
   118  		if manifest.LocalTarget().ID() == id {
   119  			result = append(result, mn)
   120  		}
   121  	}
   122  	return result
   123  }
   124  
   125  func (e *EngineState) BuildStatus(id model.TargetID) BuildStatus {
   126  	mns := e.ManifestNamesForTargetID(id)
   127  	for _, mn := range mns {
   128  		ms := e.ManifestTargets[mn].State
   129  		bs := ms.BuildStatus(id)
   130  		if !bs.IsEmpty() {
   131  			return bs
   132  		}
   133  	}
   134  	return BuildStatus{}
   135  }
   136  
   137  func (e *EngineState) UpsertManifestTarget(mt *ManifestTarget) {
   138  	mn := mt.Manifest.Name
   139  	_, ok := e.ManifestTargets[mn]
   140  	if !ok {
   141  		e.ManifestDefinitionOrder = append(e.ManifestDefinitionOrder, mn)
   142  	}
   143  	e.ManifestTargets[mn] = mt
   144  }
   145  
   146  func (e EngineState) Manifest(mn model.ManifestName) (model.Manifest, bool) {
   147  	m, ok := e.ManifestTargets[mn]
   148  	if !ok {
   149  		return model.Manifest{}, ok
   150  	}
   151  	return m.Manifest, ok
   152  }
   153  
   154  func (e EngineState) ManifestState(mn model.ManifestName) (*ManifestState, bool) {
   155  	m, ok := e.ManifestTargets[mn]
   156  	if !ok {
   157  		return nil, ok
   158  	}
   159  	return m.State, ok
   160  }
   161  
   162  // Returns Manifests in a stable order
   163  func (e EngineState) Manifests() []model.Manifest {
   164  	result := make([]model.Manifest, 0, len(e.ManifestTargets))
   165  	for _, mn := range e.ManifestDefinitionOrder {
   166  		mt, ok := e.ManifestTargets[mn]
   167  		if !ok {
   168  			continue
   169  		}
   170  		result = append(result, mt.Manifest)
   171  	}
   172  	return result
   173  }
   174  
   175  // Returns ManifestStates in a stable order
   176  func (e EngineState) ManifestStates() []*ManifestState {
   177  	result := make([]*ManifestState, 0, len(e.ManifestTargets))
   178  	for _, mn := range e.ManifestDefinitionOrder {
   179  		mt, ok := e.ManifestTargets[mn]
   180  		if !ok {
   181  			continue
   182  		}
   183  		result = append(result, mt.State)
   184  	}
   185  	return result
   186  }
   187  
   188  // Returns ManifestTargets in a stable order
   189  func (e EngineState) Targets() []*ManifestTarget {
   190  	result := make([]*ManifestTarget, 0, len(e.ManifestTargets))
   191  	for _, mn := range e.ManifestDefinitionOrder {
   192  		mt, ok := e.ManifestTargets[mn]
   193  		if !ok {
   194  			continue
   195  		}
   196  		result = append(result, mt)
   197  	}
   198  	return result
   199  }
   200  
   201  func (e *EngineState) ManifestInTriggerQueue(mn model.ManifestName) bool {
   202  	for _, queued := range e.TriggerQueue {
   203  		if queued == mn {
   204  			return true
   205  		}
   206  	}
   207  	return false
   208  }
   209  
   210  func (e EngineState) RelativeTiltfilePath() (string, error) {
   211  	wd, err := os.Getwd()
   212  	if err != nil {
   213  		return "", err
   214  	}
   215  	return filepath.Rel(wd, e.TiltfilePath)
   216  }
   217  
   218  func (e EngineState) IsEmpty() bool {
   219  	return len(e.ManifestTargets) == 0
   220  }
   221  
   222  func (e EngineState) LastTiltfileError() error {
   223  	return e.TiltfileState.LastBuild().Error
   224  }
   225  
   226  func (e *EngineState) HasDockerBuild() bool {
   227  	for _, m := range e.Manifests() {
   228  		for _, targ := range m.ImageTargets {
   229  			if targ.IsDockerBuild() {
   230  				return true
   231  			}
   232  		}
   233  	}
   234  	return false
   235  }
   236  
   237  func (e *EngineState) InitialBuildsCompleted() bool {
   238  	if e.ManifestTargets == nil || len(e.ManifestTargets) == 0 {
   239  		return false
   240  	}
   241  
   242  	for _, mt := range e.ManifestTargets {
   243  		if !mt.Manifest.TriggerMode.AutoInitial() {
   244  			continue
   245  		}
   246  
   247  		ms, _ := e.ManifestState(mt.Manifest.Name)
   248  		if ms == nil || ms.LastBuild().Empty() {
   249  			return false
   250  		}
   251  	}
   252  
   253  	return true
   254  }
   255  
   256  // TODO(nick): This will eventually implement TargetStatus
   257  type BuildStatus struct {
   258  	// Stores the times of all the pending changes,
   259  	// so we can prioritize the oldest one first.
   260  	// This map is mutable.
   261  	PendingFileChanges map[string]time.Time
   262  
   263  	LastSuccessfulResult BuildResult
   264  	LastResult           BuildResult
   265  }
   266  
   267  func newBuildStatus() *BuildStatus {
   268  	return &BuildStatus{
   269  		PendingFileChanges: make(map[string]time.Time),
   270  	}
   271  }
   272  
   273  func (s BuildStatus) IsEmpty() bool {
   274  	return len(s.PendingFileChanges) == 0 && s.LastSuccessfulResult == nil
   275  }
   276  
   277  type ManifestState struct {
   278  	Name model.ManifestName
   279  
   280  	BuildStatuses map[model.TargetID]*BuildStatus
   281  	RuntimeState  RuntimeState
   282  
   283  	PendingManifestChange time.Time
   284  
   285  	// The current build
   286  	CurrentBuild model.BuildRecord
   287  
   288  	LastSuccessfulDeployTime time.Time
   289  
   290  	// The last `BuildHistoryLimit` builds. The most recent build is first in the slice.
   291  	BuildHistory []model.BuildRecord
   292  
   293  	// The container IDs that we've run a LiveUpdate on, if any. Their contents have
   294  	// diverged from the image they are built on. If these container don't appear on
   295  	// the pod, we've lost that state and need to rebuild.
   296  	LiveUpdatedContainerIDs map[container.ID]bool
   297  
   298  	// We detected stale code and are currently doing an image build
   299  	NeedsRebuildFromCrash bool
   300  
   301  	// If a pod had to be killed because it was crashing, we keep the old log
   302  	// around for a little while so we can show it in the UX.
   303  	CrashLog model.Log
   304  
   305  	// The log stream for this resource
   306  	CombinedLog model.Log `testdiff:"ignore"`
   307  
   308  	// If this manifest was changed, which config files led to the most recent change in manifest definition
   309  	ConfigFilesThatCausedChange []string
   310  }
   311  
   312  func NewState() *EngineState {
   313  	ret := &EngineState{}
   314  	ret.Log = model.Log{}
   315  	ret.ManifestTargets = make(map[model.ManifestName]*ManifestTarget)
   316  	ret.PendingConfigFileChanges = make(map[string]time.Time)
   317  	ret.Secrets = model.SecretSet{}
   318  	ret.DockerPruneSettings = model.DefaultDockerPruneSettings()
   319  	return ret
   320  }
   321  
   322  func newManifestState(mn model.ManifestName) *ManifestState {
   323  	return &ManifestState{
   324  		Name:                    mn,
   325  		BuildStatuses:           make(map[model.TargetID]*BuildStatus),
   326  		LiveUpdatedContainerIDs: container.NewIDSet(),
   327  	}
   328  }
   329  
   330  func (ms *ManifestState) TargetID() model.TargetID {
   331  	return model.TargetID{
   332  		Type: model.TargetTypeManifest,
   333  		Name: ms.Name.TargetName(),
   334  	}
   335  }
   336  
   337  func (ms *ManifestState) BuildStatus(id model.TargetID) BuildStatus {
   338  	result, ok := ms.BuildStatuses[id]
   339  	if !ok {
   340  		return BuildStatus{}
   341  	}
   342  	return *result
   343  }
   344  
   345  func (ms *ManifestState) MutableBuildStatus(id model.TargetID) *BuildStatus {
   346  	result, ok := ms.BuildStatuses[id]
   347  	if !ok {
   348  		result = newBuildStatus()
   349  		ms.BuildStatuses[id] = result
   350  	}
   351  	return result
   352  }
   353  
   354  func (ms *ManifestState) DCRuntimeState() dockercompose.State {
   355  	ret, _ := ms.RuntimeState.(dockercompose.State)
   356  	return ret
   357  }
   358  
   359  func (ms *ManifestState) IsDC() bool {
   360  	_, ok := ms.RuntimeState.(dockercompose.State)
   361  	return ok
   362  }
   363  
   364  func (ms *ManifestState) K8sRuntimeState() K8sRuntimeState {
   365  	ret, _ := ms.RuntimeState.(K8sRuntimeState)
   366  	return ret
   367  }
   368  
   369  func (ms *ManifestState) GetOrCreateK8sRuntimeState() K8sRuntimeState {
   370  	ret, ok := ms.RuntimeState.(K8sRuntimeState)
   371  	if !ok {
   372  		ret = NewK8sRuntimeState()
   373  		ms.RuntimeState = ret
   374  	}
   375  	return ret
   376  }
   377  
   378  func (ms *ManifestState) IsK8s() bool {
   379  	_, ok := ms.RuntimeState.(K8sRuntimeState)
   380  	return ok
   381  }
   382  
   383  func (ms *ManifestState) ActiveBuild() model.BuildRecord {
   384  	return ms.CurrentBuild
   385  }
   386  
   387  func (ms *ManifestState) LastBuild() model.BuildRecord {
   388  	if len(ms.BuildHistory) == 0 {
   389  		return model.BuildRecord{}
   390  	}
   391  	return ms.BuildHistory[0]
   392  }
   393  
   394  func (ms *ManifestState) AddCompletedBuild(bs model.BuildRecord) {
   395  	ms.BuildHistory = append([]model.BuildRecord{bs}, ms.BuildHistory...)
   396  	if len(ms.BuildHistory) > model.BuildHistoryLimit {
   397  		ms.BuildHistory = ms.BuildHistory[:model.BuildHistoryLimit]
   398  	}
   399  }
   400  
   401  func (ms *ManifestState) StartedFirstBuild() bool {
   402  	return !ms.CurrentBuild.Empty() || len(ms.BuildHistory) > 0
   403  }
   404  
   405  func (ms *ManifestState) MostRecentPod() Pod {
   406  	return ms.K8sRuntimeState().MostRecentPod()
   407  }
   408  
   409  func (ms *ManifestState) HasPendingFileChanges() bool {
   410  	for _, status := range ms.BuildStatuses {
   411  		if len(status.PendingFileChanges) > 0 {
   412  			return true
   413  		}
   414  	}
   415  	return false
   416  }
   417  
   418  func (ms *ManifestState) NextBuildReason() model.BuildReason {
   419  	reason := model.BuildReasonNone
   420  	if ms.HasPendingFileChanges() {
   421  		reason = reason.With(model.BuildReasonFlagChangedFiles)
   422  	}
   423  	if !ms.PendingManifestChange.IsZero() {
   424  		reason = reason.With(model.BuildReasonFlagConfig)
   425  	}
   426  	if !ms.StartedFirstBuild() {
   427  		reason = reason.With(model.BuildReasonFlagInit)
   428  	}
   429  	if ms.NeedsRebuildFromCrash {
   430  		reason = reason.With(model.BuildReasonFlagCrash)
   431  	}
   432  	return reason
   433  }
   434  
   435  // Whether a change at the given time should trigger a build.
   436  // Used to determine if changes to synced files or config files
   437  // should kick off a new build.
   438  func (ms *ManifestState) IsPendingTime(t time.Time) bool {
   439  	return !t.IsZero() && t.After(ms.LastBuild().StartTime)
   440  }
   441  
   442  // Whether changes have been made to this Manifest's synced files
   443  // or config since the last build.
   444  //
   445  // Returns:
   446  // bool: whether changes have been made
   447  // Time: the time of the earliest change
   448  func (ms *ManifestState) HasPendingChanges() (bool, time.Time) {
   449  	return ms.HasPendingChangesBefore(time.Now())
   450  }
   451  
   452  // Like HasPendingChanges, but relative to a particular time.
   453  func (ms *ManifestState) HasPendingChangesBefore(highWaterMark time.Time) (bool, time.Time) {
   454  	ok := false
   455  	earliest := highWaterMark
   456  	t := ms.PendingManifestChange
   457  	if t.Before(earliest) && ms.IsPendingTime(t) {
   458  		ok = true
   459  		earliest = t
   460  	}
   461  
   462  	for _, status := range ms.BuildStatuses {
   463  		for _, t := range status.PendingFileChanges {
   464  			if t.Before(earliest) && ms.IsPendingTime(t) {
   465  				ok = true
   466  				earliest = t
   467  			}
   468  		}
   469  	}
   470  	if !ok {
   471  		return ok, time.Time{}
   472  	}
   473  	return ok, earliest
   474  }
   475  
   476  var _ model.TargetStatus = &ManifestState{}
   477  
   478  type YAMLManifestState struct {
   479  	HasBeenDeployed bool
   480  
   481  	CurrentApplyStartTime   time.Time
   482  	LastError               error
   483  	LastApplyFinishTime     time.Time
   484  	LastSuccessfulApplyTime time.Time
   485  	LastApplyStartTime      time.Time
   486  }
   487  
   488  func NewYAMLManifestState() *YAMLManifestState {
   489  	return &YAMLManifestState{}
   490  }
   491  
   492  func (s *YAMLManifestState) TargetID() model.TargetID {
   493  	return model.TargetID{
   494  		Type: model.TargetTypeManifest,
   495  		Name: model.UnresourcedYAMLManifestName.TargetName(),
   496  	}
   497  }
   498  
   499  func (s *YAMLManifestState) ActiveBuild() model.BuildRecord {
   500  	return model.BuildRecord{
   501  		StartTime: s.CurrentApplyStartTime,
   502  	}
   503  }
   504  
   505  func (s *YAMLManifestState) LastBuild() model.BuildRecord {
   506  	return model.BuildRecord{
   507  		StartTime:  s.LastApplyStartTime,
   508  		FinishTime: s.LastApplyFinishTime,
   509  		Error:      s.LastError,
   510  	}
   511  }
   512  
   513  var _ model.TargetStatus = &YAMLManifestState{}
   514  
   515  func ManifestTargetEndpoints(mt *ManifestTarget) (endpoints []string) {
   516  	defer func() {
   517  		sort.Strings(endpoints)
   518  	}()
   519  
   520  	// If the user specified port-forwards in the Tiltfile, we
   521  	// assume that's what they want to see in the UI
   522  	portForwards := mt.Manifest.K8sTarget().PortForwards
   523  	if len(portForwards) > 0 {
   524  		for _, pf := range portForwards {
   525  			endpoints = append(endpoints, fmt.Sprintf("http://localhost:%d/", pf.LocalPort))
   526  		}
   527  		return endpoints
   528  	}
   529  
   530  	publishedPorts := mt.Manifest.DockerComposeTarget().PublishedPorts()
   531  	if len(publishedPorts) > 0 {
   532  		for _, p := range publishedPorts {
   533  			endpoints = append(endpoints, fmt.Sprintf("http://localhost:%d/", p))
   534  		}
   535  		return endpoints
   536  	}
   537  
   538  	for _, u := range mt.State.K8sRuntimeState().LBs {
   539  		if u != nil {
   540  			endpoints = append(endpoints, u.String())
   541  		}
   542  	}
   543  	return endpoints
   544  }
   545  
   546  func StateToView(s EngineState) view.View {
   547  	ret := view.View{
   548  		IsProfiling:   s.IsProfiling,
   549  		LogTimestamps: s.LogTimestamps,
   550  	}
   551  
   552  	ret.Resources = append(ret.Resources, tiltfileResourceView(s))
   553  
   554  	for _, name := range s.ManifestDefinitionOrder {
   555  		mt, ok := s.ManifestTargets[name]
   556  		if !ok {
   557  			continue
   558  		}
   559  
   560  		ms := mt.State
   561  
   562  		var absWatchDirs []string
   563  		var absWatchPaths []string
   564  		for _, p := range mt.Manifest.LocalPaths() {
   565  			fi, err := os.Stat(p)
   566  			if err == nil && !fi.IsDir() {
   567  				absWatchPaths = append(absWatchPaths, p)
   568  			} else {
   569  				absWatchDirs = append(absWatchDirs, p)
   570  			}
   571  		}
   572  		absWatchPaths = append(absWatchPaths, s.TiltfilePath)
   573  		relWatchDirs := ospath.TryAsCwdChildren(absWatchDirs)
   574  		relWatchPaths := ospath.TryAsCwdChildren(absWatchPaths)
   575  
   576  		var pendingBuildEdits []string
   577  		for _, status := range ms.BuildStatuses {
   578  			for f := range status.PendingFileChanges {
   579  				pendingBuildEdits = append(pendingBuildEdits, f)
   580  			}
   581  		}
   582  
   583  		pendingBuildEdits = ospath.FileListDisplayNames(absWatchDirs, pendingBuildEdits)
   584  
   585  		buildHistory := append([]model.BuildRecord{}, ms.BuildHistory...)
   586  		for i, build := range buildHistory {
   587  			build.Edits = ospath.FileListDisplayNames(absWatchDirs, build.Edits)
   588  			buildHistory[i] = build
   589  		}
   590  
   591  		currentBuild := ms.CurrentBuild
   592  		currentBuild.Edits = ospath.FileListDisplayNames(absWatchDirs, ms.CurrentBuild.Edits)
   593  
   594  		// Sort the strings to make the outputs deterministic.
   595  		sort.Strings(pendingBuildEdits)
   596  
   597  		endpoints := ManifestTargetEndpoints(mt)
   598  
   599  		// NOTE(nick): Right now, the UX is designed to show the output exactly one
   600  		// pod. A better UI might summarize the pods in other ways (e.g., show the
   601  		// "most interesting" pod that's crash looping, or show logs from all pods
   602  		// at once).
   603  		_, pendingBuildSince := ms.HasPendingChanges()
   604  		r := view.Resource{
   605  			Name:               name,
   606  			DirectoriesWatched: relWatchDirs,
   607  			PathsWatched:       relWatchPaths,
   608  			LastDeployTime:     ms.LastSuccessfulDeployTime,
   609  			TriggerMode:        mt.Manifest.TriggerMode,
   610  			BuildHistory:       buildHistory,
   611  			PendingBuildEdits:  pendingBuildEdits,
   612  			PendingBuildSince:  pendingBuildSince,
   613  			PendingBuildReason: ms.NextBuildReason(),
   614  			CurrentBuild:       currentBuild,
   615  			CrashLog:           ms.CrashLog,
   616  			Endpoints:          endpoints,
   617  			ResourceInfo:       resourceInfoView(mt),
   618  		}
   619  
   620  		ret.Resources = append(ret.Resources, r)
   621  	}
   622  
   623  	ret.Log = s.Log
   624  	ret.FatalError = s.FatalError
   625  
   626  	return ret
   627  }
   628  
   629  const TiltfileManifestName = model.ManifestName("(Tiltfile)")
   630  
   631  func tiltfileResourceView(s EngineState) view.Resource {
   632  	tr := view.Resource{
   633  		Name:         TiltfileManifestName,
   634  		IsTiltfile:   true,
   635  		CurrentBuild: s.TiltfileState.CurrentBuild,
   636  		BuildHistory: s.TiltfileState.BuildHistory,
   637  	}
   638  	if !s.TiltfileState.CurrentBuild.Empty() {
   639  		tr.PendingBuildSince = s.TiltfileState.CurrentBuild.StartTime
   640  	} else {
   641  		tr.LastDeployTime = s.TiltfileState.LastBuild().FinishTime
   642  	}
   643  	if !s.TiltfileState.LastBuild().Empty() {
   644  		err := s.TiltfileState.LastBuild().Error
   645  		if err != nil {
   646  			tr.CrashLog = model.NewLog(err.Error())
   647  		}
   648  	}
   649  	return tr
   650  }
   651  
   652  func resourceInfoView(mt *ManifestTarget) view.ResourceInfoView {
   653  	if mt.Manifest.IsUnresourcedYAMLManifest() {
   654  		return view.YAMLResourceInfo{
   655  			K8sResources: mt.Manifest.K8sTarget().DisplayNames,
   656  		}
   657  	}
   658  
   659  	switch state := mt.State.RuntimeState.(type) {
   660  	case dockercompose.State:
   661  		return view.NewDCResourceInfo(mt.Manifest.DockerComposeTarget().ConfigPaths,
   662  			state.Status, state.ContainerID, state.Log(), state.StartTime)
   663  	case K8sRuntimeState:
   664  		pod := state.MostRecentPod()
   665  		return view.K8sResourceInfo{
   666  			PodName:            pod.PodID.String(),
   667  			PodCreationTime:    pod.StartedAt,
   668  			PodUpdateStartTime: pod.UpdateStartTime,
   669  			PodStatus:          pod.Status,
   670  			PodRestarts:        pod.VisibleContainerRestarts(),
   671  			PodLog:             pod.CurrentLog,
   672  		}
   673  	case LocalRuntimeState:
   674  		return view.LocalResourceInfo{}
   675  	default:
   676  		// This is silly but it was the old behavior.
   677  		return view.K8sResourceInfo{}
   678  	}
   679  }
   680  
   681  // DockerComposeConfigPath returns the path to the docker-compose yaml file of any
   682  // docker-compose manifests on this EngineState.
   683  // NOTE(maia): current assumption is only one d-c.yaml per run, so we take the
   684  // path from the first d-c manifest we see.
   685  func (s EngineState) DockerComposeConfigPath() []string {
   686  	for _, mt := range s.ManifestTargets {
   687  		if mt.Manifest.IsDC() {
   688  			return mt.Manifest.DockerComposeTarget().ConfigPaths
   689  		}
   690  	}
   691  	return []string{}
   692  }