Skip to content

Commit 282e6fd

Browse files
committed
wip: spin egress container for each mcp server
Closes: #124
1 parent 6a8e262 commit 282e6fd

File tree

13 files changed

+297
-12
lines changed

13 files changed

+297
-12
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
@@ -254,6 +254,11 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
254254
return fmt.Errorf("failed to retrieve or pull image: %v", err)
255255
}
256256

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

cmd/thv/app/stop.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,16 @@ func stopCmdFunc(cmd *cobra.Command, args []string) error {
4747
fmt.Printf("Container %s stopped successfully\n", containerName)
4848
}
4949

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

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: 57 additions & 1 deletion
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

@@ -895,6 +895,11 @@ func (c *Client) getPermissionConfigFromProfile(
895895
return nil, fmt.Errorf("unsupported transport type: %s", transportType)
896896
}
897897

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

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

pkg/container/kubernetes/client.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
backoff "github.com/cenkalti/backoff/v4"
1616
appsv1 "k8s.io/api/apps/v1"
1717
corev1 "k8s.io/api/core/v1"
18+
networkingv1 "k8s.io/api/networking/v1"
19+
1820
"k8s.io/apimachinery/pkg/api/errors"
1921
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2022
"k8s.io/apimachinery/pkg/util/intstr"
@@ -567,6 +569,85 @@ func (*Client) StopWorkload(_ context.Context, _ string) error {
567569
return nil
568570
}
569571

572+
func (c *Client) CreateNetwork(ctx context.Context, name string, labels map[string]string, internal bool) (string, error) {
573+
namespace := getCurrentNamespace()
574+
575+
// Check if the NetworkPolicy already exists
576+
_, err := c.client.NetworkingV1().NetworkPolicies(namespace).Get(ctx, name, metav1.GetOptions{})
577+
if err == nil {
578+
return name, nil // NetworkPolicy already exists
579+
}
580+
if !errors.IsNotFound(err) {
581+
return "", fmt.Errorf("failed to check if NetworkPolicy exists: %w", err)
582+
}
583+
584+
// Define the NetworkPolicy spec based on the 'internal' flag
585+
policyTypes := []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}
586+
var ingressRules []networkingv1.NetworkPolicyIngressRule
587+
588+
if internal {
589+
// Restrict ingress to pods with the same labels
590+
ingressRules = []networkingv1.NetworkPolicyIngressRule{
591+
{
592+
From: []networkingv1.NetworkPolicyPeer{
593+
{
594+
PodSelector: &metav1.LabelSelector{
595+
MatchLabels: labels,
596+
},
597+
},
598+
},
599+
},
600+
}
601+
} else {
602+
// Allow all ingress traffic
603+
ingressRules = []networkingv1.NetworkPolicyIngressRule{
604+
{
605+
From: []networkingv1.NetworkPolicyPeer{
606+
{
607+
NamespaceSelector: &metav1.LabelSelector{},
608+
},
609+
},
610+
},
611+
}
612+
}
613+
614+
// Create the NetworkPolicy object
615+
policy := &networkingv1.NetworkPolicy{
616+
ObjectMeta: metav1.ObjectMeta{
617+
Name: name,
618+
Namespace: namespace,
619+
Labels: labels,
620+
},
621+
Spec: networkingv1.NetworkPolicySpec{
622+
PodSelector: metav1.LabelSelector{
623+
MatchLabels: labels,
624+
},
625+
PolicyTypes: policyTypes,
626+
Ingress: ingressRules,
627+
},
628+
}
629+
630+
// Create the NetworkPolicy in Kubernetes
631+
_, err = c.client.NetworkingV1().NetworkPolicies(namespace).Create(ctx, policy, metav1.CreateOptions{})
632+
if err != nil {
633+
return "", fmt.Errorf("failed to create NetworkPolicy: %w", err)
634+
}
635+
636+
return name, nil
637+
}
638+
639+
func (c *Client) DeleteNetwork(ctx context.Context, name string) error {
640+
namespace := getCurrentNamespace() // Implement this function to retrieve the current namespace
641+
642+
err := c.client.NetworkingV1().NetworkPolicies(namespace).Delete(ctx, name, metav1.DeleteOptions{})
643+
if err != nil {
644+
return fmt.Errorf("failed to delete network policy %q in namespace %q: %w", name, namespace, err)
645+
}
646+
647+
fmt.Printf("Network policy %q in namespace %q has been deleted successfully.\n", name, namespace)
648+
return nil
649+
}
650+
570651
// waitForStatefulSetReady waits for a statefulset to be ready using the watch API
571652
func waitForStatefulSetReady(ctx context.Context, clientset kubernetes.Interface, namespace, name string) error {
572653
// Create a field selector to watch only this specific statefulset

pkg/container/runtime/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ type Runtime interface {
131131

132132
// BuildImage builds a Docker image from a Dockerfile in the specified context directory
133133
BuildImage(ctx context.Context, contextDir, imageName string) error
134+
135+
// CreateNetwork creates a network
136+
CreateNetwork(ctx context.Context, networkName string, labels map[string]string, internal bool) (string, error)
137+
138+
// DeleteNetwork deletes a network
139+
DeleteNetwork(ctx context.Context, networkName string) error
134140
}
135141

136142
// Monitor defines the interface for container monitoring

pkg/labels/labels.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ func AddStandardLabels(labels map[string]string, containerName, containerBaseNam
4343
labels[LabelToolType] = "mcp"
4444
}
4545

46+
// AddNetworkLabels adds network-related labels to a network
47+
func AddNetworkLabels(labels map[string]string, networkName string) {
48+
labels[LabelEnabled] = "true"
49+
labels[LabelName] = networkName
50+
}
51+
4652
// FormatToolHiveFilter formats a filter for ToolHive containers
4753
func FormatToolHiveFilter() string {
4854
return fmt.Sprintf("%s=true", LabelEnabled)

pkg/lifecycle/manager.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ type Manager interface {
2929
// ListContainers lists all ToolHive-managed containers.
3030
ListContainers(ctx context.Context, listAll bool) ([]rt.ContainerInfo, error)
3131
// DeleteContainer deletes a container and its associated proxy process.
32-
DeleteContainer(ctx context.Context, name string, forceDelete bool) error
32+
DeleteContainer(ctx context.Context, name string, forceDelete bool, removeConfig bool) error
33+
// DeleteNetwork deletes a network.
34+
DeleteNetwork(ctx context.Context, name string) error
3335
// StopContainer stops a container and its associated proxy process.
3436
StopContainer(ctx context.Context, name string) error
3537
// RunContainer runs a container in the foreground.
@@ -85,7 +87,7 @@ func (d *defaultManager) ListContainers(ctx context.Context, listAll bool) ([]rt
8587
return toolHiveContainers, nil
8688
}
8789

88-
func (d *defaultManager) DeleteContainer(ctx context.Context, name string, forceDelete bool) error {
90+
func (d *defaultManager) DeleteContainer(ctx context.Context, name string, forceDelete bool, removeConfig bool) error {
8991
// We need several fields from the container struct for deletion.
9092
container, err := d.findContainerByName(ctx, name)
9193
if err != nil {
@@ -127,15 +129,15 @@ func (d *defaultManager) DeleteContainer(ctx context.Context, name string, force
127129
} else {
128130
logger.Infof("Saved state for %s removed", baseName)
129131
}
130-
}
131132

132-
logger.Infof("Container %s removed", name)
133+
logger.Infof("Container %s removed", name)
133134

134-
if shouldRemoveClientConfig() {
135-
if err := removeClientConfigurations(name); err != nil {
136-
logger.Warnf("Warning: Failed to remove client configurations: %v", err)
137-
} else {
138-
logger.Infof("Client configurations for %s removed", name)
135+
if shouldRemoveClientConfig() {
136+
if err := removeClientConfigurations(name); err != nil {
137+
logger.Warnf("Warning: Failed to remove client configurations: %v", err)
138+
} else {
139+
logger.Infof("Client configurations for %s removed", name)
140+
}
139141
}
140142
}
141143

@@ -447,6 +449,16 @@ func (d *defaultManager) stopContainer(ctx context.Context, containerID, contain
447449
return nil
448450
}
449451

452+
func (d *defaultManager) DeleteNetwork(ctx context.Context, name string) error {
453+
// Remove the network
454+
logger.Infof("Removing network %s...", name)
455+
if err := d.runtime.DeleteNetwork(ctx, name); err != nil {
456+
return fmt.Errorf("failed to remove network: %v", err)
457+
}
458+
459+
return nil
460+
}
461+
450462
func shouldRemoveClientConfig() bool {
451463
c := config.GetConfig()
452464
return len(c.Clients.RegisteredClients) > 0 || c.Clients.AutoDiscovery

pkg/permissions/profile.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ const (
1717
ProfileNone = "none"
1818
// ProfileNetwork is the name of the built-in profile with network permissions
1919
ProfileNetwork = "network"
20+
// ProfileEgress is the name of the built-in profile with egress permissions
21+
ProfileEgress = "egress"
2022
)
2123

2224
// Profile represents a permission profile for a container
2325
type Profile struct {
26+
// Name is the name of the profile
27+
Name string `json:"name,omitempty"`
28+
2429
// Read is a list of mount declarations that the container can read from
2530
// These can be in the following formats:
2631
// - A single path: The same path will be mounted from host to container
@@ -60,6 +65,7 @@ type OutboundNetworkPermissions struct {
6065
// NewProfile creates a new permission profile
6166
func NewProfile() *Profile {
6267
return &Profile{
68+
Name: ProfileNone,
6369
Read: []MountDeclaration{},
6470
Write: []MountDeclaration{},
6571
Network: &NetworkPermissions{
@@ -94,6 +100,7 @@ func FromFile(path string) (*Profile, error) {
94100
// BuiltinNoneProfile returns the built-in profile with no permissions
95101
func BuiltinNoneProfile() *Profile {
96102
return &Profile{
103+
Name: ProfileNone,
97104
Read: []MountDeclaration{},
98105
Write: []MountDeclaration{},
99106
Network: &NetworkPermissions{
@@ -110,6 +117,7 @@ func BuiltinNoneProfile() *Profile {
110117
// BuiltinNetworkProfile returns the built-in network profile
111118
func BuiltinNetworkProfile() *Profile {
112119
return &Profile{
120+
Name: ProfileNetwork,
113121
Read: []MountDeclaration{},
114122
Write: []MountDeclaration{},
115123
Network: &NetworkPermissions{
@@ -123,6 +131,23 @@ func BuiltinNetworkProfile() *Profile {
123131
}
124132
}
125133

134+
// BuiltinEgressProfile returns the built-in egress profile
135+
func BuiltinEgressProfile() *Profile {
136+
return &Profile{
137+
Name: ProfileEgress,
138+
Read: []MountDeclaration{},
139+
Write: []MountDeclaration{},
140+
Network: &NetworkPermissions{
141+
Outbound: &OutboundNetworkPermissions{
142+
InsecureAllowAll: true,
143+
AllowTransport: []string{"tcp", "udp"},
144+
AllowHost: []string{"*"},
145+
AllowPort: []int{0, 65535},
146+
},
147+
},
148+
}
149+
}
150+
126151
// MountDeclaration represents a mount declaration for a container
127152
// It can be in one of the following formats:
128153
// - A single path: The same path will be mounted from host to container

0 commit comments

Comments
 (0)