diff --git a/README.md b/README.md index 37c0e9c..42a3460 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ The MCP DigitalOcean Integration supports the following services, allowing users | apps | Manage DigitalOcean App Platform applications, including deployments and configurations. | | droplets | Create, manage, resize, snapshot, and monitor droplets (virtual machines) on DigitalOcean. | | accounts | Get information about your DigitalOcean account, billing, balance, invoices, and SSH keys. | -| networking | Manage domains, DNS records, certificates, firewalls, reserved IPs, BYOIP Prefixes, VPCs, and CDNs. | +| networking | Manage domains, DNS records, certificates, firewalls, load balancers, reserved IPs, BYOIP Prefixes, VPCs, and CDNs. | | insights | Monitors your resources, endpoints and alert you when they're slow, unavailable, or SSL certificates are expiring. | | spaces | DigitalOcean Spaces object storage and Spaces access keys for S3-compatible storage. | | databases | Provision, manage, and monitor managed database clusters (Postgres, MySQL, Redis, etc.). | diff --git a/pkg/registry/networking/README.md b/pkg/registry/networking/README.md index 5583aa0..ca12ada 100644 --- a/pkg/registry/networking/README.md +++ b/pkg/registry/networking/README.md @@ -1,6 +1,6 @@ # Networking MCP Tools -This directory contains tools and resources for managing DigitalOcean networking features via the MCP Server. These tools enable you to create, modify, and query networking resources such as domains, certificates, firewalls, reserved IPs, VPCs, and CDNs. +This directory contains tools and resources for managing DigitalOcean networking features via the MCP Server. These tools enable you to create, modify, and query networking resources such as domains, certificates, firewalls, load balancers, reserved IPs, VPCs, and CDNs. --- @@ -162,6 +162,99 @@ This directory contains tools and resources for managing DigitalOcean networking --- +### Load Balancers + +- **load-balancer-create** + Create a load balancer. + - `Name` (string, required): Name of the load balancer. + - `Region` (string, required for regional load balancer types): Region slug (e.g., nyc3) + - `DropletIDs` (array of strings, optional): IDs of the Droplets assigned to the load balancer + - `Tag` (string, optional): Droplet tag corresponding to Droplets assigned to the load balancer + - `ForwardingRules` (array of objects, required for regional load balancer types): Forwarding rules to add + - `EntryProtocol` (string, required): The protocol used for traffic to the load balancer. The possible values are: http, https, http2, http3, tcp, or udp. + - `EntryPort` (number, required): The port on which the load balancer instance will listen. (e.g., 80, 443) + - `TargetProtocol` (string, required): The protocol used for traffic from the load balancer to the backend Droplets. The possible values are: http, https, http2, tcp, or udp + - `TargetPort` (number, required): The port on the backend Droplets to which the load balancer will send traffic. + - `TlsPassthrough` (bool, optional): A boolean value indicating whether SSL encrypted traffic will be passed through to the backend Droplets. + - `Type` (string, optional): Type of the load balancer (REGIONAL, REGIONAL_NETWORK, GLOBAL). Default is REGIONAL. + - `Network` (string, optional): Network type of the load balancer (EXTERNAL, INTERNAL). Default is EXTERNAL. + - `SizeUnit` (number, optional): Size of the load balancer in units appropriate to its type. + - `NetworkStack` (string, optional): Network stack of the load balancer (IPV4, DUALSTACK) + - `ProjectID` (string, optional): Project ID to which the load balancer will be assigned + - `TargetLoadBalancerIDs` (array of strings, optional): IDs of the target regional load balancers for a global load balancer + - `GLBSettings` (object, required for GLOBAL load balancer type): Forwarding configurations for a Global load balancer. + +- **load-balancer-delete** + Delete a load balancer by ID. + - `LoadBalancerID` (string, required): ID of the load balancer. + +- **load-balancer-delete-cache** + Delete the CDN cache of a global load balancer by ID. + - `LoadBalancerID` (string, required): ID of the load balancer. + +- **load-balancer-get** + Get a load balancer by ID. + - `LoadBalancerID` (string, required): ID of the load balancer. + +- **load-balancer-list** + List load balancers with pagination. + - `Page` (number, default: 1): Page number + - `PerPage` (number, default: 20): Items per page + +- **load-balancer-add-droplets** + Add droplets to a load balancer. + - `LoadBalancerID` (string, required): ID of the load balancer + - `DropletIDs` (array of numbers, required): Droplet IDs to assign to the load balancer + +- **load-balancer-remove-droplets** + Remove droplets from a load balancer. + - `LoadBalancerID` (string, required): ID of the load balancer + - `DropletIDs` (array of numbers, required): Droplet IDs to remove + +- **load-balancer-update** + Update a load balancer. + - `LoadBalancerID` (string, required): ID of the load balancer. + - - `Name` (string, required): Name of the load balancer. + - `Region` (string, required for regional load balancer types): Region slug (e.g., nyc3) + - `DropletIDs` (array of strings, optional): IDs of the Droplets assigned to the load balancer + - `Tag` (string, optional): Droplet tag corresponding to Droplets assigned to the load balancer + - `ForwardingRules` (array of objects, optional): Forwarding rules to add + - `EntryProtocol` (string, required): The protocol used for traffic to the load balancer. The possible values are: http, https, http2, http3, tcp, or udp. + - `EntryPort` (number, required): The port on which the load balancer instance will listen. (e.g., 80, 443) + - `TargetProtocol` (string, required): The protocol used for traffic from the load balancer to the backend Droplets. The possible values are: http, https, http2, tcp, or udp + - `TargetPort` (number, required): The port on the backend Droplets to which the load balancer will send traffic. + - `TlsPassthrough` (bool, optional): A boolean value indicating whether SSL encrypted traffic will be passed through to the backend Droplets. + - `Type` (string, optional): Type of the load balancer (REGIONAL, REGIONAL_NETWORK, GLOBAL). Default is REGIONAL. + - `Network` (string, optional): Network type of the load balancer (EXTERNAL, INTERNAL). Default is EXTERNAL. + - `SizeUnit` (number, optional): Size of the load balancer in units appropriate to its type. + - `NetworkStack` (string, optional): Network stack of the load balancer (IPV4, DUALSTACK) + - `ProjectID` (string, optional): Project ID to which the load balancer will be assigned + - `TargetLoadBalancerIDs` (array of strings, optional): IDs of the target regional load balancers for a global load balancer + - `GLBSettings` (object, required for GLOBAL load balancer type): Forwarding configurations for a Global load balancer. + + +- **load-balancer-add-forwarding-rules** + Add forwarding rules to a load balancer. + - `LoadBalancerID` (string, required): ID of the load balancer + - `ForwardingRules` (array of objects, required): Forwarding rules to add + - `EntryProtocol` (string, required): The protocol used for traffic to the load balancer. The possible values are: http, https, http2, http3, tcp, or udp. + - `EntryPort` (number, required): The port on which the load balancer instance will listen. (e.g., 80, 443) + - `TargetProtocol` (string, required): The protocol used for traffic from the load balancer to the backend Droplets. The possible values are: http, https, http2, tcp, or udp + - `TargetPort` (number, required): The port on the backend Droplets to which the load balancer will send traffic. + - `TlsPassthrough` (bool, optional): A boolean value indicating whether SSL encrypted traffic will be passed through to the backend Droplets. + +- **load-balancer-remove-forwarding-rules** + Remove forwarding rules from a load balancer. + - `LoadBalancerID` (string, required): ID of the load balancer + - `ForwardingRules` (array of objects, required): Forwarding rules to add + - `EntryProtocol` (string, required): The protocol used for traffic to the load balancer. The possible values are: http, https, http2, http3, tcp, or udp. + - `EntryPort` (number, required): The port on which the load balancer instance will listen. (e.g., 80, 443) + - `TargetProtocol` (string, required): The protocol used for traffic from the load balancer to the backend Droplets. The possible values are: http, https, http2, tcp, or udp + - `TargetPort` (number, required): The port on the backend Droplets to which the load balancer will send traffic. + - `TlsPassthrough` (bool, optional): A boolean value indicating whether SSL encrypted traffic will be passed through to the backend Droplets. + +--- + ### Reserved IPs @@ -273,6 +366,6 @@ This directory contains tools and resources for managing DigitalOcean networking - All resource identifiers (IDs, names, IPs) must be replaced with actual values in your queries. - All responses are returned in JSON format for easy parsing and integration. - For endpoints that require an ID, name, or IP, replace the placeholder with the appropriate value. -- Use the tools to automate and manage all aspects of networking from domains and DNS to VPCs, firewalls, and advanced partner connectivity. +- Use the tools to automate and manage all aspects of networking from domains and DNS to VPCs, firewalls, load balancers, and advanced partner connectivity. --- diff --git a/pkg/registry/networking/generate.go b/pkg/registry/networking/generate.go index ffe7c13..0d90f58 100644 --- a/pkg/registry/networking/generate.go +++ b/pkg/registry/networking/generate.go @@ -1,3 +1,3 @@ package networking -//go:generate mockgen -destination=./mocks.go -package networking github.com/digitalocean/godo CertificatesService,DomainsService,FirewallsService,PartnerAttachmentService,ReservedIPsService,ReservedIPV6sService,ReservedIPActionsService,ReservedIPV6ActionsService,VPCsService +//go:generate mockgen -destination=./mocks.go -package networking github.com/digitalocean/godo CertificatesService,DomainsService,FirewallsService,LoadBalancersService,PartnerAttachmentService,ReservedIPsService,ReservedIPV6sService,ReservedIPActionsService,ReservedIPV6ActionsService,VPCsService,BYOIPPrefixesService diff --git a/pkg/registry/networking/load_balancers_tools.go b/pkg/registry/networking/load_balancers_tools.go new file mode 100644 index 0000000..3445dd9 --- /dev/null +++ b/pkg/registry/networking/load_balancers_tools.go @@ -0,0 +1,638 @@ +package networking + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/digitalocean/godo" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// LoadBalancersTool provides load balancer management tools +type LoadBalancersTool struct { + client func(ctx context.Context) (*godo.Client, error) +} + +// NewLoadBalancersTool creates a new LoadBalancersTool +func NewLoadBalancersTool(client func(ctx context.Context) (*godo.Client, error)) *LoadBalancersTool { + return &LoadBalancersTool{ + client: client, + } +} + +func parseForwardingRules(rules []any) ([]godo.ForwardingRule, *mcp.CallToolResult) { + forwardingRules := []godo.ForwardingRule{} + for _, ruleData := range rules { + rule, ok := ruleData.(map[string]any) + if !ok { + return nil, mcp.NewToolResultError("invalid rule format") + } + + entryProtocol, ok := rule["EntryProtocol"].(string) + if !ok { + return nil, mcp.NewToolResultError("EntryProtocol must be a string") + } + entryPort, ok := rule["EntryPort"].(float64) + if !ok { + return nil, mcp.NewToolResultError("EntryPort must be a number") + } + targetProtocol, ok := rule["TargetProtocol"].(string) + if !ok { + return nil, mcp.NewToolResultError("TargetProtocol must be a string") + } + targetPort, ok := rule["TargetPort"].(float64) + if !ok { + return nil, mcp.NewToolResultError("TargetPort must be a number") + } + // set tlsPassthrough to false if not provided + tlsPassthrough := false + if val, ok := rule["TlsPassthrough"].(bool); ok { + tlsPassthrough = val + } + // set the certificate id to empty string if not provided + certificateID := "" + if val, ok := rule["CertificateID"].(string); ok { + certificateID = val + } + + forwardingRule := godo.ForwardingRule{ + EntryProtocol: entryProtocol, + EntryPort: int(entryPort), + TargetProtocol: targetProtocol, + TargetPort: int(targetPort), + TlsPassthrough: tlsPassthrough, + CertificateID: certificateID, + } + forwardingRules = append(forwardingRules, forwardingRule) + } + return forwardingRules, nil +} + +func (l *LoadBalancersTool) createLoadBalancer(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := req.GetArguments() + name, ok := args["Name"].(string) + if !ok || name == "" { + return mcp.NewToolResultError("Name is required"), nil + } + // Optional arguments + lbType, _ := args["Type"].(string) + network, _ := args["Network"].(string) + sizeUnit, _ := args["SizeUnit"].(float64) + networkStack, _ := args["NetworkStack"].(string) + projectID, _ := args["ProjectID"].(string) + + lbr := &godo.LoadBalancerRequest{ + Name: name, + SizeUnit: uint32(sizeUnit), + Type: lbType, + Network: network, + NetworkStack: networkStack, + ProjectID: projectID, + } + + // Global load balancer arguments + if lbType == "GLOBAL" { + targetLoadBalancerIDs, ok := args["TargetLoadBalancerIDs"].([]string) + if ok && len(targetLoadBalancerIDs) > 0 { + lbr.TargetLoadBalancerIDs = targetLoadBalancerIDs + } + + // Parse GLB settings + if glbSettings, ok := args["GLBSettings"].(map[string]any); ok && len(glbSettings) > 0 { + targetProtocol, _ := glbSettings["TargetProtocol"].(string) + targetPort, _ := glbSettings["TargetPort"].(float64) + + cdnSettings := &godo.CDNSettings{} + if cdn, ok := glbSettings["CDN"].(map[string]any); ok { + if isEnabled, ok := cdn["IsEnabled"].(bool); ok { + cdnSettings.IsEnabled = isEnabled + } + } + + rp := make(map[string]uint32) + if regionPriorities, ok := glbSettings["RegionPriorities"].(map[string]any); ok { + for k, v := range regionPriorities { + if val, ok := v.(float64); ok { + rp[k] = uint32(val) + } + } + } + + failoverThreshold, _ := glbSettings["FailoverThreshold"].(float64) + + lbr.GLBSettings = &godo.GLBSettings{ + TargetProtocol: targetProtocol, + TargetPort: uint32(targetPort), + CDN: cdnSettings, + RegionPriorities: rp, + FailoverThreshold: uint32(failoverThreshold), + } + } + } else { + // Regional load balancer arguments + region, ok := args["Region"].(string) + if !ok || region == "" { + return mcp.NewToolResultError("Region is required for REGIONAL and REGIONAL_NETWORK load balancers"), nil + } + lbr.Region = region + + // Parse forwarding rules + forwardingRules := []godo.ForwardingRule{} + if rules, ok := args["ForwardingRules"]; ok && rules != nil { + var err *mcp.CallToolResult + forwardingRules, err = parseForwardingRules(rules.([]any)) + if err != nil { + return err, nil + } + } + + if len(forwardingRules) == 0 { + return mcp.NewToolResultError("At least one forwarding rule must be provided"), nil + } + + lbr.ForwardingRules = forwardingRules + } + + // Target identifiers are optional but only one can be provided + tag, _ := args["Tag"].(string) + dropletIDs, _ := args["DropletIDs"].([]any) + if len(dropletIDs) > 0 && tag != "" { + return mcp.NewToolResultError("Only one target identifier (e.g. tag, droplets) can be specified"), nil + } + + // If droplet IDs are provided, make request with them + if len(dropletIDs) > 0 { + // Parse droplet IDs as ints + intDropletIDs := make([]int, len(dropletIDs)) + for i, id := range dropletIDs { + if did, ok := id.(float64); ok { + intDropletIDs[i] = int(did) + } + } + lbr.DropletIDs = intDropletIDs + } + // If tag is provided, make request with it + if tag != "" { + lbr.Tag = tag + } + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + lb, _, err := client.LoadBalancers.Create(ctx, lbr) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + jsonLB, err := json.MarshalIndent(lb, "", " ") + if err != nil { + return nil, fmt.Errorf("marshal error: %w", err) + } + return mcp.NewToolResultText(string(jsonLB)), nil +} + +func (l *LoadBalancersTool) deleteLoadBalancer(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + lbID := req.GetArguments()["LoadBalancerID"].(string) + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + _, err = client.LoadBalancers.Delete(ctx, lbID) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + + return mcp.NewToolResultText("Load Balancer deleted successfully"), nil +} + +func (l *LoadBalancersTool) deleteLoadBalancerCache(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + lbID := req.GetArguments()["LoadBalancerID"].(string) + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + _, err = client.LoadBalancers.PurgeCache(ctx, lbID) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + + return mcp.NewToolResultText("Load Balancer cache deleted successfully"), nil +} + +func (l *LoadBalancersTool) getLoadBalancer(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + lbID, ok := req.GetArguments()["LoadBalancerID"].(string) + if !ok || lbID == "" { + return mcp.NewToolResultError("LoadBalancer ID is required"), nil + } + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + lb, _, err := client.LoadBalancers.Get(ctx, lbID) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + jsonLB, err := json.MarshalIndent(lb, "", " ") + if err != nil { + return nil, fmt.Errorf("marshal error: %w", err) + } + return mcp.NewToolResultText(string(jsonLB)), nil +} + +func (l *LoadBalancersTool) listLoadBalancers(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + page, ok := req.GetArguments()["Page"].(float64) + if !ok { + page = 1 + } + perPage, ok := req.GetArguments()["PerPage"].(float64) + if !ok { + perPage = float64(20) + } + opt := &godo.ListOptions{ + Page: int(page), + PerPage: int(perPage), + } + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + lbs, _, err := client.LoadBalancers.List(ctx, opt) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + jsonLBs, err := json.MarshalIndent(lbs, "", " ") + if err != nil { + return nil, fmt.Errorf("marshal error: %w", err) + } + return mcp.NewToolResultText(string(jsonLBs)), nil +} + +func (l *LoadBalancersTool) addDroplets(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + lbID, ok := req.GetArguments()["LoadBalancerID"].(string) + if !ok || lbID == "" { + return mcp.NewToolResultError("Load Balancer ID is required"), nil + } + dropletIDs, ok := req.GetArguments()["DropletIDs"].([]any) + if !ok || len(dropletIDs) == 0 { + return mcp.NewToolResultError("Droplet IDs are required"), nil + } + dIDs := make([]int, len(dropletIDs)) + for i, id := range dropletIDs { + if did, ok := id.(float64); ok { + dIDs[i] = int(did) + } + } + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + _, err = client.LoadBalancers.AddDroplets(ctx, lbID, dIDs...) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + + return mcp.NewToolResultText("Droplets added successfully"), nil +} + +func (l *LoadBalancersTool) removeDroplets(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + lbID, ok := req.GetArguments()["LoadBalancerID"].(string) + if !ok || lbID == "" { + return mcp.NewToolResultError("Load Balancer ID is required"), nil + } + dropletIDs, ok := req.GetArguments()["DropletIDs"].([]any) + if !ok || len(dropletIDs) == 0 { + return mcp.NewToolResultError("Droplet IDs are required"), nil + } + dIDs := make([]int, len(dropletIDs)) + for i, id := range dropletIDs { + if did, ok := id.(float64); ok { + dIDs[i] = int(did) + } + } + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + _, err = client.LoadBalancers.RemoveDroplets(ctx, lbID, dIDs...) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + + return mcp.NewToolResultText("Droplets removed successfully"), nil +} + +func (l *LoadBalancersTool) updateLoadBalancer(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := req.GetArguments() + lbID, ok := args["LoadBalancerID"].(string) + if !ok || lbID == "" { + return mcp.NewToolResultError("Load Balancer ID is required"), nil + } + name, ok := args["Name"].(string) + if !ok || name == "" { + return mcp.NewToolResultError("Name is required"), nil + } + // Type is required for update with MCP to validate type-specific required arguments + // For example, Region is required for REGIONAL load balancers + // and GLBSettings is required for GLOBAL load balancers + // If Type is not provided and the existing load balancer is a GLOBAL load balancer + // and Region is not provided + // then api returns an error even though region is not required for GLOBAL load balancers + lbType, ok := args["Type"].(string) + if !ok || lbType == "" { + return mcp.NewToolResultError("Type is required"), nil + } + + // Optional arguments + network, _ := args["Network"].(string) + sizeUnit, _ := args["SizeUnit"].(float64) + networkStack, _ := args["NetworkStack"].(string) + projectID, _ := args["ProjectID"].(string) + + lbr := &godo.LoadBalancerRequest{ + Name: name, + SizeUnit: uint32(sizeUnit), + Type: lbType, + Network: network, + NetworkStack: networkStack, + ProjectID: projectID, + } + + if lbType == "GLOBAL" { + targetLoadBalancerIDs, ok := args["TargetLoadBalancerIDs"].([]string) + if ok && len(targetLoadBalancerIDs) > 0 { + lbr.TargetLoadBalancerIDs = targetLoadBalancerIDs + } + + // Parse GLB settings + if glbSettings, ok := args["GLBSettings"].(map[string]any); ok && len(glbSettings) > 0 { + targetProtocol, _ := glbSettings["TargetProtocol"].(string) + targetPort, _ := glbSettings["TargetPort"].(float64) + + cdnSettings := &godo.CDNSettings{} + if cdn, ok := glbSettings["CDN"].(map[string]any); ok { + if isEnabled, ok := cdn["IsEnabled"].(bool); ok { + cdnSettings.IsEnabled = isEnabled + } + } + + rp := make(map[string]uint32) + if regionPriorities, ok := glbSettings["RegionPriorities"].(map[string]any); ok { + for k, v := range regionPriorities { + if val, ok := v.(float64); ok { + rp[k] = uint32(val) + } + } + } + + failoverThreshold, _ := glbSettings["FailoverThreshold"].(float64) + + lbr.GLBSettings = &godo.GLBSettings{ + TargetProtocol: targetProtocol, + TargetPort: uint32(targetPort), + CDN: cdnSettings, + RegionPriorities: rp, + FailoverThreshold: uint32(failoverThreshold), + } + } + } else { + // Regional load balancer arguments + region, ok := args["Region"].(string) + if !ok || region == "" { + return mcp.NewToolResultError("Region is required for REGIONAL and REGIONAL_NETWORK load balancers"), nil + } + lbr.Region = region + + // Parse forwarding rules + forwardingRules := []godo.ForwardingRule{} + if rules, ok := args["ForwardingRules"]; ok && rules != nil { + var err *mcp.CallToolResult + forwardingRules, err = parseForwardingRules(rules.([]any)) + if err != nil { + return err, nil + } + } + lbr.ForwardingRules = forwardingRules + } + + // Target identifiers are optional but only one can be provided + tag, _ := args["Tag"].(string) + dropletIDs, _ := args["DropletIDs"].([]any) + if len(dropletIDs) > 0 && tag != "" { + return mcp.NewToolResultError("Only one target identifier (e.g. tag, droplets) can be specified"), nil + } + + // If droplet IDs are provided, make request with them + if len(dropletIDs) > 0 { + // Parse droplet IDs as ints + intDropletIDs := make([]int, len(dropletIDs)) + for i, id := range dropletIDs { + if did, ok := id.(float64); ok { + intDropletIDs[i] = int(did) + } + } + lbr.DropletIDs = intDropletIDs + } + // If tag is provided, make request with it + if tag != "" { + lbr.Tag = tag + } + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + lb, _, err := client.LoadBalancers.Update(ctx, lbID, lbr) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + jsonLB, err := json.MarshalIndent(lb, "", " ") + if err != nil { + return nil, fmt.Errorf("marshal error: %w", err) + } + return mcp.NewToolResultText(string(jsonLB)), nil +} + +func (l *LoadBalancersTool) addForwardingRules(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + lbID, ok := req.GetArguments()["LoadBalancerID"].(string) + if !ok || lbID == "" { + return mcp.NewToolResultError("Load Balancer ID is required"), nil + } + + // Parse forwarding rules + forwardingRules := []godo.ForwardingRule{} + if rules, ok := req.GetArguments()["ForwardingRules"]; ok && rules != nil { + var err *mcp.CallToolResult + forwardingRules, err = parseForwardingRules(rules.([]any)) + if err != nil { + return err, nil + } + } + if len(forwardingRules) == 0 { + return mcp.NewToolResultError("At least one forwarding rule must be provided"), nil + } + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + _, err = client.LoadBalancers.AddForwardingRules(ctx, lbID, forwardingRules...) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + + return mcp.NewToolResultText("Forwarding rules added successfully"), nil +} + +func (l *LoadBalancersTool) removeForwardingRules(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + lbID, ok := req.GetArguments()["LoadBalancerID"].(string) + if !ok || lbID == "" { + return mcp.NewToolResultError("Load Balancer ID is required"), nil + } + + // Parse forwarding rules + forwardingRules := []godo.ForwardingRule{} + if rules, ok := req.GetArguments()["ForwardingRules"]; ok && rules != nil { + var err *mcp.CallToolResult + forwardingRules, err = parseForwardingRules(rules.([]any)) + if err != nil { + return err, nil + } + } + if len(forwardingRules) == 0 { + return mcp.NewToolResultError("At least one forwarding rule must be provided"), nil + } + + client, err := l.client(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get DigitalOcean client: %w", err) + } + + _, err = client.LoadBalancers.RemoveForwardingRules(ctx, lbID, forwardingRules...) + if err != nil { + return mcp.NewToolResultErrorFromErr("api error", err), nil + } + + return mcp.NewToolResultText("Forwarding rules removed successfully"), nil +} + +func (l *LoadBalancersTool) Tools() []server.ServerTool { + return []server.ServerTool{ + { + Handler: l.createLoadBalancer, + Tool: mcp.NewTool("load-balancer-create", + mcp.WithDescription("Create a new Load Balancer"), + mcp.WithString("Name", mcp.Required(), mcp.Description("Name of the load balancer")), + mcp.WithString("Region", mcp.Description("Region slug (e.g., nyc3)")), + mcp.WithArray("DropletIDs", mcp.Description("IDs of the Droplets assigned to the load balancer")), + mcp.WithString("Tag", mcp.Description("Droplet tag corresponding to Droplets assigned to the load balancer")), + mcp.WithArray("ForwardingRules", mcp.Description("Forwarding rules for a load balancer")), + mcp.WithString("Type", mcp.Description("Type of the load balancer (REGIONAL, REGIONAL_NETWORK, GLOBAL)")), + mcp.WithString("Network", mcp.Description("Network type of the load balancer (EXTERNAL, INTERNAL)")), + mcp.WithNumber("SizeUnit", mcp.DefaultNumber(2), mcp.Description("Size of the load balancer in units appropriate to its type")), + mcp.WithString("NetworkStack", mcp.Description("Network stack of the load balancer (IPV4, DUALSTACK)")), + mcp.WithString("ProjectID", mcp.Description("Project ID to which the load balancer will be assigned")), + mcp.WithArray("TargetLoadBalancerIDs", mcp.Description("IDs of the target regional load balancers for a global load balancer")), + mcp.WithObject("GLBSettings", mcp.Description("Forward configurations for a global load balancer")), + ), + }, + { + Handler: l.deleteLoadBalancer, + Tool: mcp.NewTool("load-balancer-delete", + mcp.WithDestructiveHintAnnotation(true), + mcp.WithDescription("Delete a Load Balancer by ID"), + mcp.WithString("LoadBalancerID", mcp.Required(), mcp.Description("ID of the load balancer")), + ), + }, + { + Handler: l.deleteLoadBalancerCache, + Tool: mcp.NewTool("load-balancer-delete-cache", + mcp.WithDestructiveHintAnnotation(true), + mcp.WithDescription("Delete the CDN cache of a global load balancer by ID"), + mcp.WithString("LoadBalancerID", mcp.Required(), mcp.Description("ID of the load balancer")), + ), + }, + { + Handler: l.getLoadBalancer, + Tool: mcp.NewTool("load-balancer-get", + mcp.WithDescription("Get a Load Balancer by ID"), + mcp.WithString("LoadBalancerID", mcp.Required(), mcp.Description("ID of the load balancer")), + ), + }, + { + Handler: l.listLoadBalancers, + Tool: mcp.NewTool("load-balancer-list", + mcp.WithDescription("List Load Balancers with pagination"), + mcp.WithNumber("Page", mcp.DefaultNumber(1), mcp.Description("Page number")), + mcp.WithNumber("PerPage", mcp.DefaultNumber(20), mcp.Description("Items per page")), + ), + }, + { + Handler: l.addDroplets, + Tool: mcp.NewTool("load-balancer-add-droplets", + mcp.WithDescription("Add Droplets to a Load Balancer"), + mcp.WithString("LoadBalancerID", mcp.Required(), mcp.Description("ID of the load balancer")), + mcp.WithArray("DropletIDs", mcp.Required(), mcp.Description("IDs of the droplets to add")), + ), + }, + { + Handler: l.removeDroplets, + Tool: mcp.NewTool("load-balancer-remove-droplets", + mcp.WithDescription("Remove Droplets from a Load Balancer"), + mcp.WithString("LoadBalancerID", mcp.Required(), mcp.Description("ID of the load balancer")), + mcp.WithArray("DropletIDs", mcp.Required(), mcp.Description("IDs of the droplets to remove")), + ), + }, + { + Handler: l.updateLoadBalancer, + Tool: mcp.NewTool("load-balancer-update", + mcp.WithDescription("Update a Load Balancer"), + mcp.WithString("LoadBalancerID", mcp.Required(), mcp.Description("ID of the load balancer")), + mcp.WithString("Name", mcp.Required(), mcp.Description("Name of the load balancer")), + mcp.WithString("Region", mcp.Description("Region slug (e.g., nyc3)")), + mcp.WithArray("DropletIDs", mcp.Description("IDs of the Droplets assigned to the load balancer")), + mcp.WithString("Tag", mcp.Description("Droplet tag corresponding to Droplets assigned to the load balancer")), + mcp.WithArray("ForwardingRules", mcp.Description("Forwarding rules for a load balancer")), + mcp.WithString("Type", mcp.Required(), mcp.Description("Type of the load balancer (REGIONAL, REGIONAL_NETWORK, GLOBAL)")), + mcp.WithString("Network", mcp.Description("Network type of the load balancer (EXTERNAL, INTERNAL)")), + mcp.WithNumber("SizeUnit", mcp.DefaultNumber(2), mcp.Description("Size of the load balancer in units appropriate to its type")), + mcp.WithString("NetworkStack", mcp.Description("Network stack of the load balancer (IPV4, DUALSTACK)")), + mcp.WithString("ProjectID", mcp.Description("Project ID to which the load balancer will be assigned")), + mcp.WithArray("TargetLoadBalancerIDs", mcp.Description("IDs of the target regional load balancers for a global load balancer")), + mcp.WithObject("GLBSettings", mcp.Description("Forward configurations for a global load balancer")), + ), + }, + { + Handler: l.addForwardingRules, + Tool: mcp.NewTool("load-balancer-add-forwarding-rules", + mcp.WithDescription("Add Forwarding Rules to a Load Balancer"), + mcp.WithString("LoadBalancerID", mcp.Required(), mcp.Description("ID of the load balancer")), + mcp.WithArray("ForwardingRules", mcp.Required(), mcp.Description("Forwarding rules to add")), + ), + }, + { + Handler: l.removeForwardingRules, + Tool: mcp.NewTool("load-balancer-remove-forwarding-rules", + mcp.WithDescription("Remove Forwarding Rules from a Load Balancer"), + mcp.WithString("LoadBalancerID", mcp.Required(), mcp.Description("ID of the load balancer")), + mcp.WithArray("ForwardingRules", mcp.Required(), mcp.Description("Forwarding rules to remove")), + ), + }, + } +} diff --git a/pkg/registry/networking/load_balancers_tools_test.go b/pkg/registry/networking/load_balancers_tools_test.go new file mode 100644 index 0000000..6f60007 --- /dev/null +++ b/pkg/registry/networking/load_balancers_tools_test.go @@ -0,0 +1,1294 @@ +package networking + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/digitalocean/godo" + "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func setupLoadBalancersToolWithMock(loadBalancers *MockLoadBalancersService) *LoadBalancersTool { + client := func(ctx context.Context) (*godo.Client, error) { + return &godo.Client{LoadBalancers: loadBalancers}, nil + } + return NewLoadBalancersTool(client) +} + +func TestLoadBalancersTool_createLoadBalancer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testLoadBalancerWithDropletIDs := &godo.LoadBalancer{ + ID: "12345", + Region: &godo.Region{Slug: "nyc3"}, + DropletIDs: []int{111, 222}, + } + forwardingRulesArg := []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + map[string]any{ + "EntryProtocol": "https", + "EntryPort": float64(443), + "TargetProtocol": "https", + "TargetPort": float64(443), + "TlsPassthrough": true, + }, + } + mockForwardingRules := []godo.ForwardingRule{ + { + EntryProtocol: "http", + EntryPort: 80, + TargetProtocol: "http", + TargetPort: 80, + }, + { + EntryProtocol: "https", + EntryPort: 443, + TargetProtocol: "https", + TargetPort: 443, + TlsPassthrough: true, + }, + } + tests := []struct { + name string + args map[string]any + mockSetup func(m *MockLoadBalancersService) + expectError bool + expectText string + }{ + { + name: "Successful create with DropletIDs", + args: map[string]any{ + "Region": "nyc3", + "Name": "example-lb", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": forwardingRulesArg, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Create(gomock.Any(), &godo.LoadBalancerRequest{ + Region: "nyc3", + Name: "example-lb", + DropletIDs: []int{111, 222}, + ForwardingRules: mockForwardingRules, + }). + Return(testLoadBalancerWithDropletIDs, nil, nil). + Times(1) + }, + }, + { + name: "Successful create with Tag", + args: map[string]any{ + "Region": "nyc3", + "Name": "example-lb", + "Tag": "example-tag", + "ForwardingRules": forwardingRulesArg, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Create(gomock.Any(), &godo.LoadBalancerRequest{ + Region: "nyc3", + Name: "example-lb", + Tag: "example-tag", + ForwardingRules: mockForwardingRules, + }). + Return(&godo.LoadBalancer{ + ID: "12345", + Region: &godo.Region{Slug: "nyc3"}, + Tag: "example-tag", + }, nil, nil). + Times(1) + }, + }, + { + name: "Successful create with optional arguments provided", + args: map[string]any{ + "Region": "nyc3", + "Name": "example-lb", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": forwardingRulesArg, + "Type": "REGIONAL_NETWORK", + "Network": "INTERNAL", + "SizeUnit": float64(4), + "NetworkStack": "DUALSTACK", + "ProjectID": "example-project-id", + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Create(gomock.Any(), &godo.LoadBalancerRequest{ + Region: "nyc3", + Name: "example-lb", + DropletIDs: []int{111, 222}, + ForwardingRules: mockForwardingRules, + SizeUnit: 4, + Type: "REGIONAL_NETWORK", + Network: "INTERNAL", + NetworkStack: "DUALSTACK", + ProjectID: "example-project-id", + }). + Return(&godo.LoadBalancer{ + ID: "12345", + Region: &godo.Region{Slug: "nyc3"}, + DropletIDs: []int{111, 222}, + SizeUnit: 4, + Type: "REGIONAL_NETWORK", + Network: "INTERNAL", + NetworkStack: "DUALSTACK", + ProjectID: "example-project-id", + }, nil, nil). + Times(1) + }, + }, + { + name: "Successful create Global Load Balancer", + args: map[string]any{ + "Name": "example-global-lb", + "Tag": "example-tag", + "Type": "GLOBAL", + "GLBSettings": map[string]any{ + "TargetPort": float64(80), + "TargetProtocol": "http", + "RegionPriorities": map[string]any{"dev1": float64(1), "dev2": float64(2)}, + "FailoverThreshold": float64(10), + "CDN": map[string]any{ + "IsEnabled": true, + }, + }, + "TargetLoadBalancerIDs": []string{"target-lb-1", "target-lb-2"}, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Create(gomock.Any(), &godo.LoadBalancerRequest{ + Name: "example-global-lb", + Tag: "example-tag", + Type: "GLOBAL", + GLBSettings: &godo.GLBSettings{ + TargetPort: 80, + TargetProtocol: "http", + RegionPriorities: map[string]uint32{"dev1": 1, "dev2": 2}, + FailoverThreshold: 10, + CDN: &godo.CDNSettings{ + IsEnabled: true, + }, + }, + TargetLoadBalancerIDs: []string{"target-lb-1", "target-lb-2"}, + }). + Return(&godo.LoadBalancer{ + ID: "12345", + Name: "example-global-lb", + Type: "GLOBAL", + GLBSettings: &godo.GLBSettings{ + TargetPort: 80, + TargetProtocol: "http", + RegionPriorities: map[string]uint32{"dev1": 1, "dev2": 2}, + FailoverThreshold: 10, + CDN: &godo.CDNSettings{ + IsEnabled: true, + }, + }, + TargetLoadBalancerIDs: []string{"target-lb-1", "target-lb-2"}, + }, nil, nil). + Times(1) + }, + }, + { + name: "Missing Region argument", + args: map[string]any{ + "Name": "example-lb", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": forwardingRulesArg, + }, + mockSetup: nil, + expectError: true, + expectText: "Region is required for REGIONAL and REGIONAL_NETWORK load balancers", + }, + { + name: "Missing Name argument", + args: map[string]any{ + "Region": "nyc3", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": forwardingRulesArg, + }, + mockSetup: nil, + expectError: true, + expectText: "Name is required", + }, + { + name: "Both DropletIDs and Tag arguments provided", + args: map[string]any{ + "Region": "nyc3", + "Name": "example-lb", + "DropletIDs": []any{float64(111), float64(222)}, + "Tag": "web-servers", + "ForwardingRules": forwardingRulesArg, + }, + mockSetup: nil, + expectError: true, + expectText: "Only one target identifier (e.g. tag, droplets) can be specified", + }, + { + name: "Missing ForwardingRules argument", + args: map[string]any{ + "Region": "nyc3", + "Name": "example-lb", + "DropletIDs": []any{float64(111), float64(222)}, + }, + mockSetup: nil, + expectError: true, + expectText: "At least one forwarding rule must be provided", + }, + { + name: "API error", + args: map[string]any{ + "Region": "nyc3", + "Name": "example-lb", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": forwardingRulesArg, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Create(gomock.Any(), &godo.LoadBalancerRequest{ + Region: "nyc3", + Name: "example-lb", + DropletIDs: []int{111, 222}, + ForwardingRules: mockForwardingRules, + }). + Return(nil, nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: tc.args}} + resp, err := tool.createLoadBalancer(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + if tc.expectText != "" { + require.Contains(t, resp.Content[0].(mcp.TextContent).Text, tc.expectText) + } + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + var outLoadBalancer godo.LoadBalancer + require.NoError(t, json.Unmarshal([]byte(resp.Content[0].(mcp.TextContent).Text), &outLoadBalancer)) + require.Equal(t, testLoadBalancerWithDropletIDs.ID, outLoadBalancer.ID) + }) + } +} + +func TestLoadBalancersTool_deleteLoadBalancer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + args map[string]any + mockSetup func(m *MockLoadBalancersService) + expectError bool + expectText string + }{ + { + name: "Successful delete", + args: map[string]any{ + "LoadBalancerID": "12345", + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Delete(gomock.Any(), "12345"). + Return(nil, nil). + Times(1) + }, + expectText: "Load Balancer deleted successfully", + }, + { + name: "API error", + args: map[string]any{ + "LoadBalancerID": "12345", + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Delete(gomock.Any(), "12345"). + Return(nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: tc.args}} + resp, err := tool.deleteLoadBalancer(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + require.Equal(t, tc.expectText, resp.Content[0].(mcp.TextContent).Text) + }) + } +} + +func TestLoadBalancersTool_deleteLoadBalancerCache(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + args map[string]any + mockSetup func(m *MockLoadBalancersService) + expectError bool + expectText string + }{ + { + name: "Successful delete cache", + args: map[string]any{ + "LoadBalancerID": "12345", + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + PurgeCache(gomock.Any(), "12345"). + Return(nil, nil). + Times(1) + }, + expectText: "Load Balancer cache deleted successfully", + }, + { + name: "API error", + args: map[string]any{ + "LoadBalancerID": "12345", + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + PurgeCache(gomock.Any(), "12345"). + Return(nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: tc.args}} + resp, err := tool.deleteLoadBalancerCache(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + require.Equal(t, tc.expectText, resp.Content[0].(mcp.TextContent).Text) + }) + } +} + +func TestLoadBalancersTool_getLoadBalancer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testLoadBalancer := &godo.LoadBalancer{ + ID: "12345", + Region: &godo.Region{Slug: "nyc3"}, + DropletIDs: []int{111, 222}, + } + tests := []struct { + name string + lbID string + mockSetup func(m *MockLoadBalancersService) + expectError bool + }{ + { + name: "Successful get", + lbID: "12345", + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Get(gomock.Any(), "12345"). + Return(testLoadBalancer, nil, nil). + Times(1) + }, + }, + { + name: "API error", + lbID: "12345", + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Get(gomock.Any(), "12345"). + Return(nil, nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + { + name: "Missing ID argument", + lbID: "", + mockSetup: nil, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + args := map[string]any{} + if tc.name != "Missing ID argument" { + args["LoadBalancerID"] = tc.lbID + } + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: args}} + resp, err := tool.getLoadBalancer(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + var outLoadBalancer godo.LoadBalancer + require.NoError(t, json.Unmarshal([]byte(resp.Content[0].(mcp.TextContent).Text), &outLoadBalancer)) + require.Equal(t, testLoadBalancer.ID, outLoadBalancer.ID) + }) + } +} + +func TestLoadBalancersTool_listLoadBalancers(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testLoadBalancers := []godo.LoadBalancer{ + { + ID: "12345", + Region: &godo.Region{Slug: "nyc3"}, + DropletIDs: []int{111, 222}, + }, + { + ID: "67890", + Region: &godo.Region{Slug: "sfo2"}, + DropletIDs: []int{333, 444}, + }, + } + tests := []struct { + name string + page float64 + perPage float64 + mockSetup func(m *MockLoadBalancersService) + expectError bool + }{ + { + name: "Successful list", + page: 2, + perPage: 1, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + List(gomock.Any(), &godo.ListOptions{Page: 2, PerPage: 1}). + Return(testLoadBalancers, nil, nil). + Times(1) + }, + }, + { + name: "API error", + page: 2, + perPage: 1, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + List(gomock.Any(), &godo.ListOptions{Page: 2, PerPage: 1}). + Return(nil, nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + { + name: "Default pagination", + page: 0, + perPage: 0, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + List(gomock.Any(), &godo.ListOptions{Page: 1, PerPage: 20}). + Return(testLoadBalancers, nil, nil). + Times(1) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + args := map[string]any{} + if tc.page != 0 { + args["Page"] = tc.page + } + if tc.perPage != 0 { + args["PerPage"] = tc.perPage + } + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: args}} + resp, err := tool.listLoadBalancers(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + var outLoadBalancers []godo.LoadBalancer + require.NoError(t, json.Unmarshal([]byte(resp.Content[0].(mcp.TextContent).Text), &outLoadBalancers)) + require.GreaterOrEqual(t, len(testLoadBalancers), len(outLoadBalancers)) + }) + } +} + +func TestLoadBalancersTool_addDroplets(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + lbID string + dropletIDs []any + mockSetup func(m *MockLoadBalancersService) + expectError bool + expectText string + }{ + { + name: "Successful add droplets", + lbID: "12345", + dropletIDs: []any{float64(111), float64(222)}, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + AddDroplets(gomock.Any(), "12345", []int{111, 222}). + Return(nil, nil). + Times(1) + }, + expectText: "Droplets added successfully", + }, + { + name: "API error", + lbID: "12345", + dropletIDs: []any{float64(111), float64(222)}, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + AddDroplets(gomock.Any(), "12345", []int{111, 222}). + Return(nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + { + name: "Missing load balancer ID argument", + lbID: "", + dropletIDs: []any{float64(111), float64(222)}, + mockSetup: nil, + expectError: true, + }, + { + name: "Missing droplet IDs argument", + lbID: "12345", + dropletIDs: nil, + mockSetup: nil, + expectError: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + args := map[string]any{} + if tc.lbID != "" { + args["LoadBalancerID"] = tc.lbID + } + if tc.dropletIDs != nil { + args["DropletIDs"] = tc.dropletIDs + } + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: args}} + resp, err := tool.addDroplets(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + require.Equal(t, tc.expectText, resp.Content[0].(mcp.TextContent).Text) + }) + } +} + +func TestLoadBalancersTool_removeDroplets(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + lbID string + dropletIDs []any + mockSetup func(m *MockLoadBalancersService) + expectError bool + expectText string + }{ + { + name: "Successful remove droplets", + lbID: "12345", + dropletIDs: []any{float64(111), float64(222)}, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + RemoveDroplets(gomock.Any(), "12345", []int{111, 222}). + Return(nil, nil). + Times(1) + }, + expectText: "Droplets removed successfully", + }, + { + name: "API error", + lbID: "12345", + dropletIDs: []any{float64(111), float64(222)}, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + RemoveDroplets(gomock.Any(), "12345", []int{111, 222}). + Return(nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + { + name: "Missing load balancer ID argument", + lbID: "", + dropletIDs: []any{float64(111), float64(222)}, + mockSetup: nil, + expectError: true, + }, + { + name: "Missing droplet IDs argument", + lbID: "12345", + dropletIDs: nil, + mockSetup: nil, + expectError: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + args := map[string]any{} + if tc.lbID != "" { + args["LoadBalancerID"] = tc.lbID + } + if tc.dropletIDs != nil { + args["DropletIDs"] = tc.dropletIDs + } + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: args}} + resp, err := tool.removeDroplets(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + require.Equal(t, tc.expectText, resp.Content[0].(mcp.TextContent).Text) + }) + } +} + +func TestLoadBalancersTool_updateLoadBalancer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testLoadBalancer := &godo.LoadBalancer{ + ID: "12345", + Name: "example-lb-updated", + Region: &godo.Region{Slug: "nyc3"}, + DropletIDs: []int{111, 222}, + } + + tests := []struct { + name string + args map[string]any + mockSetup func(m *MockLoadBalancersService) + expectError bool + expectText string + }{ + { + name: "Successful update", + args: map[string]any{ + "LoadBalancerID": "12345", + "Name": "example-lb-updated", + "Type": "REGIONAL", + "Region": "nyc3", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Update(gomock.Any(), "12345", &godo.LoadBalancerRequest{ + Region: "nyc3", + Name: "example-lb-updated", + Type: "REGIONAL", + DropletIDs: []int{111, 222}, + ForwardingRules: []godo.ForwardingRule{ + { + EntryProtocol: "http", + EntryPort: 80, + TargetProtocol: "http", + TargetPort: 80, + }, + }, + }). + Return(testLoadBalancer, nil, nil). + Times(1) + }, + }, + { + name: "Successful update Global Load Balancer", + args: map[string]any{ + "LoadBalancerID": "12345", + "Name": "example-global-lb-updated", + "Type": "GLOBAL", + "Tag": "example-tag-updated", + "GLBSettings": map[string]any{ + "TargetPort": float64(20), + "TargetProtocol": "http", + "RegionPriorities": map[string]any{"dev1": float64(2), "dev2": float64(1)}, + "FailoverThreshold": float64(50), + "CDN": map[string]any{ + "IsEnabled": true, + }, + }, + "TargetLoadBalancerIDs": []string{"target-lb-3", "target-lb-4"}, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Update(gomock.Any(), "12345", &godo.LoadBalancerRequest{ + Name: "example-global-lb-updated", + Tag: "example-tag-updated", + Type: "GLOBAL", + GLBSettings: &godo.GLBSettings{ + TargetPort: 20, + TargetProtocol: "http", + RegionPriorities: map[string]uint32{"dev1": 2, "dev2": 1}, + FailoverThreshold: 50, + CDN: &godo.CDNSettings{ + IsEnabled: true, + }, + }, + TargetLoadBalancerIDs: []string{"target-lb-3", "target-lb-4"}, + }). + Return(&godo.LoadBalancer{ + ID: "12345", + Name: "example-global-lb-updated", + Type: "GLOBAL", + Tag: "example-tag-updated", + GLBSettings: &godo.GLBSettings{ + TargetPort: 20, + TargetProtocol: "http", + RegionPriorities: map[string]uint32{"dev1": 2, "dev2": 1}, + FailoverThreshold: 50, + CDN: &godo.CDNSettings{ + IsEnabled: true, + }, + }, + TargetLoadBalancerIDs: []string{"target-lb-3", "target-lb-4"}, + }, nil, nil). + Times(1) + }, + }, + { + name: "Successful update with optional arguments provided", + args: map[string]any{ + "LoadBalancerID": "12345", + "Name": "example-lb-updated", + "Type": "REGIONAL_NETWORK", + "Region": "nyc3", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + "Network": "INTERNAL", + "SizeUnit": float64(4), + "NetworkStack": "DUALSTACK", + "ProjectID": "example-project-id", + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Update(gomock.Any(), "12345", &godo.LoadBalancerRequest{ + Region: "nyc3", + Name: "example-lb-updated", + Type: "REGIONAL_NETWORK", + DropletIDs: []int{111, 222}, + ForwardingRules: []godo.ForwardingRule{ + { + EntryProtocol: "http", + EntryPort: 80, + TargetProtocol: "http", + TargetPort: 80, + }, + }, + SizeUnit: 4, + Network: "INTERNAL", + NetworkStack: "DUALSTACK", + ProjectID: "example-project-id", + }). + Return(&godo.LoadBalancer{ + ID: "12345", + Region: &godo.Region{Slug: "nyc3"}, + Name: "example-lb-updated", + Type: "REGIONAL_NETWORK", + DropletIDs: []int{111, 222}, + SizeUnit: 4, + Network: "INTERNAL", + NetworkStack: "DUALSTACK", + ProjectID: "example-project-id", + }, nil, nil). + Times(1) + }, + }, + { + name: "Missing LoadBalancerID argument", + args: map[string]any{ + "Region": "nyc3", + "Name": "example-lb", + "Type": "REGIONAL", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: nil, + expectError: true, + expectText: "Load Balancer ID is required", + }, + { + name: "Missing Name argument", + args: map[string]any{ + "LoadBalancerID": "12345", + "DropletIDs": []any{float64(111), float64(222)}, + "Region": "nyc3", + "Type": "REGIONAL", + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: nil, + expectError: true, + expectText: "Name is required", + }, + { + name: "Missing Type argument", + args: map[string]any{ + "LoadBalancerID": "12345", + "Name": "example-lb", + "Region": "nyc3", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: nil, + expectError: true, + expectText: "Type is required", + }, + { + name: "Missing Region argument for REGIONAL type", + args: map[string]any{ + "LoadBalancerID": "12345", + "Name": "example-lb", + "Type": "REGIONAL", + "DropletIDs": []any{float64(111), float64(222)}, + }, + mockSetup: nil, + expectError: true, + expectText: "Region is required for REGIONAL and REGIONAL_NETWORK load balancers", + }, + { + name: "Both DropletIDs and Tag arguments cannot be provided", + args: map[string]any{ + "LoadBalancerID": "12345", + "Region": "nyc3", + "Name": "example-lb", + "Type": "REGIONAL", + "DropletIDs": []any{float64(111), float64(222)}, + "Tag": "web-servers", + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: nil, + expectError: true, + expectText: "Only one target identifier (e.g. tag, droplets) can be specified", + }, + { + name: "API error", + args: map[string]any{ + "LoadBalancerID": "12345", + "Name": "example-lb", + "Region": "nyc3", + "Type": "REGIONAL", + "DropletIDs": []any{float64(111), float64(222)}, + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + Update(gomock.Any(), "12345", &godo.LoadBalancerRequest{ + Region: "nyc3", + Name: "example-lb", + Type: "REGIONAL", + DropletIDs: []int{111, 222}, + ForwardingRules: []godo.ForwardingRule{ + { + EntryProtocol: "http", + EntryPort: 80, + TargetProtocol: "http", + TargetPort: 80, + }, + }, + }). + Return(nil, nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: tc.args}} + resp, err := tool.updateLoadBalancer(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + if tc.expectText != "" { + require.Contains(t, resp.Content[0].(mcp.TextContent).Text, tc.expectText) + } + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + var outLoadBalancer godo.LoadBalancer + require.NoError(t, json.Unmarshal([]byte(resp.Content[0].(mcp.TextContent).Text), &outLoadBalancer)) + require.Equal(t, testLoadBalancer.ID, outLoadBalancer.ID) + require.Contains(t, resp.Content[0].(mcp.TextContent).Text, tc.expectText) + }) + } +} + +func TestLoadBalancersTool_addForwardingRules(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + args map[string]any + mockSetup func(m *MockLoadBalancersService) + expectError bool + expectText string + }{ + { + name: "Successful add forwarding rules", + args: map[string]any{ + "LoadBalancerID": "12345", + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + AddForwardingRules(gomock.Any(), "12345", []godo.ForwardingRule{ + { + EntryProtocol: "http", + EntryPort: 80, + TargetProtocol: "http", + TargetPort: 80, + }, + }). + Times(1) + }, + expectText: "Forwarding rules added successfully", + }, + { + name: "API error", + args: map[string]any{ + "LoadBalancerID": "12345", + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + AddForwardingRules(gomock.Any(), "12345", []godo.ForwardingRule{ + { + EntryProtocol: "http", + EntryPort: 80, + TargetProtocol: "http", + TargetPort: 80, + }, + }). + Return(nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + { + name: "Missing LoadBalancerID argument", + args: map[string]any{ + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: nil, + expectError: true, + }, + { + name: "Missing ForwardingRules argument", + args: map[string]any{ + "LoadBalancerID": "12345", + }, + mockSetup: nil, + expectError: true, + expectText: "Forwarding Rules are required", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: tc.args}} + resp, err := tool.addForwardingRules(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + require.Equal(t, tc.expectText, resp.Content[0].(mcp.TextContent).Text) + }) + } +} + +func TestLoadBalancersTool_removeForwardingRules(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + args map[string]any + mockSetup func(m *MockLoadBalancersService) + expectError bool + expectText string + }{ + { + name: "Successful remove forwarding rules", + args: map[string]any{ + "LoadBalancerID": "12345", + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + RemoveForwardingRules(gomock.Any(), "12345", []godo.ForwardingRule{ + { + EntryProtocol: "http", + EntryPort: 80, + TargetProtocol: "http", + TargetPort: 80, + }, + }). + Times(1) + }, + expectText: "Forwarding rules removed successfully", + }, + { + name: "API error", + args: map[string]any{ + "LoadBalancerID": "12345", + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": float64(80), + "TargetProtocol": "http", + "TargetPort": float64(80), + }, + }, + }, + mockSetup: func(m *MockLoadBalancersService) { + m.EXPECT(). + RemoveForwardingRules(gomock.Any(), "12345", []godo.ForwardingRule{ + { + EntryProtocol: "http", + EntryPort: 80, + TargetProtocol: "http", + TargetPort: 80, + }, + }). + Return(nil, errors.New("api error")). + Times(1) + }, + expectError: true, + }, + { + name: "Missing LoadBalancerID argument", + args: map[string]any{ + "ForwardingRules": []any{ + map[string]any{ + "EntryProtocol": "http", + "EntryPort": 80, + "TargetProtocol": "http", + "TargetPort": 80, + }, + }, + }, + mockSetup: nil, + expectError: true, + expectText: "LoadBalancerID is required", + }, + { + name: "Missing ForwardingRules argument", + args: map[string]any{ + "LoadBalancerID": "12345", + }, + mockSetup: nil, + expectError: true, + expectText: "At least one forwarding rule must be provided", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockLoadBalancers := NewMockLoadBalancersService(ctrl) + if tc.mockSetup != nil { + tc.mockSetup(mockLoadBalancers) + } + tool := setupLoadBalancersToolWithMock(mockLoadBalancers) + req := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: tc.args}} + resp, err := tool.removeForwardingRules(context.Background(), req) + if tc.expectError { + require.NotNil(t, resp) + require.True(t, resp.IsError) + return + } + require.NoError(t, err) + require.NotNil(t, resp) + require.False(t, resp.IsError) + require.Equal(t, tc.expectText, resp.Content[0].(mcp.TextContent).Text) + }) + } +} diff --git a/pkg/registry/networking/mocks.go b/pkg/registry/networking/mocks.go index 7c6a982..7cb4fb8 100644 --- a/pkg/registry/networking/mocks.go +++ b/pkg/registry/networking/mocks.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/digitalocean/godo (interfaces: CertificatesService,DomainsService,FirewallsService,PartnerAttachmentService,ReservedIPsService,ReservedIPV6sService,ReservedIPActionsService,ReservedIPV6ActionsService,VPCsService,BYOIPPrefixesService) +// Source: github.com/digitalocean/godo (interfaces: CertificatesService,DomainsService,FirewallsService,LoadBalancersService,PartnerAttachmentService,ReservedIPsService,ReservedIPV6sService,ReservedIPActionsService,ReservedIPV6ActionsService,VPCsService,BYOIPPrefixesService) // // Generated by this command: // -// mockgen -destination=./pkg/registry/networking/mocks.go -package networking github.com/digitalocean/godo CertificatesService,DomainsService,FirewallsService,PartnerAttachmentService,ReservedIPsService,ReservedIPV6sService,ReservedIPActionsService,ReservedIPV6ActionsService,VPCsService,BYOIPPrefixesService +// mockgen -destination=./mocks.go -package networking github.com/digitalocean/godo CertificatesService,DomainsService,FirewallsService,LoadBalancersService,PartnerAttachmentService,ReservedIPsService,ReservedIPV6sService,ReservedIPActionsService,ReservedIPV6ActionsService,VPCsService,BYOIPPrefixesService // // Package networking is a generated GoMock package. @@ -563,6 +563,236 @@ func (mr *MockFirewallsServiceMockRecorder) Update(arg0, arg1, arg2 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockFirewallsService)(nil).Update), arg0, arg1, arg2) } +// MockLoadBalancersService is a mock of LoadBalancersService interface. +type MockLoadBalancersService struct { + ctrl *gomock.Controller + recorder *MockLoadBalancersServiceMockRecorder + isgomock struct{} +} + +// MockLoadBalancersServiceMockRecorder is the mock recorder for MockLoadBalancersService. +type MockLoadBalancersServiceMockRecorder struct { + mock *MockLoadBalancersService +} + +// NewMockLoadBalancersService creates a new mock instance. +func NewMockLoadBalancersService(ctrl *gomock.Controller) *MockLoadBalancersService { + mock := &MockLoadBalancersService{ctrl: ctrl} + mock.recorder = &MockLoadBalancersServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLoadBalancersService) EXPECT() *MockLoadBalancersServiceMockRecorder { + return m.recorder +} + +// AddDroplets mocks base method. +func (m *MockLoadBalancersService) AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*godo.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, lbID} + for _, a := range dropletIDs { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AddDroplets", varargs...) + ret0, _ := ret[0].(*godo.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddDroplets indicates an expected call of AddDroplets. +func (mr *MockLoadBalancersServiceMockRecorder) AddDroplets(ctx, lbID any, dropletIDs ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, lbID}, dropletIDs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDroplets", reflect.TypeOf((*MockLoadBalancersService)(nil).AddDroplets), varargs...) +} + +// AddForwardingRules mocks base method. +func (m *MockLoadBalancersService) AddForwardingRules(ctx context.Context, lbID string, rules ...godo.ForwardingRule) (*godo.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, lbID} + for _, a := range rules { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AddForwardingRules", varargs...) + ret0, _ := ret[0].(*godo.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddForwardingRules indicates an expected call of AddForwardingRules. +func (mr *MockLoadBalancersServiceMockRecorder) AddForwardingRules(ctx, lbID any, rules ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, lbID}, rules...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddForwardingRules", reflect.TypeOf((*MockLoadBalancersService)(nil).AddForwardingRules), varargs...) +} + +// Create mocks base method. +func (m *MockLoadBalancersService) Create(arg0 context.Context, arg1 *godo.LoadBalancerRequest) (*godo.LoadBalancer, *godo.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*godo.LoadBalancer) + ret1, _ := ret[1].(*godo.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Create indicates an expected call of Create. +func (mr *MockLoadBalancersServiceMockRecorder) Create(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockLoadBalancersService)(nil).Create), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockLoadBalancersService) Delete(ctx context.Context, lbID string) (*godo.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, lbID) + ret0, _ := ret[0].(*godo.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockLoadBalancersServiceMockRecorder) Delete(ctx, lbID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockLoadBalancersService)(nil).Delete), ctx, lbID) +} + +// Get mocks base method. +func (m *MockLoadBalancersService) Get(arg0 context.Context, arg1 string) (*godo.LoadBalancer, *godo.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(*godo.LoadBalancer) + ret1, _ := ret[1].(*godo.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Get indicates an expected call of Get. +func (mr *MockLoadBalancersServiceMockRecorder) Get(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockLoadBalancersService)(nil).Get), arg0, arg1) +} + +// List mocks base method. +func (m *MockLoadBalancersService) List(arg0 context.Context, arg1 *godo.ListOptions) ([]godo.LoadBalancer, *godo.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", arg0, arg1) + ret0, _ := ret[0].([]godo.LoadBalancer) + ret1, _ := ret[1].(*godo.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// List indicates an expected call of List. +func (mr *MockLoadBalancersServiceMockRecorder) List(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockLoadBalancersService)(nil).List), arg0, arg1) +} + +// ListByNames mocks base method. +func (m *MockLoadBalancersService) ListByNames(arg0 context.Context, arg1 []string, arg2 *godo.ListOptions) ([]godo.LoadBalancer, *godo.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByNames", arg0, arg1, arg2) + ret0, _ := ret[0].([]godo.LoadBalancer) + ret1, _ := ret[1].(*godo.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ListByNames indicates an expected call of ListByNames. +func (mr *MockLoadBalancersServiceMockRecorder) ListByNames(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByNames", reflect.TypeOf((*MockLoadBalancersService)(nil).ListByNames), arg0, arg1, arg2) +} + +// ListByUUIDs mocks base method. +func (m *MockLoadBalancersService) ListByUUIDs(arg0 context.Context, arg1 []string, arg2 *godo.ListOptions) ([]godo.LoadBalancer, *godo.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByUUIDs", arg0, arg1, arg2) + ret0, _ := ret[0].([]godo.LoadBalancer) + ret1, _ := ret[1].(*godo.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ListByUUIDs indicates an expected call of ListByUUIDs. +func (mr *MockLoadBalancersServiceMockRecorder) ListByUUIDs(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByUUIDs", reflect.TypeOf((*MockLoadBalancersService)(nil).ListByUUIDs), arg0, arg1, arg2) +} + +// PurgeCache mocks base method. +func (m *MockLoadBalancersService) PurgeCache(ctx context.Context, lbID string) (*godo.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PurgeCache", ctx, lbID) + ret0, _ := ret[0].(*godo.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PurgeCache indicates an expected call of PurgeCache. +func (mr *MockLoadBalancersServiceMockRecorder) PurgeCache(ctx, lbID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PurgeCache", reflect.TypeOf((*MockLoadBalancersService)(nil).PurgeCache), ctx, lbID) +} + +// RemoveDroplets mocks base method. +func (m *MockLoadBalancersService) RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*godo.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, lbID} + for _, a := range dropletIDs { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveDroplets", varargs...) + ret0, _ := ret[0].(*godo.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveDroplets indicates an expected call of RemoveDroplets. +func (mr *MockLoadBalancersServiceMockRecorder) RemoveDroplets(ctx, lbID any, dropletIDs ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, lbID}, dropletIDs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveDroplets", reflect.TypeOf((*MockLoadBalancersService)(nil).RemoveDroplets), varargs...) +} + +// RemoveForwardingRules mocks base method. +func (m *MockLoadBalancersService) RemoveForwardingRules(ctx context.Context, lbID string, rules ...godo.ForwardingRule) (*godo.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, lbID} + for _, a := range rules { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemoveForwardingRules", varargs...) + ret0, _ := ret[0].(*godo.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveForwardingRules indicates an expected call of RemoveForwardingRules. +func (mr *MockLoadBalancersServiceMockRecorder) RemoveForwardingRules(ctx, lbID any, rules ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, lbID}, rules...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveForwardingRules", reflect.TypeOf((*MockLoadBalancersService)(nil).RemoveForwardingRules), varargs...) +} + +// Update mocks base method. +func (m *MockLoadBalancersService) Update(ctx context.Context, lbID string, lbr *godo.LoadBalancerRequest) (*godo.LoadBalancer, *godo.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, lbID, lbr) + ret0, _ := ret[0].(*godo.LoadBalancer) + ret1, _ := ret[1].(*godo.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Update indicates an expected call of Update. +func (mr *MockLoadBalancersServiceMockRecorder) Update(ctx, lbID, lbr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockLoadBalancersService)(nil).Update), ctx, lbID, lbr) +} + // MockPartnerAttachmentService is a mock of PartnerAttachmentService interface. type MockPartnerAttachmentService struct { ctrl *gomock.Controller diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index f75abbc..8b17ac1 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -69,6 +69,7 @@ func registerNetworkingTools(s *server.MCPServer, getClient getClientFn) error { s.AddTools(networking.NewCertificateTool(getClient).Tools()...) s.AddTools(networking.NewDomainsTool(getClient).Tools()...) s.AddTools(networking.NewFirewallTool(getClient).Tools()...) + s.AddTools(networking.NewLoadBalancersTool(getClient).Tools()...) s.AddTools(networking.NewReservedIPTool(getClient).Tools()...) s.AddTools(networking.NewBYOIPPrefixTool(getClient).Tools()...) // Partner attachments doesn't have much users so this has been disabled