github.com/mponton/terratest@v0.44.0/modules/docker/inspect.go (about) 1 package docker 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strconv" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/mponton/terratest/modules/logger" 12 "github.com/mponton/terratest/modules/shell" 13 "github.com/stretchr/testify/require" 14 ) 15 16 // ContainerInspect defines the output of the Inspect method, with the options returned by 'docker inspect' 17 // converted into a more friendly and testable interface 18 type ContainerInspect struct { 19 // ID of the inspected container 20 ID string 21 22 // Name of the inspected container 23 Name string 24 25 // time.Time that the container was created 26 Created time.Time 27 28 // String representing the container's status 29 Status string 30 31 // Whether the container is currently running or not 32 Running bool 33 34 // Container's exit code 35 ExitCode uint8 36 37 // String with the container's error message, if there is any 38 Error string 39 40 // Ports exposed by the container 41 Ports []Port 42 43 // Volume bindings made to the container 44 Binds []VolumeBind 45 46 // Health check 47 Health HealthCheck 48 } 49 50 // Port represents a single port mapping exported by the container 51 type Port struct { 52 HostPort uint16 53 ContainerPort uint16 54 Protocol string 55 } 56 57 // VolumeBind represents a single volume binding made to the container 58 type VolumeBind struct { 59 Source string 60 Destination string 61 } 62 63 // HealthCheck represents the current health history of the container 64 type HealthCheck struct { 65 // Health check status 66 Status string 67 68 // Current count of failing health checks 69 FailingStreak uint8 70 71 // Log of failures 72 Log []HealthLog 73 } 74 75 // HealthLog represents the output of a single Health check of the container 76 type HealthLog struct { 77 // Start time of health check 78 Start string 79 80 // End time of health check 81 End string 82 83 // Exit code of health check 84 ExitCode uint8 85 86 // Output of health check 87 Output string 88 } 89 90 // inspectOutput defines options that will be returned by 'docker inspect', in JSON format. 91 // Not all options are included here, only the ones that we might need 92 type inspectOutput struct { 93 Id string 94 Created string 95 Name string 96 State struct { 97 Health HealthCheck 98 Status string 99 Running bool 100 ExitCode uint8 101 Error string 102 } 103 NetworkSettings struct { 104 Ports map[string][]struct { 105 HostIp string 106 HostPort string 107 } 108 } 109 HostConfig struct { 110 Binds []string 111 } 112 } 113 114 // Inspect runs the 'docker inspect {container id}' command and returns a ContainerInspect 115 // struct, converted from the output JSON, along with any errors 116 func Inspect(t *testing.T, id string) *ContainerInspect { 117 out, err := InspectE(t, id) 118 require.NoError(t, err) 119 120 return out 121 } 122 123 // InspectE runs the 'docker inspect {container id}' command and returns a ContainerInspect 124 // struct, converted from the output JSON, along with any errors 125 func InspectE(t *testing.T, id string) (*ContainerInspect, error) { 126 cmd := shell.Command{ 127 Command: "docker", 128 Args: []string{"container", "inspect", id}, 129 // inspect is a short-running command, don't print the output. 130 Logger: logger.Discard, 131 } 132 133 out, err := shell.RunCommandAndGetStdOutE(t, cmd) 134 if err != nil { 135 return nil, err 136 } 137 138 var containers []inspectOutput 139 err = json.Unmarshal([]byte(out), &containers) 140 if err != nil { 141 return nil, err 142 } 143 144 if len(containers) == 0 { 145 return nil, fmt.Errorf("no container found with ID %s", id) 146 } 147 148 container := containers[0] 149 150 return transformContainer(t, container) 151 } 152 153 // transformContainerPorts converts 'docker inspect' output JSON into a more friendly and testable format 154 func transformContainer(t *testing.T, container inspectOutput) (*ContainerInspect, error) { 155 name := strings.TrimLeft(container.Name, "/") 156 157 ports, err := transformContainerPorts(container) 158 if err != nil { 159 return nil, err 160 } 161 162 volumes := transformContainerVolumes(container) 163 164 created, err := time.Parse(time.RFC3339Nano, container.Created) 165 if err != nil { 166 return nil, err 167 } 168 169 inspect := ContainerInspect{ 170 ID: container.Id, 171 Name: name, 172 Created: created, 173 Status: container.State.Status, 174 Running: container.State.Running, 175 ExitCode: container.State.ExitCode, 176 Error: container.State.Error, 177 Ports: ports, 178 Binds: volumes, 179 Health: HealthCheck{ 180 Status: container.State.Health.Status, 181 FailingStreak: container.State.Health.FailingStreak, 182 Log: container.State.Health.Log, 183 }, 184 } 185 186 return &inspect, nil 187 } 188 189 // transformContainerPorts converts Docker's ports from the following json into a more testable format 190 // 191 // { 192 // "80/tcp": [ 193 // { 194 // "HostIp": "" 195 // "HostPort": "8080" 196 // } 197 // ] 198 // } 199 func transformContainerPorts(container inspectOutput) ([]Port, error) { 200 var ports []Port 201 202 cPorts := container.NetworkSettings.Ports 203 204 for key, portBinding := range cPorts { 205 split := strings.Split(key, "/") 206 207 containerPort, err := strconv.ParseUint(split[0], 10, 16) 208 if err != nil { 209 return nil, err 210 } 211 212 var protocol string 213 if len(split) > 1 { 214 protocol = split[1] 215 } 216 217 for _, port := range portBinding { 218 hostPort, err := strconv.ParseUint(port.HostPort, 10, 16) 219 if err != nil { 220 return nil, err 221 } 222 223 ports = append(ports, Port{ 224 HostPort: uint16(hostPort), 225 ContainerPort: uint16(containerPort), 226 Protocol: protocol, 227 }) 228 } 229 } 230 231 return ports, nil 232 } 233 234 // GetExposedHostPort returns an exposed host port according to requested container port. Returns 0 if the requested port is not exposed. 235 func (inspectOutput ContainerInspect) GetExposedHostPort(containerPort uint16) uint16 { 236 for _, port := range inspectOutput.Ports { 237 if port.ContainerPort == containerPort { 238 return port.HostPort 239 } 240 } 241 return uint16(0) 242 } 243 244 // transformContainerVolumes converts Docker's volume bindings from the 245 // format "/foo/bar:/foo/baz" into a more testable one 246 func transformContainerVolumes(container inspectOutput) []VolumeBind { 247 binds := container.HostConfig.Binds 248 volumes := make([]VolumeBind, 0, len(binds)) 249 250 for _, bind := range binds { 251 var source, dest string 252 253 split := strings.Split(bind, ":") 254 255 // Considering it as an unbound volume 256 dest = split[0] 257 258 if len(split) == 2 { 259 source = split[0] 260 dest = split[1] 261 } 262 263 volumes = append(volumes, VolumeBind{ 264 Source: source, 265 Destination: dest, 266 }) 267 } 268 269 return volumes 270 }