Skip to content

Commit 7768aa8

Browse files
committed
spin egress container for each mcp server
Closes: #124 spin up and connect networks move logic to internal docker fixes from rebase fix squid.conf fix lint
1 parent f6cfcab commit 7768aa8

File tree

16 files changed

+411
-48
lines changed

16 files changed

+411
-48
lines changed

cmd/thv/app/rm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func rmCmdFunc(cmd *cobra.Command, args []string) error {
3737
}
3838

3939
// Delete container.
40-
if err := manager.DeleteContainer(ctx, containerName, rmForce); err != nil {
40+
if err := manager.DeleteContainer(ctx, containerName, rmForce, true); err != nil {
4141
return fmt.Errorf("failed to delete container: %v", err)
4242
}
4343

cmd/thv/app/run.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
255255
return fmt.Errorf("failed to retrieve or pull image: %v", err)
256256
}
257257

258+
// pull the egress image if it is not already pulled
259+
if err := pullImage(ctx, config.EgressImage, rt); err != nil {
260+
return fmt.Errorf("failed to retrieve or pull egress image: %v", err)
261+
}
262+
258263
// Configure the RunConfig with transport, ports, permissions, etc.
259264
if err := configureRunConfig(runConfig, runTransport, runPort, runTargetPort, runEnv); err != nil {
260265
return err

cmd/thv/app/stop.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/spf13/cobra"
88

99
"github.com/stacklok/toolhive/pkg/lifecycle"
10+
"github.com/stacklok/toolhive/pkg/logger"
1011
)
1112

1213
var stopCmd = &cobra.Command{
@@ -47,5 +48,16 @@ func stopCmdFunc(cmd *cobra.Command, args []string) error {
4748
fmt.Printf("Container %s stopped successfully\n", containerName)
4849
}
4950

51+
// Stop associated egress container
52+
egressContainerName := containerName + "-egress"
53+
err = manager.StopContainer(ctx, egressContainerName)
54+
if err != nil {
55+
if errors.Is(err, lifecycle.ErrContainerNotFound) {
56+
logger.Infof("Egress container %s is not running", egressContainerName)
57+
} else {
58+
return fmt.Errorf("failed to stop egress container %q: %w", egressContainerName, err)
59+
}
60+
}
61+
5062
return nil
5163
}

pkg/api/v1/servers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func (s *ServerRoutes) deleteServer(w http.ResponseWriter, r *http.Request) {
145145
ctx := r.Context()
146146
name := chi.URLParam(r, "name")
147147
forceDelete := r.URL.Query().Get("force") == "true"
148-
err := s.manager.DeleteContainer(ctx, name, forceDelete)
148+
err := s.manager.DeleteContainer(ctx, name, forceDelete, true)
149149
if err != nil {
150150
if errors.Is(err, lifecycle.ErrContainerNotFound) {
151151
http.Error(w, "Server not found", http.StatusNotFound)

pkg/container/docker/client.go

Lines changed: 89 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ func NewClient(ctx context.Context) (*Client, error) {
7979

8080
c, err := NewClientWithSocketPath(ctx, socketPath, runtimeType)
8181
if err != nil {
82-
logger.Debugf("Failed to create client for %s: %v", sp, err)
8382
lastErr = err
83+
logger.Debugf("Failed to create client for %s: %v", sp, err)
8484
continue
8585
}
8686

@@ -208,12 +208,16 @@ func setupPortBindings(hostConfig *container.HostConfig, portBindings map[string
208208
// If options is nil, default options will be used.
209209
func (c *Client) DeployWorkload(
210210
ctx context.Context,
211-
image, name string,
211+
image,
212+
name string,
212213
command []string,
213-
envVars, labels map[string]string,
214+
envVars,
215+
labels map[string]string,
214216
permissionProfile *permissions.Profile,
215217
transportType string,
216218
options *runtime.DeployWorkloadOptions,
219+
isMcpServer bool,
220+
isEgress bool,
217221
) (string, error) {
218222
// Get permission config from profile
219223
permissionConfig, err := c.getPermissionConfigFromProfile(permissionProfile, transportType)
@@ -262,8 +266,13 @@ func (c *Client) DeployWorkload(
262266
}
263267
}
264268

269+
containerName := name
270+
if isEgress {
271+
containerName = fmt.Sprintf("%s-egress", name)
272+
}
273+
265274
// Check if container with this name already exists
266-
existingID, err := c.findExistingContainer(ctx, name)
275+
existingID, err := c.findExistingContainer(ctx, containerName)
267276
if err != nil {
268277
return "", err
269278
}
@@ -282,14 +291,32 @@ func (c *Client) DeployWorkload(
282291
// Container was removed and needs to be recreated
283292
}
284293

294+
// create network depending on type
295+
var endpointsConfig map[string]*network.EndpointSettings
296+
if isMcpServer {
297+
networkName := fmt.Sprintf("toolhive-%s-internal", name)
298+
endpointsConfig = map[string]*network.EndpointSettings{
299+
networkName: {},
300+
}
301+
} else if isEgress {
302+
internalNetworkName := fmt.Sprintf("toolhive-%s-internal", name)
303+
endpointsConfig = map[string]*network.EndpointSettings{
304+
internalNetworkName: {},
305+
"toolhive-external": {},
306+
}
307+
}
308+
networkConfig := &network.NetworkingConfig{
309+
EndpointsConfig: endpointsConfig,
310+
}
311+
285312
// Create the container
286313
resp, err := c.client.ContainerCreate(
287314
ctx,
288315
config,
289316
hostConfig,
290-
&network.NetworkingConfig{},
317+
networkConfig,
291318
nil,
292-
name,
319+
containerName,
293320
)
294321
if err != nil {
295322
return "", NewContainerError(err, "", fmt.Sprintf("failed to create container: %v", err))
@@ -844,29 +871,6 @@ func convertRelativePathToAbsolute(source string, mountDecl permissions.MountDec
844871
return absPath, true
845872
}
846873

847-
// needsNetworkAccess determines if the container needs network access
848-
func (*Client) needsNetworkAccess(profile *permissions.Profile, transportType string) bool {
849-
// SSE transport always needs network access
850-
if transportType == "sse" || transportType == "inspector" {
851-
return true
852-
}
853-
854-
// Check if the profile has network settings that require network access
855-
if profile.Network != nil && profile.Network.Outbound != nil {
856-
outbound := profile.Network.Outbound
857-
858-
// Any of these conditions require network access
859-
if outbound.InsecureAllowAll ||
860-
len(outbound.AllowTransport) > 0 ||
861-
len(outbound.AllowHost) > 0 ||
862-
len(outbound.AllowPort) > 0 {
863-
return true
864-
}
865-
}
866-
867-
return false
868-
}
869-
870874
// getPermissionConfigFromProfile converts a permission profile to a container permission config
871875
func (c *Client) getPermissionConfigFromProfile(
872876
profile *permissions.Profile,
@@ -885,16 +889,16 @@ func (c *Client) getPermissionConfigFromProfile(
885889
c.addReadOnlyMounts(config, profile.Read)
886890
c.addReadWriteMounts(config, profile.Write)
887891

888-
// Determine network mode
889-
if c.needsNetworkAccess(profile, transportType) {
890-
config.NetworkMode = "bridge"
891-
}
892-
893892
// Validate transport type
894893
if transportType != "sse" && transportType != "stdio" && transportType != "inspector" {
895894
return nil, fmt.Errorf("unsupported transport type: %s", transportType)
896895
}
897896

897+
// Add necessary capabilities for egress containers
898+
if profile.Name == permissions.ProfileEgress {
899+
config.CapAdd = append(config.CapAdd, "CAP_SETUID", "CAP_SETGID")
900+
}
901+
898902
return config, nil
899903
}
900904

@@ -1240,3 +1244,54 @@ func (c *Client) handleExistingContainer(
12401244
// Container was removed and needs to be recreated
12411245
return false, nil
12421246
}
1247+
1248+
// CreateNetwork creates a network following configuration.
1249+
func (c *Client) CreateNetwork(
1250+
ctx context.Context,
1251+
name string,
1252+
labels map[string]string,
1253+
internal bool,
1254+
) (string, error) {
1255+
// Check if the network already exists
1256+
networks, err := c.client.NetworkList(ctx, network.ListOptions{
1257+
Filters: filters.NewArgs(filters.Arg("name", name)),
1258+
})
1259+
if err != nil {
1260+
return "", fmt.Errorf("failed to list networks: %w", err)
1261+
}
1262+
if len(networks) > 0 {
1263+
// Network already exists, return its ID
1264+
return networks[0].ID, nil
1265+
}
1266+
1267+
networkCreate := network.CreateOptions{
1268+
Driver: "bridge",
1269+
Internal: internal,
1270+
Labels: labels,
1271+
}
1272+
1273+
resp, err := c.client.NetworkCreate(ctx, name, networkCreate)
1274+
if err != nil {
1275+
return "", err
1276+
}
1277+
return resp.ID, nil
1278+
}
1279+
1280+
// DeleteNetwork deletes a network by name.
1281+
func (c *Client) DeleteNetwork(ctx context.Context, name string) error {
1282+
// find the network by name
1283+
networks, err := c.client.NetworkList(ctx, network.ListOptions{
1284+
Filters: filters.NewArgs(filters.Arg("name", name)),
1285+
})
1286+
if err != nil {
1287+
return err
1288+
}
1289+
if len(networks) == 0 {
1290+
return fmt.Errorf("network %s not found", name)
1291+
}
1292+
1293+
if err := c.client.NetworkRemove(ctx, networks[0].ID); err != nil {
1294+
return fmt.Errorf("failed to remove network %s: %w", name, err)
1295+
}
1296+
return nil
1297+
}

pkg/container/kubernetes/client.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,9 @@ func (c *Client) DeployWorkload(ctx context.Context,
256256
containerLabels map[string]string,
257257
_ *permissions.Profile, // TODO: Implement permission profile support for Kubernetes
258258
transportType string,
259-
options *runtime.DeployWorkloadOptions) (string, error) {
259+
options *runtime.DeployWorkloadOptions,
260+
_ bool,
261+
_ bool) (string, error) {
260262
namespace := getCurrentNamespace()
261263
containerLabels["app"] = containerName
262264
containerLabels["toolhive"] = "true"
@@ -567,6 +569,19 @@ func (*Client) StopWorkload(_ context.Context, _ string) error {
567569
return nil
568570
}
569571

572+
// CreateNetwork implements runtime.Runtime.
573+
func (*Client) CreateNetwork(_ context.Context, _ string, _ map[string]string, _ bool) (string, error) {
574+
// just noop
575+
logger.Infof("CreateNetwork is not supported in Kubernetes runtime. Skipping network creation.")
576+
return "", nil
577+
}
578+
579+
// DeleteNetwork implements runtime.Runtime.
580+
func (*Client) DeleteNetwork(_ context.Context, _ string) error {
581+
logger.Infof("DeleteNetwork is not supported in Kubernetes runtime. Skipping network deletion.")
582+
return nil
583+
}
584+
570585
// waitForStatefulSetReady waits for a statefulset to be ready using the watch API
571586
func waitForStatefulSetReady(ctx context.Context, clientset kubernetes.Interface, namespace, name string) error {
572587
// Create a field selector to watch only this specific statefulset

pkg/container/kubernetes/client_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ func TestCreateContainerWithPodTemplatePatch(t *testing.T) {
179179
nil,
180180
"stdio",
181181
options,
182+
false,
183+
false,
182184
)
183185

184186
// Check that there was no error
@@ -664,6 +666,8 @@ func TestCreateContainerWithMCP(t *testing.T) {
664666
nil,
665667
tc.transportType,
666668
tc.options,
669+
false,
670+
false,
667671
)
668672

669673
// Check that there was no error

pkg/container/runtime/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ type Runtime interface {
8181
permissionProfile *permissions.Profile,
8282
transportType string,
8383
options *DeployWorkloadOptions,
84+
isMcpServer bool,
85+
isEgress bool,
8486
) (string, error)
8587

8688
// ListWorkloads lists all deployed workloads managed by this runtime.
@@ -131,6 +133,12 @@ type Runtime interface {
131133

132134
// BuildImage builds a Docker image from a Dockerfile in the specified context directory
133135
BuildImage(ctx context.Context, contextDir, imageName string) error
136+
137+
// CreateNetwork creates a network
138+
CreateNetwork(ctx context.Context, networkName string, labels map[string]string, internal bool) (string, error)
139+
140+
// DeleteNetwork deletes a network
141+
DeleteNetwork(ctx context.Context, networkName string) error
134142
}
135143

136144
// Monitor defines the interface for container monitoring

pkg/labels/labels.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ const (
2828

2929
// LabelToolType is the label that indicates the type of tool
3030
LabelToolType = "toolhive-tool-type"
31+
32+
// LabelEnabledValue is the value for the LabelEnabled label
33+
LabelEnabledValue = "true"
3134
)
3235

3336
// AddStandardLabels adds standard labels to a container
3437
func AddStandardLabels(labels map[string]string, containerName, containerBaseName, transportType string, port int) {
3538
// Add standard labels
36-
labels[LabelEnabled] = "true"
39+
labels[LabelEnabled] = LabelEnabledValue
3740
labels[LabelName] = containerName
3841
labels[LabelBaseName] = containerBaseName
3942
labels[LabelTransport] = transportType
@@ -43,15 +46,21 @@ func AddStandardLabels(labels map[string]string, containerName, containerBaseNam
4346
labels[LabelToolType] = "mcp"
4447
}
4548

49+
// AddNetworkLabels adds network-related labels to a network
50+
func AddNetworkLabels(labels map[string]string, networkName string) {
51+
labels[LabelEnabled] = LabelEnabledValue
52+
labels[LabelName] = networkName
53+
}
54+
4655
// FormatToolHiveFilter formats a filter for ToolHive containers
4756
func FormatToolHiveFilter() string {
48-
return fmt.Sprintf("%s=true", LabelEnabled)
57+
return fmt.Sprintf("%s=%s", LabelEnabled, LabelEnabledValue)
4958
}
5059

5160
// IsToolHiveContainer checks if a container is managed by ToolHive
5261
func IsToolHiveContainer(labels map[string]string) bool {
5362
value, ok := labels[LabelEnabled]
54-
return ok && strings.ToLower(value) == "true"
63+
return ok && strings.ToLower(value) == LabelEnabledValue
5564
}
5665

5766
// GetContainerName gets the container name from labels

0 commit comments

Comments
 (0)