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  }