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

     1  package dockercompose
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strconv"
     7  	"time"
     8  
     9  	"github.com/docker/docker/api/types"
    10  	"github.com/docker/go-connections/nat"
    11  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  
    13  	"github.com/tilt-dev/tilt/internal/container"
    14  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    15  	"github.com/tilt-dev/tilt/pkg/model"
    16  )
    17  
    18  // Status strings taken from comments on:
    19  // https://godoc.org/github.com/docker/docker/api/types#ContainerState
    20  const ContainerStatusCreated = "created"
    21  const ContainerStatusRunning = "running"
    22  const ContainerStatusPaused = "paused"
    23  const ContainerStatusRestarting = "restarting"
    24  const ContainerStatusRemoving = "removing"
    25  const ContainerStatusExited = "exited"
    26  const ContainerStatusDead = "dead"
    27  
    28  // Helper functions for dealing with ContainerState.
    29  const ZeroTime = "0001-01-01T00:00:00Z"
    30  
    31  type State struct {
    32  	ContainerState v1alpha1.DockerContainerState
    33  	ContainerID    container.ID
    34  	Ports          []v1alpha1.DockerPortBinding
    35  	LastReadyTime  time.Time
    36  
    37  	SpanID model.LogSpanID
    38  }
    39  
    40  func (State) RuntimeState() {}
    41  
    42  func (s State) RuntimeStatus() v1alpha1.RuntimeStatus {
    43  	if s.ContainerState.Error != "" || s.ContainerState.ExitCode != 0 {
    44  		return v1alpha1.RuntimeStatusError
    45  	}
    46  	if s.ContainerState.Running ||
    47  		s.ContainerState.Status == ContainerStatusRunning ||
    48  		s.ContainerState.Status == ContainerStatusExited {
    49  		return v1alpha1.RuntimeStatusOK
    50  	}
    51  	if s.ContainerState.Status == "" {
    52  		return v1alpha1.RuntimeStatusUnknown
    53  	}
    54  	return v1alpha1.RuntimeStatusPending
    55  }
    56  
    57  func (s State) RuntimeStatusError() error {
    58  	status := s.RuntimeStatus()
    59  	if status != v1alpha1.RuntimeStatusError {
    60  		return nil
    61  	}
    62  	if s.ContainerState.Error != "" {
    63  		return fmt.Errorf("Container %s: %s", s.ContainerID, s.ContainerState.Error)
    64  	}
    65  	if s.ContainerState.ExitCode != 0 {
    66  		return fmt.Errorf("Container %s exited with %d", s.ContainerID, s.ContainerState.ExitCode)
    67  	}
    68  	return fmt.Errorf("Container %s error status: %s", s.ContainerID, s.ContainerState.Status)
    69  }
    70  
    71  func (s State) WithContainerState(state v1alpha1.DockerContainerState) State {
    72  	s.ContainerState = state
    73  
    74  	if s.RuntimeStatus() == v1alpha1.RuntimeStatusOK {
    75  		s.LastReadyTime = time.Now()
    76  	}
    77  
    78  	return s
    79  }
    80  
    81  func (s State) WithPorts(ports []v1alpha1.DockerPortBinding) State {
    82  	s.Ports = ports
    83  	return s
    84  }
    85  
    86  func (s State) WithSpanID(spanID model.LogSpanID) State {
    87  	s.SpanID = spanID
    88  	return s
    89  }
    90  
    91  func (s State) WithContainerID(cID container.ID) State {
    92  	if cID == s.ContainerID {
    93  		return s
    94  	}
    95  	s.ContainerID = cID
    96  	s.ContainerState = v1alpha1.DockerContainerState{}
    97  	return s
    98  }
    99  
   100  func (s State) HasEverBeenReadyOrSucceeded() bool {
   101  	return !s.LastReadyTime.IsZero()
   102  }
   103  
   104  // Convert ContainerState into an apiserver-compatible state model.
   105  func ToContainerState(state *types.ContainerState) *v1alpha1.DockerContainerState {
   106  	if state == nil {
   107  		return nil
   108  	}
   109  	var startedAt, finishedAt time.Time
   110  	var err error
   111  	if state.StartedAt != "" && state.StartedAt != ZeroTime {
   112  		startedAt, err = time.Parse(time.RFC3339Nano, state.StartedAt)
   113  		if err != nil {
   114  			startedAt = time.Time{}
   115  		}
   116  	}
   117  
   118  	if state.FinishedAt != "" && state.FinishedAt != ZeroTime {
   119  		finishedAt, err = time.Parse(time.RFC3339Nano, state.FinishedAt)
   120  		if err != nil {
   121  			finishedAt = time.Time{}
   122  		}
   123  	}
   124  
   125  	return &v1alpha1.DockerContainerState{
   126  		Status:     state.Status,
   127  		Running:    state.Running,
   128  		Error:      state.Error,
   129  		ExitCode:   int32(state.ExitCode),
   130  		StartedAt:  metav1.NewMicroTime(startedAt),
   131  		FinishedAt: metav1.NewMicroTime(finishedAt),
   132  	}
   133  }
   134  
   135  // Convert a full into an apiserver-compatible status model.
   136  func ToServiceStatus(id container.ID, name string, state *types.ContainerState, ports nat.PortMap) v1alpha1.DockerComposeServiceStatus {
   137  	status := v1alpha1.DockerComposeServiceStatus{}
   138  	status.ContainerID = string(id)
   139  	status.ContainerName = name
   140  	status.ContainerState = ToContainerState(state)
   141  
   142  	for containerPort, bindings := range ports {
   143  		for _, binding := range bindings {
   144  			p, err := strconv.Atoi(binding.HostPort)
   145  			if err != nil || p == 0 {
   146  				continue
   147  			}
   148  			status.PortBindings = append(status.PortBindings, v1alpha1.DockerPortBinding{
   149  				ContainerPort: int32(containerPort.Int()),
   150  				HostIP:        binding.HostIP,
   151  				HostPort:      int32(p),
   152  			})
   153  		}
   154  	}
   155  
   156  	// `ports` is a map, so make sure the ports come out in a deterministic order.
   157  	sort.Slice(status.PortBindings, func(i, j int) bool {
   158  		pi := status.PortBindings[i]
   159  		pj := status.PortBindings[j]
   160  		if pi.HostPort < pj.HostPort {
   161  			return true
   162  		}
   163  		if pi.HostPort > pj.HostPort {
   164  			return false
   165  		}
   166  		return pi.HostIP < pj.HostIP
   167  	})
   168  	return status
   169  }