@@ -23,6 +23,7 @@ import (
23
23
"github.com/docker/docker/client"
24
24
"github.com/docker/docker/pkg/stdcopy"
25
25
"github.com/docker/go-connections/nat"
26
+ v1 "github.com/opencontainers/image-spec/specs-go/v1"
26
27
27
28
"github.com/stacklok/toolhive/pkg/container/docker/sdk"
28
29
"github.com/stacklok/toolhive/pkg/container/images"
@@ -61,6 +62,61 @@ type dockerAPI interface {
61
62
ContainerList (ctx context.Context , options container.ListOptions ) ([]container.Summary , error )
62
63
ContainerInspect (ctx context.Context , containerID string ) (container.InspectResponse , error )
63
64
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 )
64
120
}
65
121
66
122
// Client implements the Deployer interface for Docker (and compatible runtimes)
@@ -70,6 +126,7 @@ type Client struct {
70
126
client * client.Client
71
127
api dockerAPI
72
128
imageManager images.ImageManager
129
+ ops deployOps
73
130
}
74
131
75
132
// NewClient creates a new container client
@@ -88,10 +145,25 @@ func NewClient(ctx context.Context) (*Client, error) {
88
145
api : dockerClient ,
89
146
imageManager : imageManager ,
90
147
}
148
+ // Default ops implementation uses the real client methods.
149
+ c .ops = c
91
150
92
151
return c , nil
93
152
}
94
153
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
+
95
167
// DeployWorkload creates and starts a workload.
96
168
// It configures the workload based on the provided permission profile and transport type.
97
169
// If options is nil, default options will be used.
@@ -130,7 +202,7 @@ func (c *Client) DeployWorkload(
130
202
"toolhive-external" : {},
131
203
}
132
204
133
- err = c .createExternalNetworks (ctx )
205
+ err = c .ops . createExternalNetworks (ctx )
134
206
if err != nil {
135
207
return 0 , fmt .Errorf ("failed to create external networks: %v" , err )
136
208
}
@@ -141,14 +213,14 @@ func (c *Client) DeployWorkload(
141
213
142
214
internalNetworkLabels := map [string ]string {}
143
215
lb .AddNetworkLabels (internalNetworkLabels , networkName )
144
- err := c .createNetwork (ctx , networkName , internalNetworkLabels , true )
216
+ err := c .ops . createNetwork (ctx , networkName , internalNetworkLabels , true )
145
217
if err != nil {
146
218
return 0 , fmt .Errorf ("failed to create internal network: %v" , err )
147
219
}
148
220
149
221
// create dns container
150
222
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 )
152
224
if dnsContainerIP != "" {
153
225
additionalDNS = dnsContainerIP
154
226
}
@@ -158,9 +230,8 @@ func (c *Client) DeployWorkload(
158
230
159
231
// create egress container
160
232
egressContainerName := fmt .Sprintf ("%s-egress" , name )
161
- _ , err = createEgressSquidContainer (
233
+ _ , err = c . ops . createEgressSquidContainer (
162
234
ctx ,
163
- c ,
164
235
name ,
165
236
egressContainerName ,
166
237
attachStdio ,
@@ -188,7 +259,7 @@ func (c *Client) DeployWorkload(
188
259
// about ingress/egress/dns containers.
189
260
lb .AddNetworkIsolationLabel (labels , networkIsolation )
190
261
191
- err = c .createMcpContainer (
262
+ err = c .ops . createMcpContainer (
192
263
ctx ,
193
264
name ,
194
265
networkName ,
@@ -218,7 +289,7 @@ func (c *Client) DeployWorkload(
218
289
if err != nil {
219
290
return 0 , err // extractFirstPort already wraps the error with context.
220
291
}
221
- hostPort , err = c .createIngressContainer (ctx , name , firstPortInt , attachStdio , externalEndpointsConfig )
292
+ hostPort , err = c .ops . createIngressContainer (ctx , name , firstPortInt , attachStdio , externalEndpointsConfig )
222
293
if err != nil {
223
294
return 0 , fmt .Errorf ("failed to create ingress container: %v" , err )
224
295
}
@@ -952,7 +1023,7 @@ func (c *Client) handleExistingContainer(
952
1023
desiredHostConfig * container.HostConfig ,
953
1024
) (bool , error ) {
954
1025
// Get container info
955
- info , err := c .client .ContainerInspect (ctx , containerID )
1026
+ info , err := c .api .ContainerInspect (ctx , containerID )
956
1027
if err != nil {
957
1028
return false , NewContainerError (err , containerID , fmt .Sprintf ("failed to inspect container: %v" , err ))
958
1029
}
@@ -964,7 +1035,7 @@ func (c *Client) handleExistingContainer(
964
1035
// Check if the container is running
965
1036
if ! info .State .Running {
966
1037
// 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 {})
968
1039
if err != nil {
969
1040
return false , NewContainerError (err , containerID , fmt .Sprintf ("failed to start existing container: %v" , err ))
970
1041
}
@@ -1040,7 +1111,7 @@ func (c *Client) deleteNetwork(ctx context.Context, name string) error {
1040
1111
1041
1112
// removeContainer removes a container by ID, without removing any associated networks or proxy containers.
1042
1113
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 {
1044
1115
Force : true ,
1045
1116
})
1046
1117
if err != nil {
@@ -1202,7 +1273,7 @@ func (c *Client) createContainer(
1202
1273
}
1203
1274
1204
1275
// Create the container
1205
- resp , err := c .client .ContainerCreate (
1276
+ resp , err := c .api .ContainerCreate (
1206
1277
ctx ,
1207
1278
config ,
1208
1279
hostConfig ,
@@ -1215,7 +1286,7 @@ func (c *Client) createContainer(
1215
1286
}
1216
1287
1217
1288
// Start the container
1218
- err = c .client .ContainerStart (ctx , resp .ID , container.StartOptions {})
1289
+ err = c .api .ContainerStart (ctx , resp .ID , container.StartOptions {})
1219
1290
if err != nil {
1220
1291
return "" , NewContainerError (err , resp .ID , fmt .Sprintf ("failed to start container: %v" , err ))
1221
1292
}
0 commit comments