From e4bc9e2233434a780d2092226c4fade91e45a0af Mon Sep 17 00:00:00 2001 From: Aditya Saha Date: Wed, 23 Oct 2024 18:05:51 -0400 Subject: [PATCH 1/4] Add load balancer monitoring endpoints (#745) --- monitoring.go | 196 ++++++++++++++++++++++++++++++++++++++++++- monitoring_test.go | 204 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 397 insertions(+), 3 deletions(-) diff --git a/monitoring.go b/monitoring.go index 937bb8d9..00feb256 100644 --- a/monitoring.go +++ b/monitoring.go @@ -10,9 +10,10 @@ import ( ) const ( - monitoringBasePath = "v2/monitoring" - alertPolicyBasePath = monitoringBasePath + "/alerts" - dropletMetricsBasePath = monitoringBasePath + "/metrics/droplet" + monitoringBasePath = "v2/monitoring" + alertPolicyBasePath = monitoringBasePath + "/alerts" + dropletMetricsBasePath = monitoringBasePath + "/metrics/droplet" + loadBalancerMetricsBasePath = monitoringBasePath + "/metrics/load_balancer" DropletCPUUtilizationPercent = "v1/insights/droplet/cpu" DropletMemoryUtilizationPercent = "v1/insights/droplet/memory_utilization_percent" @@ -67,6 +68,34 @@ type MonitoringService interface { GetDropletCachedMemory(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error) GetDropletFreeMemory(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error) GetDropletTotalMemory(context.Context, *DropletMetricsRequest) (*MetricsResponse, *Response, error) + + GetLoadBalancerFrontendHttpRequestsPerSecond(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendConnectionsCurrent(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendConnectionsLimit(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendCpuUtilization(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendNetworkThroughputHttp(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendNetworkThroughputUdp(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendNetworkThroughputTcp(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendNlbTcpNetworkThroughput(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendNlbUdpNetworkThroughput(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendFirewallDroppedBytes(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendFirewallDroppedPackets(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendHttpResponses(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendTlsConnectionsCurrent(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendTlsConnectionsLimit(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerFrontendTlsConnectionsExceedingRateLimit(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHttpSessionDurationAvg(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHttpSessionDuration50P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHttpSessionDuration95P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHttpResponseTimeAvg(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHttpResponseTime50P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHttpResponseTime95P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHttpResponseTime99P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsQueueSize(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHttpResponses(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsConnections(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsHealthChecks(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + GetLoadBalancerDropletsDowntime(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) } // MonitoringServiceOp handles communication with monitoring related methods of the @@ -163,6 +192,13 @@ type DropletBandwidthMetricsRequest struct { Direction string } +// LoadBalancerMetricsRequest holds the information needed to retrieve Load Balancer various metrics. +type LoadBalancerMetricsRequest struct { + LoadBalancerID string + Start time.Time + End time.Time +} + // MetricsResponse holds a Metrics query response. type MetricsResponse struct { Status string `json:"status"` @@ -372,3 +408,157 @@ func (s *MonitoringServiceOp) getDropletMetrics(ctx context.Context, path string return root, resp, err } + +// GetLoadBalancerFrontendHttpRequestsPerSecond retrieves frontend HTTP requests per second for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendHttpRequestsPerSecond(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_http_requests_per_second", args) +} + +// GetLoadBalancerFrontendConnectionsCurrent retrieves frontend total current active connections for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendConnectionsCurrent(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_connections_current", args) +} + +// GetLoadBalancerFrontendConnectionsLimit retrieves frontend max connections limit for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendConnectionsLimit(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_connections_limit", args) +} + +// GetLoadBalancerFrontendCpuUtilization retrieves frontend average percentage cpu utilization for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendCpuUtilization(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_cpu_utilization", args) +} + +// GetLoadBalancerFrontendNetworkThroughputHttp retrieves frontend HTTP throughput for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendNetworkThroughputHttp(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_network_throughput_http", args) +} + +// GetLoadBalancerFrontendNetworkThroughputUdp retrieves frontend UDP throughput for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendNetworkThroughputUdp(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_network_throughput_udp", args) +} + +// GetLoadBalancerFrontendNetworkThroughputTcp retrieves frontend TCP throughput for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendNetworkThroughputTcp(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_network_throughput_tcp", args) +} + +// GetLoadBalancerFrontendNlbTcpNetworkThroughput retrieves frontend TCP throughput for a given network load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendNlbTcpNetworkThroughput(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_nlb_tcp_network_throughput", args) +} + +// GetLoadBalancerFrontendNlbUdpNetworkThroughput retrieves frontend UDP throughput for a given network load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendNlbUdpNetworkThroughput(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_nlb_udp_network_throughput", args) +} + +// GetLoadBalancerFrontendFirewallDroppedBytes retrieves firewall dropped bytes for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendFirewallDroppedBytes(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_firewall_dropped_bytes", args) +} + +// GetLoadBalancerFrontendFirewallDroppedPackets retrieves firewall dropped packets for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendFirewallDroppedPackets(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_firewall_dropped_packets", args) +} + +// GetLoadBalancerFrontendHttpResponses retrieves frontend HTTP rate of response code for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendHttpResponses(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_http_responses", args) +} + +// GetLoadBalancerFrontendTlsConnectionsCurrent retrieves frontend current TLS connections rate for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendTlsConnectionsCurrent(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_tls_connections_current", args) +} + +// GetLoadBalancerFrontendTlsConnectionsLimit retrieves frontend max TLS connections limit for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendTlsConnectionsLimit(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_tls_connections_limit", args) +} + +// GetLoadBalancerFrontendTlsConnectionsExceedingRateLimit retrieves frontend closed TLS connections for exceeded rate limit for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerFrontendTlsConnectionsExceedingRateLimit(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/frontend_tls_connections_exceeding_rate_limit", args) +} + +// GetLoadBalancerDropletsHttpSessionDurationAvg retrieves droplet average HTTP session duration for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHttpSessionDurationAvg(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_http_session_duration_avg", args) +} + +// GetLoadBalancerDropletsHttpSessionDuration50P retrieves droplet 50th percentile HTTP session duration for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHttpSessionDuration50P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_http_session_duration_50p", args) +} + +// GetLoadBalancerDropletsHttpSessionDuration95P retrieves droplet 95th percentile HTTP session duration for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHttpSessionDuration95P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_http_session_duration_95p", args) +} + +// GetLoadBalancerDropletsHttpResponseTimeAvg retrieves droplet average HTTP response time for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHttpResponseTimeAvg(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_http_response_time_avg", args) +} + +// GetLoadBalancerDropletsHttpResponseTime50P retrieves droplet 50th percentile HTTP response time for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHttpResponseTime50P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_http_response_time_50p", args) +} + +// GetLoadBalancerDropletsHttpResponseTime95P retrieves droplet 95th percentile HTTP response time for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHttpResponseTime95P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_http_response_time_95p", args) +} + +// GetLoadBalancerDropletsHttpResponseTime99P retrieves droplet 99th percentile HTTP response time for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHttpResponseTime99P(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_http_response_time_99p", args) +} + +// GetLoadBalancerDropletsQueueSize retrieves droplet queue size for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsQueueSize(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_queue_size", args) +} + +// GetLoadBalancerDropletsHttpResponses retrieves droplet HTTP rate of response code for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHttpResponses(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_http_responses", args) +} + +// GetLoadBalancerDropletsConnections retrieves droplet active connections for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsConnections(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_connections", args) +} + +// GetLoadBalancerDropletsHealthChecks retrieves droplet health check status for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsHealthChecks(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_health_checks", args) +} + +// GetLoadBalancerDropletsDowntime retrieves droplet downtime status for a given load balancer. +func (s *MonitoringServiceOp) GetLoadBalancerDropletsDowntime(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + return s.getLoadBalancerMetrics(ctx, "/droplets_downtime", args) +} + +func (s *MonitoringServiceOp) getLoadBalancerMetrics(ctx context.Context, path string, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) { + fullPath := loadBalancerMetricsBasePath + path + req, err := s.client.NewRequest(ctx, http.MethodGet, fullPath, nil) + if err != nil { + return nil, nil, err + } + + q := req.URL.Query() + q.Add("lb_id", args.LoadBalancerID) + q.Add("start", fmt.Sprintf("%d", args.Start.Unix())) + q.Add("end", fmt.Sprintf("%d", args.End.Unix())) + req.URL.RawQuery = q.Encode() + + root := new(MetricsResponse) + resp, err := s.client.Do(ctx, req, root) + + return root, resp, err +} diff --git a/monitoring_test.go b/monitoring_test.go index 408fb6b6..83dde8da 100644 --- a/monitoring_test.go +++ b/monitoring_test.go @@ -1,6 +1,7 @@ package godo import ( + "context" "encoding/json" "fmt" "net/http" @@ -725,6 +726,89 @@ var ( }, }, } + + testLBResponseJSON = ` + { + "status": "success", + "data": { + "resultType": "matrix", + "result": [ + { + "metric": { + "lb_id": "d699d327-5ad6-4894-8172-e8105f711cd4", + "region": "s2r1" + }, + "values": [ + [ + 1729453800, + "1.5082956259357405" + ], + [ + 1729453920, + "1.4755197853656865" + ], + [ + 1729454040, + "1.2758099714636817" + ], + [ + 1729454160, + "1.4922870556695833" + ], + [ + 1729454280, + "1.509813789633041" + ], + [ + 1729454400, + "1.3252809931070697" + ] + ] + } + ] + } + }` + + testLBResponse = &MetricsResponse{ + Status: "success", + Data: MetricsData{ + ResultType: "matrix", + Result: []metrics.SampleStream{ + { + Metric: metrics.Metric{ + "lb_id": "d699d327-5ad6-4894-8172-e8105f711cd4", + "region": "s2r1", + }, + Values: []metrics.SamplePair{ + { + Timestamp: 1729453800000, + Value: 1.5082956259357405, + }, + { + Timestamp: 1729453920000, + Value: 1.4755197853656865, + }, + { + Timestamp: 1729454040000, + Value: 1.2758099714636817, + }, + { + Timestamp: 1729454160000, + Value: 1.4922870556695833, + }, + { + Timestamp: 1729454280000, + Value: 1.509813789633041, + }, + { + Timestamp: 1729454400000, + Value: 1.3252809931070697, + }, + }, + }, + }, + }, + } ) func TestAlertPolicies_List(t *testing.T) { @@ -1305,3 +1389,123 @@ func TestGetDropletCPU(t *testing.T) { assert.Equal(t, testCPUResponse, metricsResp) } + +func TestGetLoadBalancerMetrics(t *testing.T) { + setup() + defer teardown() + + for _, tc := range []struct { + testFunc func(ctx context.Context, args *LoadBalancerMetricsRequest) (*MetricsResponse, *Response, error) + path string + }{ + { + client.Monitoring.GetLoadBalancerFrontendHttpRequestsPerSecond, + "/frontend_http_requests_per_second"}, + { + client.Monitoring.GetLoadBalancerFrontendConnectionsCurrent, + "/frontend_connections_current"}, + { + client.Monitoring.GetLoadBalancerFrontendConnectionsLimit, + "/frontend_connections_limit"}, + { + client.Monitoring.GetLoadBalancerFrontendCpuUtilization, + "/frontend_cpu_utilization"}, + { + client.Monitoring.GetLoadBalancerFrontendNetworkThroughputHttp, + "/frontend_network_throughput_http"}, + { + client.Monitoring.GetLoadBalancerFrontendNetworkThroughputUdp, + "/frontend_network_throughput_udp"}, + { + client.Monitoring.GetLoadBalancerFrontendNetworkThroughputTcp, + "/frontend_network_throughput_tcp"}, + { + client.Monitoring.GetLoadBalancerFrontendNlbTcpNetworkThroughput, + "/frontend_nlb_tcp_network_throughput"}, + { + client.Monitoring.GetLoadBalancerFrontendNlbUdpNetworkThroughput, + "/frontend_nlb_udp_network_throughput"}, + { + client.Monitoring.GetLoadBalancerFrontendFirewallDroppedBytes, + "/frontend_firewall_dropped_bytes"}, + { + client.Monitoring.GetLoadBalancerFrontendFirewallDroppedPackets, + "/frontend_firewall_dropped_packets"}, + { + client.Monitoring.GetLoadBalancerFrontendHttpResponses, + "/frontend_http_responses"}, + { + client.Monitoring.GetLoadBalancerFrontendTlsConnectionsCurrent, + "/frontend_tls_connections_current"}, + { + client.Monitoring.GetLoadBalancerFrontendTlsConnectionsLimit, + "/frontend_tls_connections_limit"}, + { + client.Monitoring.GetLoadBalancerFrontendTlsConnectionsExceedingRateLimit, + "/frontend_tls_connections_exceeding_rate_limit"}, + { + client.Monitoring.GetLoadBalancerDropletsHttpSessionDurationAvg, + "/droplets_http_session_duration_avg"}, + { + client.Monitoring.GetLoadBalancerDropletsHttpSessionDuration50P, + "/droplets_http_session_duration_50p"}, + { + client.Monitoring.GetLoadBalancerDropletsHttpSessionDuration95P, + "/droplets_http_session_duration_95p"}, + { + client.Monitoring.GetLoadBalancerDropletsHttpResponseTimeAvg, + "/droplets_http_response_time_avg"}, + { + client.Monitoring.GetLoadBalancerDropletsHttpResponseTime50P, + "/droplets_http_response_time_50p"}, + { + client.Monitoring.GetLoadBalancerDropletsHttpResponseTime95P, + "/droplets_http_response_time_95p"}, + { + client.Monitoring.GetLoadBalancerDropletsHttpResponseTime99P, + "/droplets_http_response_time_99p"}, + { + client.Monitoring.GetLoadBalancerDropletsQueueSize, + "/droplets_queue_size"}, + { + client.Monitoring.GetLoadBalancerDropletsHttpResponses, + "/droplets_http_responses"}, + { + client.Monitoring.GetLoadBalancerDropletsConnections, + "/droplets_connections"}, + { + client.Monitoring.GetLoadBalancerDropletsHealthChecks, + "/droplets_health_checks"}, + { + client.Monitoring.GetLoadBalancerDropletsDowntime, + "/droplets_downtime", + }, + } { + now := time.Now() + metricReq := &LoadBalancerMetricsRequest{ + LoadBalancerID: "123", + Start: now.Add(-300 * time.Second), + End: now, + } + + mux.HandleFunc("/v2/monitoring/metrics/load_balancer"+tc.path, func(w http.ResponseWriter, r *http.Request) { + lbID := r.URL.Query().Get("lb_id") + start := r.URL.Query().Get("start") + end := r.URL.Query().Get("end") + + assert.Equal(t, metricReq.LoadBalancerID, lbID) + assert.Equal(t, fmt.Sprintf("%d", metricReq.Start.Unix()), start) + assert.Equal(t, fmt.Sprintf("%d", metricReq.End.Unix()), end) + testMethod(t, r, http.MethodGet) + + fmt.Fprintf(w, testLBResponseJSON) + }) + + metricsResp, _, err := tc.testFunc(ctx, metricReq) + if err != nil { + t.Errorf("Monitoring.%v returned error: %v", tc.path, err) + } + + assert.Equal(t, testLBResponse, metricsResp) + } +} From 2cdd0a2ab99e33e2f60bdd5505ba3d48e3be78c4 Mon Sep 17 00:00:00 2001 From: Blesswin Samuel Date: Thu, 24 Oct 2024 20:04:31 +0530 Subject: [PATCH 2/4] Add archive field to AppSpec to archive/restore apps (#746) * Add archive field to AppSpec to archive/restore apps * Add note about closed beta --- apps.gen.go | 3 +++ apps_accessors.go | 8 ++++++++ apps_accessors_test.go | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/apps.gen.go b/apps.gen.go index bf03259c..0232ae94 100644 --- a/apps.gen.go +++ b/apps.gen.go @@ -466,6 +466,9 @@ type AppLogDestinationSpecPapertrail struct { type AppMaintenanceSpec struct { // Indicates whether maintenance mode should be enabled for the app. Enabled bool `json:"enabled,omitempty"` + // Indicates whether the app should be archived. Setting this to true implies that enabled is set to true. + // Note that this feature is currently in closed beta. + Archive bool `json:"archive,omitempty"` } // AppRouteSpec struct for AppRouteSpec diff --git a/apps_accessors.go b/apps_accessors.go index 0293734e..02e7c4b1 100644 --- a/apps_accessors.go +++ b/apps_accessors.go @@ -1421,6 +1421,14 @@ func (a *AppLogDestinationSpecPapertrail) GetEndpoint() string { return a.Endpoint } +// GetArchive returns the Archive field. +func (a *AppMaintenanceSpec) GetArchive() bool { + if a == nil { + return false + } + return a.Archive +} + // GetEnabled returns the Enabled field. func (a *AppMaintenanceSpec) GetEnabled() bool { if a == nil { diff --git a/apps_accessors_test.go b/apps_accessors_test.go index 9138c5a7..418f4330 100644 --- a/apps_accessors_test.go +++ b/apps_accessors_test.go @@ -1245,6 +1245,13 @@ func TestAppLogDestinationSpecPapertrail_GetEndpoint(tt *testing.T) { a.GetEndpoint() } +func TestAppMaintenanceSpec_GetArchive(tt *testing.T) { + a := &AppMaintenanceSpec{} + a.GetArchive() + a = nil + a.GetArchive() +} + func TestAppMaintenanceSpec_GetEnabled(tt *testing.T) { a := &AppMaintenanceSpec{} a.GetEnabled() From 1c20d59cb920e7b1b0ba0c88bba60ddf648a6ef1 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Thu, 24 Oct 2024 11:28:49 -0400 Subject: [PATCH 3/4] Prep v1.128.0 release. (#747) --- CHANGELOG.md | 9 +++++++++ godo.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69fb86d8..667255b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## [v1.128.0] - 2024-10-24 + +- #746 - @blesswinsamuel - Add archive field to AppSpec to archive/restore apps +- #745 - @asaha2 - Add load balancer monitoring endpoints +- #744 - @asaha2 - Adjust delete dangerous +- #743 - @asaha2 - Introduce droplet autoscale godo methods +- #740 - @blesswinsamuel - Add maintenance field to AppSpec to enable/disable maintenance mode +- #739 - @markusthoemmes - Add protocol to AppSpec and pending to detect responses + ## [v1.127.0] - 2024-10-18 - #737 - @loosla - [databases]: change Opensearch ism_history_max_docs type to int64 to … diff --git a/godo.go b/godo.go index 995469e9..edf0f6d4 100644 --- a/godo.go +++ b/godo.go @@ -21,7 +21,7 @@ import ( ) const ( - libraryVersion = "1.127.0" + libraryVersion = "1.128.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" From 2654a9d1e887e754f7d3c91f92c6987f124f6df2 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Fri, 25 Oct 2024 10:50:08 -0400 Subject: [PATCH 4/4] Support Droplet GPU information (#748) * sizes: support disk_info * sizes: support gpu_info * droplets: support listing GPU Droplets. --- droplets.go | 12 +++++ droplets_test.go | 92 ++++++++++++++++++++++++++++++++++++ sizes.go | 48 +++++++++++++++---- sizes_test.go | 119 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 257 insertions(+), 14 deletions(-) diff --git a/droplets.go b/droplets.go index 5f198636..1ed09ec8 100644 --- a/droplets.go +++ b/droplets.go @@ -17,6 +17,7 @@ var errNoNetworks = errors.New("no networks have been defined") // See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Droplets type DropletsService interface { List(context.Context, *ListOptions) ([]Droplet, *Response, error) + ListWithGPUs(context.Context, *ListOptions) ([]Droplet, *Response, error) ListByName(context.Context, string, *ListOptions) ([]Droplet, *Response, error) ListByTag(context.Context, string, *ListOptions) ([]Droplet, *Response, error) Get(context.Context, int) (*Droplet, *Response, error) @@ -321,6 +322,17 @@ func (s *DropletsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Dropl return s.list(ctx, path) } +// ListWithGPUs lists all Droplets with GPUs. +func (s *DropletsServiceOp) ListWithGPUs(ctx context.Context, opt *ListOptions) ([]Droplet, *Response, error) { + path := fmt.Sprintf("%s?type=gpus", dropletBasePath) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + return s.list(ctx, path) +} + // ListByName lists all Droplets filtered by name returning only exact matches. // It is case-insensitive func (s *DropletsServiceOp) ListByName(ctx context.Context, name string, opt *ListOptions) ([]Droplet, *Response, error) { diff --git a/droplets_test.go b/droplets_test.go index 17a62d75..ddd23f9e 100644 --- a/droplets_test.go +++ b/droplets_test.go @@ -44,6 +44,98 @@ func TestDroplets_ListDroplets(t *testing.T) { } } +func TestDroplets_ListDropletsWithGPUs(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + if r.URL.Query().Get("type") != "gpus" { + t.Errorf("Droplets.ListWithGPUs did not request with a type parameter") + } + fmt.Fprint(w, `{ + "droplets": [ + { + "id": 1, + "size": { + "gpu_info": { + "count": 1, + "vram": { + "amount": 8, + "unit": "gib" + }, + "model": "nvidia_tesla_v100" + }, + "disk_info": [ + { + "type": "local", + "size": { + "amount": 200, + "unit": "gib" + } + }, + { + "type": "scratch", + "size": { + "amount": 40960, + "unit": "gib" + } + } + ] + } + } + ], + "meta": { + "total": 1 + } + }`) + }) + + droplets, resp, err := client.Droplets.ListWithGPUs(ctx, nil) + if err != nil { + t.Errorf("Droplets.List returned error: %v", err) + } + + expectedDroplets := []Droplet{ + { + ID: 1, + Size: &Size{ + GPUInfo: &GPUInfo{ + Count: 1, + VRAM: &VRAM{ + Amount: 8, + Unit: "gib", + }, + Model: "nvidia_tesla_v100", + }, + DiskInfo: []DiskInfo{ + { + Type: "local", + Size: &DiskSize{ + Amount: 200, + Unit: "gib", + }, + }, + { + Type: "scratch", + Size: &DiskSize{ + Amount: 40960, + Unit: "gib", + }, + }, + }, + }, + }, + } + if !reflect.DeepEqual(droplets, expectedDroplets) { + t.Errorf("Droplets.List\nDroplets: got=%#v\nwant=%#v", droplets, expectedDroplets) + } + expectedMeta := &Meta{Total: 1} + if !reflect.DeepEqual(resp.Meta, expectedMeta) { + t.Errorf("Droplets.List\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) + } +} + func TestDroplets_ListDropletsByTag(t *testing.T) { setup() defer teardown() diff --git a/sizes.go b/sizes.go index a3cb7452..72d5321c 100644 --- a/sizes.go +++ b/sizes.go @@ -22,16 +22,44 @@ var _ SizesService = &SizesServiceOp{} // Size represents a DigitalOcean Size type Size struct { - Slug string `json:"slug,omitempty"` - Memory int `json:"memory,omitempty"` - Vcpus int `json:"vcpus,omitempty"` - Disk int `json:"disk,omitempty"` - PriceMonthly float64 `json:"price_monthly,omitempty"` - PriceHourly float64 `json:"price_hourly,omitempty"` - Regions []string `json:"regions,omitempty"` - Available bool `json:"available,omitempty"` - Transfer float64 `json:"transfer,omitempty"` - Description string `json:"description,omitempty"` + Slug string `json:"slug,omitempty"` + Memory int `json:"memory,omitempty"` + Vcpus int `json:"vcpus,omitempty"` + Disk int `json:"disk,omitempty"` + PriceMonthly float64 `json:"price_monthly,omitempty"` + PriceHourly float64 `json:"price_hourly,omitempty"` + Regions []string `json:"regions,omitempty"` + Available bool `json:"available,omitempty"` + Transfer float64 `json:"transfer,omitempty"` + Description string `json:"description,omitempty"` + GPUInfo *GPUInfo `json:"gpu_info,omitempty"` + DiskInfo []DiskInfo `json:"disk_info,omitempty"` +} + +// DiskInfo containing information about the disks available to Droplets created +// with this size. +type DiskInfo struct { + Type string `json:"type,omitempty"` + Size *DiskSize `json:"size,omitempty"` +} + +// DiskSize provides information about the size of a disk. +type DiskSize struct { + Amount int `json:"amount,omitempty"` + Unit string `json:"unit,omitempty"` +} + +// GPUInfo provides information about the GPU available to Droplets created with this size. +type GPUInfo struct { + Count int `json:"count,omitempty"` + VRAM *VRAM `json:"vram,omitempty"` + Model string `json:"model,omitempty"` +} + +// VRAM provides information about the amount of VRAM available to the GPU. +type VRAM struct { + Amount int `json:"amount,omitempty"` + Unit string `json:"unit,omitempty"` } func (s Size) String() string { diff --git a/sizes_test.go b/sizes_test.go index 784a88f6..145ea16c 100644 --- a/sizes_test.go +++ b/sizes_test.go @@ -23,6 +23,15 @@ func TestSizes_List(t *testing.T) { Available: true, Transfer: 1, Description: "Basic", + DiskInfo: []DiskInfo{ + { + Type: "local", + Size: &DiskSize{ + Amount: 25, + Unit: "gib", + }, + }, + }, }, { Slug: "512mb", @@ -35,6 +44,51 @@ func TestSizes_List(t *testing.T) { Available: true, Transfer: 1, Description: "Legacy Basic", + DiskInfo: []DiskInfo{ + { + Type: "local", + Size: &DiskSize{ + Amount: 20, + Unit: "gib", + }, + }, + }, + }, + { + Slug: "gpu-h100x8-640gb-200", + Memory: 1966080, + Vcpus: 160, + Disk: 200, + PriceMonthly: 35414.4, + PriceHourly: 52.7, + Regions: []string{"tor1"}, + Available: true, + Transfer: 60, + Description: "H100 GPU - 8X (small disk)", + GPUInfo: &GPUInfo{ + Count: 8, + VRAM: &VRAM{ + Amount: 640, + Unit: "gib", + }, + Model: "nvidia_h100", + }, + DiskInfo: []DiskInfo{ + { + Type: "local", + Size: &DiskSize{ + Amount: 200, + Unit: "gib", + }, + }, + { + Type: "scratch", + Size: &DiskSize{ + Amount: 40960, + Unit: "gib", + }, + }, + }, }, } @@ -55,7 +109,16 @@ func TestSizes_List(t *testing.T) { "nyc2" ], "available": true, - "description": "Basic" + "description": "Basic", + "disk_info": [ + { + "type": "local", + "size": { + "amount": 25, + "unit": "gib" + } + } + ] }, { "slug": "512mb", @@ -70,11 +133,59 @@ func TestSizes_List(t *testing.T) { "nyc2" ], "available": true, - "description": "Legacy Basic" + "description": "Legacy Basic", + "disk_info": [ + { + "type": "local", + "size": { + "amount": 20, + "unit": "gib" + } + } + ] + }, + { + "slug": "gpu-h100x8-640gb-200", + "memory": 1966080, + "vcpus": 160, + "disk": 200, + "transfer": 60, + "price_monthly": 35414.4, + "price_hourly": 52.7, + "regions": [ + "tor1" + ], + "available": true, + "description": "H100 GPU - 8X (small disk)", + "networking_throughput": 10000, + "gpu_info": { + "count": 8, + "vram": { + "amount": 640, + "unit": "gib" + }, + "model": "nvidia_h100" + }, + "disk_info": [ + { + "type": "local", + "size": { + "amount": 200, + "unit": "gib" + } + }, + { + "type": "scratch", + "size": { + "amount": 40960, + "unit": "gib" + } + } + ] } ], "meta": { - "total": 2 + "total": 3 } }`) }) @@ -88,7 +199,7 @@ func TestSizes_List(t *testing.T) { t.Errorf("Sizes.List returned sizes %+v, expected %+v", sizes, expectedSizes) } - expectedMeta := &Meta{Total: 2} + expectedMeta := &Meta{Total: 3} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Sizes.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) }