Skip to content

Commit

Permalink
feat: expose JSON representation of a container with Inspect (#2534)
Browse files Browse the repository at this point in the history
* feat: expose JSON representation of a container with Inspect

* chore: deprecated c.Name and c.Ports

They can be used simply calling c.Inspect, without any other calculation

* chore: simplify state logic

* fix: update test

* fix: do not cache result in State
  • Loading branch information
mdelapenya committed May 7, 2024
1 parent b181e3e commit 5fa6548
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 97 deletions.
5 changes: 3 additions & 2 deletions container.go
Expand Up @@ -39,8 +39,9 @@ type Container interface {
Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the first exposed port
PortEndpoint(context.Context, nat.Port, string) (string, error) // get proto://ip:port string for the given exposed port
Host(context.Context) (string, error) // get host where the container port is exposed
Inspect(context.Context) (*types.ContainerJSON, error) // get container info
MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port
Ports(context.Context) (nat.PortMap, error) // get all exposed ports
Ports(context.Context) (nat.PortMap, error) // Deprecated: Use c.Inspect(ctx).NetworkSettings.Ports instead
SessionID() string // get session id
IsRunning() bool
Start(context.Context) error // start the container
Expand All @@ -50,7 +51,7 @@ type Container interface {
FollowOutput(LogConsumer) // Deprecated: it will be removed in the next major release
StartLogProducer(context.Context, ...LogProductionOption) error // Deprecated: Use the ContainerRequest instead
StopLogProducer() error // Deprecated: it will be removed in the next major release
Name(context.Context) (string, error) // get container name
Name(context.Context) (string, error) // Deprecated: Use c.Inspect(ctx).Name instead
State(context.Context) (*types.ContainerState, error) // returns container's running state
Networks(context.Context) ([]string, error) // get container networks
NetworkAliases(context.Context) (map[string][]string, error) // get container network aliases for a network
Expand Down
55 changes: 32 additions & 23 deletions docker.go
Expand Up @@ -114,11 +114,13 @@ func (c *DockerContainer) IsRunning() bool {
// Endpoint gets proto://host:port string for the first exposed port
// Will returns just host:port if proto is ""
func (c *DockerContainer) Endpoint(ctx context.Context, proto string) (string, error) {
ports, err := c.Ports(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return "", err
}

ports := inspect.NetworkSettings.Ports

// get first port
var firstPort nat.Port
for p := range ports {
Expand Down Expand Up @@ -161,19 +163,31 @@ func (c *DockerContainer) Host(ctx context.Context) (string, error) {
return host, nil
}

// Inspect gets the raw container info, caching the result for subsequent calls
func (c *DockerContainer) Inspect(ctx context.Context) (*types.ContainerJSON, error) {
if c.raw != nil {
return c.raw, nil
}

json, err := c.inspectRawContainer(ctx)
if err != nil {
return nil, err
}

return json, nil
}

// MappedPort gets externally mapped port for a container port
func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Port, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return "", err
}
if inspect.ContainerJSONBase.HostConfig.NetworkMode == "host" {
return port, nil
}
ports, err := c.Ports(ctx)
if err != nil {
return "", err
}

ports := inspect.NetworkSettings.Ports

for k, p := range ports {
if k.Port() != port.Port() {
Expand All @@ -191,9 +205,10 @@ func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Po
return "", errors.New("port not found")
}

// Deprecated: use c.Inspect(ctx).NetworkSettings.Ports instead.
// Ports gets the exposed ports for the container.
func (c *DockerContainer) Ports(ctx context.Context) (nat.PortMap, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -260,6 +275,7 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro
defer c.provider.Close()

c.isRunning = false
c.raw = nil // invalidate the cache, as the container representation will change after stopping

err = c.stoppedHook(ctx)
if err != nil {
Expand Down Expand Up @@ -298,6 +314,7 @@ func (c *DockerContainer) Terminate(ctx context.Context) error {

c.sessionID = ""
c.isRunning = false
c.raw = nil // invalidate the cache here too
return errors.Join(errs...)
}

Expand All @@ -313,16 +330,6 @@ func (c *DockerContainer) inspectRawContainer(ctx context.Context) (*types.Conta
return c.raw, nil
}

func (c *DockerContainer) inspectContainer(ctx context.Context) (*types.ContainerJSON, error) {
defer c.provider.Close()
inspect, err := c.provider.client.ContainerInspect(ctx, c.ID)
if err != nil {
return nil, err
}

return &inspect, nil
}

// Logs will fetch both STDOUT and STDERR from the current container. Returns a
// ReadCloser and leaves it up to the caller to extract what it wants.
func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) {
Expand Down Expand Up @@ -388,16 +395,18 @@ func (c *DockerContainer) followOutput(consumer LogConsumer) {
c.consumers = append(c.consumers, consumer)
}

// Deprecated: use c.Inspect(ctx).Name instead.
// Name gets the name of the container.
func (c *DockerContainer) Name(ctx context.Context) (string, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return "", err
}
return inspect.Name, nil
}

// State returns container's running state
// State returns container's running state. This method does not use the cache
// and always fetches the latest state from the Docker daemon.
func (c *DockerContainer) State(ctx context.Context) (*types.ContainerState, error) {
inspect, err := c.inspectRawContainer(ctx)
if err != nil {
Expand All @@ -411,7 +420,7 @@ func (c *DockerContainer) State(ctx context.Context) (*types.ContainerState, err

// Networks gets the names of the networks the container is attached to.
func (c *DockerContainer) Networks(ctx context.Context) ([]string, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return []string{}, err
}
Expand All @@ -429,7 +438,7 @@ func (c *DockerContainer) Networks(ctx context.Context) ([]string, error) {

// ContainerIP gets the IP address of the primary network within the container.
func (c *DockerContainer) ContainerIP(ctx context.Context) (string, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return "", err
}
Expand All @@ -452,7 +461,7 @@ func (c *DockerContainer) ContainerIP(ctx context.Context) (string, error) {
func (c *DockerContainer) ContainerIPs(ctx context.Context) ([]string, error) {
ips := make([]string, 0)

inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return nil, err
}
Expand All @@ -467,7 +476,7 @@ func (c *DockerContainer) ContainerIPs(ctx context.Context) ([]string, error) {

// NetworkAliases gets the aliases of the container for the networks it is attached to.
func (c *DockerContainer) NetworkAliases(ctx context.Context) (map[string][]string, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return map[string][]string{}, err
}
Expand Down
46 changes: 38 additions & 8 deletions docker_test.go
Expand Up @@ -267,8 +267,8 @@ func TestContainerTerminationResetsState(t *testing.T) {
if nginxA.SessionID() != "" {
t.Fatal("Internal state must be reset.")
}
ports, err := nginxA.Ports(ctx)
if err == nil || ports != nil {
inspect, err := nginxA.Inspect(ctx)
if err == nil || inspect != nil {
t.Fatal("expected error from container inspect.")
}
}
Expand Down Expand Up @@ -306,7 +306,7 @@ func TestContainerStateAfterTermination(t *testing.T) {
assert.Nil(t, state, "expected nil container inspect.")
})

t.Run("Non-nil State after termination if raw as already set", func(t *testing.T) {
t.Run("Nil State after termination if raw as already set", func(t *testing.T) {
ctx := context.Background()
nginx, err := createContainerFn(ctx)
if err != nil {
Expand All @@ -327,7 +327,7 @@ func TestContainerStateAfterTermination(t *testing.T) {
state, err = nginx.State(ctx)
require.Error(t, err, "expected error from container inspect after container termination.")

assert.NotNil(t, state, "unexpected nil container inspect after container termination.")
assert.Nil(t, state, "unexpected nil container inspect after container termination.")
})
}

Expand Down Expand Up @@ -539,10 +539,12 @@ func TestContainerCreationWithName(t *testing.T) {
require.NoError(t, err)
terminateContainerOnEnd(t, ctx, nginxC)

name, err := nginxC.Name(ctx)
inspect, err := nginxC.Inspect(ctx)
if err != nil {
t.Fatal(err)
}

name := inspect.Name
if name != expectedName {
t.Errorf("Expected container name '%s'. Got '%s'.", expectedName, name)
}
Expand Down Expand Up @@ -1320,6 +1322,29 @@ func TestContainerWithCustomHostname(t *testing.T) {
}
}

func TestContainerInspect_RawInspectIsCleanedOnStop(t *testing.T) {
container, err := GenericContainer(context.Background(), GenericContainerRequest{
ContainerRequest: ContainerRequest{
Image: nginxImage,
},
Started: true,
})
require.NoError(t, err)
terminateContainerOnEnd(t, context.Background(), container)

inspect, err := container.Inspect(context.Background())
require.NoError(t, err)

assert.NotEmpty(t, inspect.ID)

container.Stop(context.Background(), nil)

// type assertion to ensure that the container is a DockerContainer
dc := container.(*DockerContainer)

assert.Nil(t, dc.raw)
}

func readHostname(tb testing.TB, containerId string) string {
containerClient, err := NewDockerClientWithOpts(context.Background())
if err != nil {
Expand Down Expand Up @@ -1984,10 +2009,13 @@ func TestDockerProviderFindContainerByName(t *testing.T) {
Started: true,
})
require.NoError(t, err)
c1Name, err := c1.Name(ctx)

c1Inspect, err := c1.Inspect(ctx)
require.NoError(t, err)
terminateContainerOnEnd(t, ctx, c1)

c1Name := c1Inspect.Name

c2, err := GenericContainer(ctx, GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: ContainerRequest{
Expand Down Expand Up @@ -2036,8 +2064,10 @@ func TestImageBuiltFromDockerfile_KeepBuiltImage(t *testing.T) {
require.NoError(t, err, "create container should not fail")
defer func() { _ = c.Terminate(context.Background()) }()
// Get the image ID.
containerName, err := c.Name(ctx)
require.NoError(t, err, "get container name should not fail")
containerInspect, err := c.Inspect(ctx)
require.NoError(t, err, "container inspect should not fail")

containerName := containerInspect.Name
containerDetails, err := cli.ContainerInspect(ctx, containerName)
require.NoError(t, err, "inspect container should not fail")
containerImage := containerDetails.Image
Expand Down
4 changes: 3 additions & 1 deletion modules/localstack/localstack_test.go
Expand Up @@ -127,9 +127,11 @@ func TestRunContainer(t *testing.T) {
require.NoError(t, err)
assert.NotNil(t, container)

rawPorts, err := container.Ports(ctx)
inspect, err := container.Inspect(ctx)
require.NoError(t, err)

rawPorts := inspect.NetworkSettings.Ports

ports := 0
// only one port is exposed among all the ports in the container
for _, v := range rawPorts {
Expand Down
5 changes: 5 additions & 0 deletions wait/exec_test.go
Expand Up @@ -62,6 +62,11 @@ func (st mockExecTarget) Host(_ context.Context) (string, error) {
return "", errors.New("not implemented")
}

func (st mockExecTarget) Inspect(ctx context.Context) (*types.ContainerJSON, error) {
return nil, errors.New("not implemented")
}

// Deprecated: use Inspect instead
func (st mockExecTarget) Ports(ctx context.Context) (nat.PortMap, error) {
return nil, errors.New("not implemented")
}
Expand Down
5 changes: 5 additions & 0 deletions wait/exit_test.go
Expand Up @@ -20,6 +20,11 @@ func (st exitStrategyTarget) Host(ctx context.Context) (string, error) {
return "", nil
}

func (st exitStrategyTarget) Inspect(ctx context.Context) (*types.ContainerJSON, error) {
return nil, nil
}

// Deprecated: use Inspect instead
func (st exitStrategyTarget) Ports(ctx context.Context) (nat.PortMap, error) {
return nil, nil
}
Expand Down
5 changes: 5 additions & 0 deletions wait/health_test.go
Expand Up @@ -21,6 +21,11 @@ func (st healthStrategyTarget) Host(ctx context.Context) (string, error) {
return "", nil
}

func (st healthStrategyTarget) Inspect(ctx context.Context) (*types.ContainerJSON, error) {
return nil, nil
}

// Deprecated: use Inspect instead
func (st healthStrategyTarget) Ports(ctx context.Context) (nat.PortMap, error) {
return nil, nil
}
Expand Down
5 changes: 4 additions & 1 deletion wait/host_port.go
Expand Up @@ -90,10 +90,13 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
internalPort := hp.Port
if internalPort == "" {
var ports nat.PortMap
ports, err = target.Ports(ctx)
inspect, err := target.Inspect(ctx)
if err != nil {
return err
}

ports = inspect.NetworkSettings.Ports

if len(ports) > 0 {
for p := range ports {
internalPort = p
Expand Down
36 changes: 24 additions & 12 deletions wait/host_port_test.go
Expand Up @@ -80,12 +80,18 @@ func TestWaitForExposedPortSucceeds(t *testing.T) {
HostImpl: func(_ context.Context) (string, error) {
return "localhost", nil
},
PortsImpl: func(_ context.Context) (nat.PortMap, error) {
return nat.PortMap{
"80": []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: port.Port(),
InspectImpl: func(_ context.Context) (*types.ContainerJSON, error) {
return &types.ContainerJSON{
NetworkSettings: &types.NetworkSettings{
NetworkSettingsBase: types.NetworkSettingsBase{
Ports: nat.PortMap{
"80": []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: port.Port(),
},
},
},
},
},
}, nil
Expand Down Expand Up @@ -500,12 +506,18 @@ func TestHostPortStrategySucceedsGivenShellIsNotInstalled(t *testing.T) {
HostImpl: func(_ context.Context) (string, error) {
return "localhost", nil
},
PortsImpl: func(_ context.Context) (nat.PortMap, error) {
return nat.PortMap{
"80": []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: port.Port(),
InspectImpl: func(_ context.Context) (*types.ContainerJSON, error) {
return &types.ContainerJSON{
NetworkSettings: &types.NetworkSettings{
NetworkSettingsBase: types.NetworkSettingsBase{
Ports: nat.PortMap{
"80": []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: port.Port(),
},
},
},
},
},
}, nil
Expand Down

0 comments on commit 5fa6548

Please sign in to comment.