github.com/tilt-dev/tilt@v0.36.0/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 health := state.Health 126 healthStatus := "" 127 if health != nil { 128 healthStatus = health.Status 129 } 130 131 return &v1alpha1.DockerContainerState{ 132 Status: state.Status, 133 Running: state.Running, 134 Error: state.Error, 135 ExitCode: int32(state.ExitCode), 136 StartedAt: metav1.NewMicroTime(startedAt), 137 FinishedAt: metav1.NewMicroTime(finishedAt), 138 HealthStatus: healthStatus, 139 } 140 } 141 142 // Returns the output of a healthcheck if the container is unhealthy. 143 func ToHealthcheckOutput(state *types.ContainerState) string { 144 health := state.Health 145 healthStatus := "" 146 if health == nil { 147 return "" 148 } 149 150 healthStatus = health.Status 151 if healthStatus != types.Unhealthy || len(health.Log) == 0 { 152 return "" 153 } 154 155 return health.Log[len(health.Log)-1].Output 156 } 157 158 // Convert a full into an apiserver-compatible status model. 159 func ToServiceStatus(id container.ID, name string, state *types.ContainerState, ports nat.PortMap) v1alpha1.DockerComposeServiceStatus { 160 status := v1alpha1.DockerComposeServiceStatus{} 161 status.ContainerID = string(id) 162 status.ContainerName = name 163 status.ContainerState = ToContainerState(state) 164 165 for containerPort, bindings := range ports { 166 for _, binding := range bindings { 167 p, err := strconv.Atoi(binding.HostPort) 168 if err != nil || p == 0 { 169 continue 170 } 171 status.PortBindings = append(status.PortBindings, v1alpha1.DockerPortBinding{ 172 ContainerPort: int32(containerPort.Int()), 173 HostIP: binding.HostIP, 174 HostPort: int32(p), 175 }) 176 } 177 } 178 179 // `ports` is a map, so make sure the ports come out in a deterministic order. 180 sort.Slice(status.PortBindings, func(i, j int) bool { 181 pi := status.PortBindings[i] 182 pj := status.PortBindings[j] 183 if pi.HostPort < pj.HostPort { 184 return true 185 } 186 if pi.HostPort > pj.HostPort { 187 return false 188 } 189 return pi.HostIP < pj.HostIP 190 }) 191 return status 192 }