Skip to content

Commit 5dffcba

Browse files
authored
Add Docker workload creation unit tests; extend dockerAPI seam (#1883)
Extend dockerAPI seam for create/start/remove Route create/start/remove via dockerAPI to enable hermetic unit tests for createMcpContainer and createContainer. Update handleExistingContainer, removeContainer, and createContainer to use c.api. Adjust fakeDockerAPI to implement the extended interface. Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent cc4d03d commit 5dffcba

File tree

4 files changed

+983
-12
lines changed

4 files changed

+983
-12
lines changed

pkg/container/docker/client.go

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/docker/docker/client"
2424
"github.com/docker/docker/pkg/stdcopy"
2525
"github.com/docker/go-connections/nat"
26+
v1 "github.com/opencontainers/image-spec/specs-go/v1"
2627

2728
"github.com/stacklok/toolhive/pkg/container/docker/sdk"
2829
"github.com/stacklok/toolhive/pkg/container/images"
@@ -61,6 +62,61 @@ type dockerAPI interface {
6162
ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error)
6263
ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error)
6364
ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error
65+
ContainerCreate(
66+
ctx context.Context,
67+
config *container.Config,
68+
hostConfig *container.HostConfig,
69+
networkingConfig *network.NetworkingConfig,
70+
platform *v1.Platform,
71+
containerName string,
72+
) (container.CreateResponse, error)
73+
ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error
74+
ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error
75+
}
76+
77+
// deployOps defines the internal operations used by DeployWorkload.
78+
// It allows unit tests to substitute a fake implementation to avoid hitting a real Docker daemon.
79+
type deployOps interface {
80+
createExternalNetworks(ctx context.Context) error
81+
createNetwork(ctx context.Context, name string, labels map[string]string, internal bool) error
82+
createDnsContainer(
83+
ctx context.Context,
84+
dnsContainerName string,
85+
attachStdio bool,
86+
networkName string,
87+
endpointsConfig map[string]*network.EndpointSettings,
88+
) (string, string, error)
89+
createEgressSquidContainer(
90+
ctx context.Context,
91+
containerName string,
92+
squidContainerName string,
93+
attachStdio bool,
94+
exposedPorts map[string]struct{},
95+
endpointsConfig map[string]*network.EndpointSettings,
96+
perm *permissions.NetworkPermissions,
97+
) (string, error)
98+
createMcpContainer(
99+
ctx context.Context,
100+
name string,
101+
networkName string,
102+
image string,
103+
command []string,
104+
envVars map[string]string,
105+
labels map[string]string,
106+
attachStdio bool,
107+
permissionConfig *runtime.PermissionConfig,
108+
additionalDNS string,
109+
exposedPorts map[string]struct{},
110+
portBindings map[string][]runtime.PortBinding,
111+
isolateNetwork bool,
112+
) error
113+
createIngressContainer(
114+
ctx context.Context,
115+
containerName string,
116+
upstreamPort int,
117+
attachStdio bool,
118+
externalEndpointsConfig map[string]*network.EndpointSettings,
119+
) (int, error)
64120
}
65121

66122
// Client implements the Deployer interface for Docker (and compatible runtimes)
@@ -70,6 +126,7 @@ type Client struct {
70126
client *client.Client
71127
api dockerAPI
72128
imageManager images.ImageManager
129+
ops deployOps
73130
}
74131

75132
// NewClient creates a new container client
@@ -88,10 +145,25 @@ func NewClient(ctx context.Context) (*Client, error) {
88145
api: dockerClient,
89146
imageManager: imageManager,
90147
}
148+
// Default ops implementation uses the real client methods.
149+
c.ops = c
91150

92151
return c, nil
93152
}
94153

154+
// createEgressSquidContainer wraps the package-level createEgressSquidContainer to satisfy deployOps.
155+
func (c *Client) createEgressSquidContainer(
156+
ctx context.Context,
157+
containerName string,
158+
squidContainerName string,
159+
attachStdio bool,
160+
exposedPorts map[string]struct{},
161+
endpointsConfig map[string]*network.EndpointSettings,
162+
perm *permissions.NetworkPermissions,
163+
) (string, error) {
164+
return createEgressSquidContainer(ctx, c, containerName, squidContainerName, attachStdio, exposedPorts, endpointsConfig, perm)
165+
}
166+
95167
// DeployWorkload creates and starts a workload.
96168
// It configures the workload based on the provided permission profile and transport type.
97169
// If options is nil, default options will be used.
@@ -130,7 +202,7 @@ func (c *Client) DeployWorkload(
130202
"toolhive-external": {},
131203
}
132204

133-
err = c.createExternalNetworks(ctx)
205+
err = c.ops.createExternalNetworks(ctx)
134206
if err != nil {
135207
return 0, fmt.Errorf("failed to create external networks: %v", err)
136208
}
@@ -141,14 +213,14 @@ func (c *Client) DeployWorkload(
141213

142214
internalNetworkLabels := map[string]string{}
143215
lb.AddNetworkLabels(internalNetworkLabels, networkName)
144-
err := c.createNetwork(ctx, networkName, internalNetworkLabels, true)
216+
err := c.ops.createNetwork(ctx, networkName, internalNetworkLabels, true)
145217
if err != nil {
146218
return 0, fmt.Errorf("failed to create internal network: %v", err)
147219
}
148220

149221
// create dns container
150222
dnsContainerName := fmt.Sprintf("%s-dns", name)
151-
_, dnsContainerIP, err := c.createDnsContainer(ctx, dnsContainerName, attachStdio, networkName, externalEndpointsConfig)
223+
_, dnsContainerIP, err := c.ops.createDnsContainer(ctx, dnsContainerName, attachStdio, networkName, externalEndpointsConfig)
152224
if dnsContainerIP != "" {
153225
additionalDNS = dnsContainerIP
154226
}
@@ -158,9 +230,8 @@ func (c *Client) DeployWorkload(
158230

159231
// create egress container
160232
egressContainerName := fmt.Sprintf("%s-egress", name)
161-
_, err = createEgressSquidContainer(
233+
_, err = c.ops.createEgressSquidContainer(
162234
ctx,
163-
c,
164235
name,
165236
egressContainerName,
166237
attachStdio,
@@ -188,7 +259,7 @@ func (c *Client) DeployWorkload(
188259
// about ingress/egress/dns containers.
189260
lb.AddNetworkIsolationLabel(labels, networkIsolation)
190261

191-
err = c.createMcpContainer(
262+
err = c.ops.createMcpContainer(
192263
ctx,
193264
name,
194265
networkName,
@@ -218,7 +289,7 @@ func (c *Client) DeployWorkload(
218289
if err != nil {
219290
return 0, err // extractFirstPort already wraps the error with context.
220291
}
221-
hostPort, err = c.createIngressContainer(ctx, name, firstPortInt, attachStdio, externalEndpointsConfig)
292+
hostPort, err = c.ops.createIngressContainer(ctx, name, firstPortInt, attachStdio, externalEndpointsConfig)
222293
if err != nil {
223294
return 0, fmt.Errorf("failed to create ingress container: %v", err)
224295
}
@@ -952,7 +1023,7 @@ func (c *Client) handleExistingContainer(
9521023
desiredHostConfig *container.HostConfig,
9531024
) (bool, error) {
9541025
// Get container info
955-
info, err := c.client.ContainerInspect(ctx, containerID)
1026+
info, err := c.api.ContainerInspect(ctx, containerID)
9561027
if err != nil {
9571028
return false, NewContainerError(err, containerID, fmt.Sprintf("failed to inspect container: %v", err))
9581029
}
@@ -964,7 +1035,7 @@ func (c *Client) handleExistingContainer(
9641035
// Check if the container is running
9651036
if !info.State.Running {
9661037
// Container exists but is not running, start it
967-
err = c.client.ContainerStart(ctx, containerID, container.StartOptions{})
1038+
err = c.api.ContainerStart(ctx, containerID, container.StartOptions{})
9681039
if err != nil {
9691040
return false, NewContainerError(err, containerID, fmt.Sprintf("failed to start existing container: %v", err))
9701041
}
@@ -1040,7 +1111,7 @@ func (c *Client) deleteNetwork(ctx context.Context, name string) error {
10401111

10411112
// removeContainer removes a container by ID, without removing any associated networks or proxy containers.
10421113
func (c *Client) removeContainer(ctx context.Context, containerID string) error {
1043-
err := c.client.ContainerRemove(ctx, containerID, container.RemoveOptions{
1114+
err := c.api.ContainerRemove(ctx, containerID, container.RemoveOptions{
10441115
Force: true,
10451116
})
10461117
if err != nil {
@@ -1202,7 +1273,7 @@ func (c *Client) createContainer(
12021273
}
12031274

12041275
// Create the container
1205-
resp, err := c.client.ContainerCreate(
1276+
resp, err := c.api.ContainerCreate(
12061277
ctx,
12071278
config,
12081279
hostConfig,
@@ -1215,7 +1286,7 @@ func (c *Client) createContainer(
12151286
}
12161287

12171288
// Start the container
1218-
err = c.client.ContainerStart(ctx, resp.ID, container.StartOptions{})
1289+
err = c.api.ContainerStart(ctx, resp.ID, container.StartOptions{})
12191290
if err != nil {
12201291
return "", NewContainerError(err, resp.ID, fmt.Sprintf("failed to start container: %v", err))
12211292
}

0 commit comments

Comments
 (0)