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 }