Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose JSON representation of a container with Inspect #2534

Merged
merged 5 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions container.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading
Loading