From 5b0efa1a3b180f3f827d35051b3ad5698e2f2f01 Mon Sep 17 00:00:00 2001 From: Aditya Saha Date: Tue, 22 Oct 2024 17:03:17 -0400 Subject: [PATCH] Introduce droplet autoscale godo methods (#743) * Introduce droplet autoscale godo methods * Adjust variable names * Adjust public api model --- droplet_autoscale.go | 258 ++++++++++++++++ droplet_autoscale_test.go | 617 ++++++++++++++++++++++++++++++++++++++ godo.go | 2 + 3 files changed, 877 insertions(+) create mode 100644 droplet_autoscale.go create mode 100644 droplet_autoscale_test.go diff --git a/droplet_autoscale.go b/droplet_autoscale.go new file mode 100644 index 00000000..f9483882 --- /dev/null +++ b/droplet_autoscale.go @@ -0,0 +1,258 @@ +package godo + +import ( + "context" + "fmt" + "net/http" + "time" +) + +const ( + dropletAutoscaleBasePath = "/v2/droplets/autoscale" +) + +// DropletAutoscaleService defines an interface for managing droplet autoscale pools through DigitalOcean API +type DropletAutoscaleService interface { + Create(context.Context, *DropletAutoscalePoolRequest) (*DropletAutoscalePool, *Response, error) + Get(context.Context, string) (*DropletAutoscalePool, *Response, error) + List(context.Context, *ListOptions) ([]*DropletAutoscalePool, *Response, error) + ListMembers(context.Context, string, *ListOptions) ([]*DropletAutoscaleResource, *Response, error) + ListHistory(context.Context, string, *ListOptions) ([]*DropletAutoscaleHistoryEvent, *Response, error) + Update(context.Context, string, *DropletAutoscalePoolRequest) (*DropletAutoscalePool, *Response, error) + Delete(context.Context, string) (*Response, error) + DeleteDangerous(context.Context, string) (*Response, error) +} + +// DropletAutoscalePool represents a DigitalOcean droplet autoscale pool +type DropletAutoscalePool struct { + ID string `json:"id"` + Name string `json:"name"` + Config *DropletAutoscaleConfiguration `json:"config"` + DropletTemplate *DropletAutoscaleResourceTemplate `json:"droplet_template"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + CurrentUtilization *DropletAutoscaleResourceUtilization `json:"current_utilization,omitempty"` + Status string `json:"status"` +} + +// DropletAutoscaleConfiguration represents a DigitalOcean droplet autoscale pool configuration +type DropletAutoscaleConfiguration struct { + MinInstances uint64 `json:"min_instances,omitempty"` + MaxInstances uint64 `json:"max_instances,omitempty"` + TargetCPUUtilization float64 `json:"target_cpu_utilization,omitempty"` + TargetMemoryUtilization float64 `json:"target_memory_utilization,omitempty"` + CooldownMinutes uint32 `json:"cooldown_minutes,omitempty"` + TargetNumberInstances uint64 `json:"target_number_instances,omitempty"` +} + +// DropletAutoscaleResourceTemplate represents a DigitalOcean droplet autoscale pool resource template +type DropletAutoscaleResourceTemplate struct { + Size string `json:"size"` + Region string `json:"region"` + Image string `json:"image"` + Tags []string `json:"tags"` + SSHKeys []string `json:"ssh_keys"` + VpcUUID string `json:"vpc_uuid"` + WithDropletAgent bool `json:"with_droplet_agent"` + ProjectID string `json:"project_id"` + IPV6 bool `json:"ipv6"` + UserData string `json:"user_data"` +} + +// DropletAutoscaleResourceUtilization represents a DigitalOcean droplet autoscale pool resource utilization +type DropletAutoscaleResourceUtilization struct { + Memory float64 `json:"memory,omitempty"` + CPU float64 `json:"cpu,omitempty"` +} + +// DropletAutoscaleResource represents a DigitalOcean droplet autoscale pool resource +type DropletAutoscaleResource struct { + DropletID uint64 `json:"droplet_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + HealthStatus string `json:"health_status"` + UnhealthyReason string `json:"unhealthy_reason,omitempty"` + Status string `json:"status"` + CurrentUtilization *DropletAutoscaleResourceUtilization `json:"current_utilization,omitempty"` +} + +// DropletAutoscaleHistoryEvent represents a DigitalOcean droplet autoscale pool history event +type DropletAutoscaleHistoryEvent struct { + HistoryEventID string `json:"history_event_id"` + CurrentInstanceCount uint64 `json:"current_instance_count"` + DesiredInstanceCount uint64 `json:"desired_instance_count"` + Reason string `json:"reason"` + Status string `json:"status"` + ErrorReason string `json:"error_reason,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// DropletAutoscalePoolRequest represents a DigitalOcean droplet autoscale pool create/update request +type DropletAutoscalePoolRequest struct { + Name string `json:"name"` + Config *DropletAutoscaleConfiguration `json:"config"` + DropletTemplate *DropletAutoscaleResourceTemplate `json:"droplet_template"` +} + +type dropletAutoscalePoolRoot struct { + AutoscalePool *DropletAutoscalePool `json:"autoscale_pool"` +} + +type dropletAutoscalePoolsRoot struct { + AutoscalePools []*DropletAutoscalePool `json:"autoscale_pools"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +type dropletAutoscaleMembersRoot struct { + Droplets []*DropletAutoscaleResource `json:"droplets"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +type dropletAutoscaleHistoryEventsRoot struct { + History []*DropletAutoscaleHistoryEvent `json:"history"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +// DropletAutoscaleServiceOp handles communication with droplet autoscale-related methods of the DigitalOcean API +type DropletAutoscaleServiceOp struct { + client *Client +} + +var _ DropletAutoscaleService = &DropletAutoscaleServiceOp{} + +// Create a new droplet autoscale pool +func (d *DropletAutoscaleServiceOp) Create(ctx context.Context, createReq *DropletAutoscalePoolRequest) (*DropletAutoscalePool, *Response, error) { + req, err := d.client.NewRequest(ctx, http.MethodPost, dropletAutoscaleBasePath, createReq) + if err != nil { + return nil, nil, err + } + root := new(dropletAutoscalePoolRoot) + resp, err := d.client.Do(ctx, req, root) + if err != nil { + return nil, nil, err + } + return root.AutoscalePool, resp, nil +} + +// Get an existing droplet autoscale pool +func (d *DropletAutoscaleServiceOp) Get(ctx context.Context, id string) (*DropletAutoscalePool, *Response, error) { + req, err := d.client.NewRequest(ctx, http.MethodGet, fmt.Sprintf("%s/%s", dropletAutoscaleBasePath, id), nil) + if err != nil { + return nil, nil, err + } + root := new(dropletAutoscalePoolRoot) + resp, err := d.client.Do(ctx, req, root) + if err != nil { + return nil, nil, err + } + return root.AutoscalePool, resp, err +} + +// List all existing droplet autoscale pools +func (d *DropletAutoscaleServiceOp) List(ctx context.Context, opts *ListOptions) ([]*DropletAutoscalePool, *Response, error) { + path, err := addOptions(dropletAutoscaleBasePath, opts) + if err != nil { + return nil, nil, err + } + req, err := d.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(dropletAutoscalePoolsRoot) + resp, err := d.client.Do(ctx, req, root) + if err != nil { + return nil, nil, err + } + if root.Links != nil { + resp.Links = root.Links + } + if root.Meta != nil { + resp.Meta = root.Meta + } + return root.AutoscalePools, resp, err +} + +// ListMembers all members for an existing droplet autoscale pool +func (d *DropletAutoscaleServiceOp) ListMembers(ctx context.Context, id string, opts *ListOptions) ([]*DropletAutoscaleResource, *Response, error) { + path, err := addOptions(fmt.Sprintf("%s/%s/members", dropletAutoscaleBasePath, id), opts) + if err != nil { + return nil, nil, err + } + req, err := d.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(dropletAutoscaleMembersRoot) + resp, err := d.client.Do(ctx, req, root) + if err != nil { + return nil, nil, err + } + if root.Links != nil { + resp.Links = root.Links + } + if root.Meta != nil { + resp.Meta = root.Meta + } + return root.Droplets, resp, err +} + +// ListHistory all history events for an existing droplet autoscale pool +func (d *DropletAutoscaleServiceOp) ListHistory(ctx context.Context, id string, opts *ListOptions) ([]*DropletAutoscaleHistoryEvent, *Response, error) { + path, err := addOptions(fmt.Sprintf("%s/%s/history", dropletAutoscaleBasePath, id), opts) + if err != nil { + return nil, nil, err + } + req, err := d.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(dropletAutoscaleHistoryEventsRoot) + resp, err := d.client.Do(ctx, req, root) + if err != nil { + return nil, nil, err + } + if root.Links != nil { + resp.Links = root.Links + } + if root.Meta != nil { + resp.Meta = root.Meta + } + return root.History, resp, err +} + +// Update an existing autoscale pool +func (d *DropletAutoscaleServiceOp) Update(ctx context.Context, id string, updateReq *DropletAutoscalePoolRequest) (*DropletAutoscalePool, *Response, error) { + req, err := d.client.NewRequest(ctx, http.MethodPut, fmt.Sprintf("%s/%s", dropletAutoscaleBasePath, id), updateReq) + if err != nil { + return nil, nil, err + } + root := new(dropletAutoscalePoolRoot) + resp, err := d.client.Do(ctx, req, root) + if err != nil { + return nil, nil, err + } + return root.AutoscalePool, resp, nil +} + +// Delete an existing autoscale pool +func (d *DropletAutoscaleServiceOp) Delete(ctx context.Context, id string) (*Response, error) { + req, err := d.client.NewRequest(ctx, http.MethodDelete, fmt.Sprintf("%s/%s", dropletAutoscaleBasePath, id), nil) + if err != nil { + return nil, err + } + return d.client.Do(ctx, req, nil) +} + +// DeleteDangerous deletes an existing autoscale pool with all underlying resources +func (d *DropletAutoscaleServiceOp) DeleteDangerous(ctx context.Context, id string) (*Response, error) { + req, err := d.client.NewRequest(ctx, http.MethodDelete, fmt.Sprintf("%s/%s", dropletAutoscaleBasePath, id), nil) + req.Header.Set("X-Dangerous", "true") + if err != nil { + return nil, err + } + return d.client.Do(ctx, req, nil) +} diff --git a/droplet_autoscale_test.go b/droplet_autoscale_test.go new file mode 100644 index 00000000..6f80ac18 --- /dev/null +++ b/droplet_autoscale_test.go @@ -0,0 +1,617 @@ +package godo + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var dropletAutoscaleListHistoryJSONResponse = ` +{ + "history": [ + { + "history_event_id": "4344c45f-7574-493b-a96c-df805c65a900", + "current_instance_count": 0, + "desired_instance_count": 1, + "reason": "configuration update", + "status": "success", + "created_at": "2024-10-18T19:03:09Z", + "updated_at": "2024-10-18T19:03:09Z" + }, + { + "history_event_id": "9ad436f7-af57-49ff-b416-0043721055b2", + "current_instance_count": 1, + "desired_instance_count": 2, + "reason": "scaling up (desired=2 current=1)", + "status": "success", + "created_at": "2024-10-18T19:15:24Z", + "updated_at": "2024-10-18T19:15:24Z" + }, + { + "history_event_id": "45390191-d077-49e9-a3c4-c2eb903bc1a2", + "current_instance_count": 2, + "desired_instance_count": 1, + "reason": "scaling down (desired=1 current=2)", + "status": "success", + "created_at": "2024-10-18T19:47:24Z", + "updated_at": "2024-10-18T19:47:24Z" + } + ], + "links": {}, + "meta": { + "total": 3 + } +} +` + +var dropletAutoscaleListMembersJSONResponse = ` +{ + "droplets": [ + { + "droplet_id": 1677149, + "created_at": "2024-10-18T19:03:09Z", + "updated_at": "2024-10-18T19:03:24Z", + "health_status": "healthy", + "status": "active", + "current_utilization": { + "memory": 0.35, + "cpu": 0.0012 + } + }, + { + "droplet_id": 1677150, + "created_at": "2024-10-18T19:04:09Z", + "updated_at": "2024-10-18T19:04:24Z", + "health_status": "healthy", + "status": "active", + "current_utilization": { + "memory": 0.40, + "cpu": 0.0013 + } + } + ], + "links": {}, + "meta": { + "total": 2 + } +} +` + +var dropletAutoscaleListJSONResponse = ` +{ + "autoscale_pools": [ + { + "id": "a4456a02-133d-4fea-8f2d-94dc6a7bf9c9", + "name": "test-autoscalergroup-03", + "config": { + "min_instances": 1, + "max_instances": 5, + "target_cpu_utilization": 0.5, + "cooldown_minutes": 5 + }, + "droplet_template": { + "size": "s-1vcpu-512mb-10gb", + "region": "s2r1", + "image": "547864", + "tags": [ + "test-ag-01" + ], + "ssh_keys": [ + "372862", + "367582", + "355790" + ], + "vpc_uuid": "72b0812c-7535-4388-8507-5ad29b4487b3", + "with_droplet_agent": false, + "project_id": "", + "ipv6": true, + "user_data": "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n" + }, + "created_at": "2024-10-21T13:05:23Z", + "updated_at": "2024-10-21T13:05:23Z", + "current_utilization": { + "memory": 0.33, + "cpu": 0.0007 + }, + "status": "active" + }, + { + "id": "1044bfca-e490-44a1-aa1c-6f002daf6a13", + "name": "test-autoscalergroup-01", + "config": { + "min_instances": 1, + "max_instances": 5, + "target_cpu_utilization": 0.5, + "cooldown_minutes": 5 + }, + "droplet_template": { + "size": "s-1vcpu-512mb-10gb", + "region": "s2r1", + "image": "547864", + "tags": [ + "test-ag-01" + ], + "ssh_keys": [ + "372862", + "367582", + "355790" + ], + "vpc_uuid": "72b0812c-7535-4388-8507-5ad29b4487b3", + "with_droplet_agent": false, + "project_id": "", + "ipv6": true, + "user_data": "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n" + }, + "created_at": "2024-10-18T19:03:08Z", + "updated_at": "2024-10-18T19:03:08Z", + "current_utilization": { + "memory": 0.35, + "cpu": 0.0009 + }, + "status": "active" + }, + { + "id": "b92962b5-26a5-4e63-a1d9-a0f5d44b4f23", + "name": "test-autoscalergroup-02", + "config": { + "min_instances": 1, + "max_instances": 5, + "target_cpu_utilization": 0.5, + "cooldown_minutes": 5 + }, + "droplet_template": { + "size": "s-1vcpu-512mb-10gb", + "region": "s2r1", + "image": "547864", + "tags": [ + "test-ag-01" + ], + "ssh_keys": [ + "372862", + "367582", + "355790" + ], + "vpc_uuid": "72b0812c-7535-4388-8507-5ad29b4487b3", + "with_droplet_agent": false, + "project_id": "", + "ipv6": true, + "user_data": "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n" + }, + "created_at": "2024-10-21T13:05:12Z", + "updated_at": "2024-10-21T13:05:12Z", + "current_utilization": { + "memory": 0.56, + "cpu": 0.0002 + }, + "status": "active" + } + ], + "links": {}, + "meta": { + "total": 3 + } +} +` + +var dropletAutoscaleGetJSONResponse = ` +{ + "autoscale_pool": { + "id": "1044bfca-e490-44a1-aa1c-6f002daf6a13", + "name": "test-autoscalergroup-01", + "config": { + "min_instances": 1, + "max_instances": 5, + "target_cpu_utilization": 0.5, + "cooldown_minutes": 5 + }, + "droplet_template": { + "size": "s-1vcpu-512mb-10gb", + "region": "s2r1", + "image": "547864", + "tags": [ + "test-ag-01" + ], + "ssh_keys": [ + "372862", + "367582", + "355790" + ], + "vpc_uuid": "72b0812c-7535-4388-8507-5ad29b4487b3", + "with_droplet_agent": false, + "project_id": "", + "ipv6": true, + "user_data": "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n" + }, + "created_at": "2024-10-18T19:03:08Z", + "updated_at": "2024-10-18T19:03:08Z", + "current_utilization": { + "memory": 0.35, + "cpu": 0.0008 + }, + "status": "active" + } +} +` + +func TestDropletAutoscaler_Get(t *testing.T) { + setup() + defer teardown() + + autoscalePoolID := "1044bfca-e490-44a1-aa1c-6f002daf6a13" + mux.HandleFunc(fmt.Sprintf("%s/%s", dropletAutoscaleBasePath, autoscalePoolID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprintf(w, dropletAutoscaleGetJSONResponse) + }) + + expectedPoolResp := &DropletAutoscalePool{ + ID: "1044bfca-e490-44a1-aa1c-6f002daf6a13", + Name: "test-autoscalergroup-01", + Config: &DropletAutoscaleConfiguration{ + MinInstances: 1, + MaxInstances: 5, + TargetCPUUtilization: 0.5, + CooldownMinutes: 5, + }, + DropletTemplate: &DropletAutoscaleResourceTemplate{ + Size: "s-1vcpu-512mb-10gb", + Region: "s2r1", + Image: "547864", + Tags: []string{"test-ag-01"}, + SSHKeys: []string{"372862", "367582", "355790"}, + VpcUUID: "72b0812c-7535-4388-8507-5ad29b4487b3", + IPV6: true, + UserData: "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n", + }, + CurrentUtilization: &DropletAutoscaleResourceUtilization{ + Memory: 0.35, + CPU: 0.0008, + }, + Status: "active", + } + + gotPoolResp, _, err := client.DropletAutoscale.Get(ctx, autoscalePoolID) + require.NoError(t, err) + require.NotNil(t, gotPoolResp) + expectedPoolResp.CreatedAt = gotPoolResp.CreatedAt + expectedPoolResp.UpdatedAt = gotPoolResp.UpdatedAt + assert.Equal(t, expectedPoolResp, gotPoolResp) +} + +func TestDropletAutoscaler_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc(dropletAutoscaleBasePath, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprintf(w, dropletAutoscaleListJSONResponse) + }) + + expectedConfig := &DropletAutoscaleConfiguration{ + MinInstances: 1, + MaxInstances: 5, + TargetCPUUtilization: 0.5, + CooldownMinutes: 5, + } + expectedDropletTemplate := &DropletAutoscaleResourceTemplate{ + Size: "s-1vcpu-512mb-10gb", + Region: "s2r1", + Image: "547864", + Tags: []string{"test-ag-01"}, + SSHKeys: []string{"372862", "367582", "355790"}, + VpcUUID: "72b0812c-7535-4388-8507-5ad29b4487b3", + IPV6: true, + UserData: "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n", + } + expectedPoolsResp := []*DropletAutoscalePool{ + { + ID: "1044bfca-e490-44a1-aa1c-6f002daf6a13", + Name: "test-autoscalergroup-01", + Config: expectedConfig, + DropletTemplate: expectedDropletTemplate, + CurrentUtilization: &DropletAutoscaleResourceUtilization{ + Memory: 0.35, + CPU: 0.0009, + }, + Status: "active", + }, + { + ID: "b92962b5-26a5-4e63-a1d9-a0f5d44b4f23", + Name: "test-autoscalergroup-02", + Config: expectedConfig, + DropletTemplate: expectedDropletTemplate, + CurrentUtilization: &DropletAutoscaleResourceUtilization{ + Memory: 0.56, + CPU: 0.0002, + }, + Status: "active", + }, + { + ID: "a4456a02-133d-4fea-8f2d-94dc6a7bf9c9", + Name: "test-autoscalergroup-03", + Config: expectedConfig, + DropletTemplate: expectedDropletTemplate, + CurrentUtilization: &DropletAutoscaleResourceUtilization{ + Memory: 0.33, + CPU: 0.0007, + }, + Status: "active", + }, + } + + gotPoolsResp, _, err := client.DropletAutoscale.List(ctx, nil) + require.NoError(t, err) + require.NotEmpty(t, gotPoolsResp) + sort.SliceStable(gotPoolsResp, func(i, j int) bool { + return gotPoolsResp[i].Name < gotPoolsResp[j].Name + }) + for idx := range gotPoolsResp { + expectedPoolsResp[idx].CreatedAt = gotPoolsResp[idx].CreatedAt + expectedPoolsResp[idx].UpdatedAt = gotPoolsResp[idx].UpdatedAt + } + assert.Equal(t, expectedPoolsResp, gotPoolsResp) +} + +func TestDropletAutoscaler_ListMembers(t *testing.T) { + setup() + defer teardown() + + autoscalePoolID := "1044bfca-e490-44a1-aa1c-6f002daf6a13" + mux.HandleFunc(fmt.Sprintf("%s/%s/members", dropletAutoscaleBasePath, autoscalePoolID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprintf(w, dropletAutoscaleListMembersJSONResponse) + }) + + expectedMembersResp := []*DropletAutoscaleResource{ + { + DropletID: 1677149, + HealthStatus: "healthy", + Status: "active", + CurrentUtilization: &DropletAutoscaleResourceUtilization{ + Memory: 0.35, + CPU: 0.0012, + }, + }, + { + DropletID: 1677150, + HealthStatus: "healthy", + Status: "active", + CurrentUtilization: &DropletAutoscaleResourceUtilization{ + Memory: 0.40, + CPU: 0.0013, + }, + }, + } + + gotMembersResp, _, err := client.DropletAutoscale.ListMembers(ctx, autoscalePoolID, nil) + require.NoError(t, err) + require.NotEmpty(t, gotMembersResp) + sort.SliceStable(gotMembersResp, func(i, j int) bool { + return gotMembersResp[i].DropletID < gotMembersResp[j].DropletID + }) + for idx := range gotMembersResp { + expectedMembersResp[idx].CreatedAt = gotMembersResp[idx].CreatedAt + expectedMembersResp[idx].UpdatedAt = gotMembersResp[idx].UpdatedAt + } + assert.Equal(t, expectedMembersResp, gotMembersResp) +} + +func TestDropletAutoscaler_ListHistory(t *testing.T) { + setup() + defer teardown() + + autoscalePoolID := "1044bfca-e490-44a1-aa1c-6f002daf6a13" + mux.HandleFunc(fmt.Sprintf("%s/%s/history", dropletAutoscaleBasePath, autoscalePoolID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprintf(w, dropletAutoscaleListHistoryJSONResponse) + }) + + expectedHistoryResp := []*DropletAutoscaleHistoryEvent{ + { + HistoryEventID: "4344c45f-7574-493b-a96c-df805c65a900", + CurrentInstanceCount: 0, + DesiredInstanceCount: 1, + Reason: "configuration update", + Status: "success", + }, + { + HistoryEventID: "9ad436f7-af57-49ff-b416-0043721055b2", + CurrentInstanceCount: 1, + DesiredInstanceCount: 2, + Reason: "scaling up (desired=2 current=1)", + Status: "success", + }, + { + HistoryEventID: "45390191-d077-49e9-a3c4-c2eb903bc1a2", + CurrentInstanceCount: 2, + DesiredInstanceCount: 1, + Reason: "scaling down (desired=1 current=2)", + Status: "success", + }, + } + + gotHistoryResp, _, err := client.DropletAutoscale.ListHistory(ctx, autoscalePoolID, nil) + require.NoError(t, err) + require.NotEmpty(t, gotHistoryResp) + sort.SliceStable(gotHistoryResp, func(i, j int) bool { + return gotHistoryResp[i].CreatedAt.Before(gotHistoryResp[j].CreatedAt) + }) + for idx := range gotHistoryResp { + expectedHistoryResp[idx].CreatedAt = gotHistoryResp[idx].CreatedAt + expectedHistoryResp[idx].UpdatedAt = gotHistoryResp[idx].UpdatedAt + } + assert.Equal(t, expectedHistoryResp, gotHistoryResp) +} + +func TestDropletAutoscaler_Create(t *testing.T) { + setup() + defer teardown() + + createReq := &DropletAutoscalePoolRequest{ + Name: "test-autoscalergroup-01", + Config: &DropletAutoscaleConfiguration{ + MinInstances: 1, + MaxInstances: 5, + TargetCPUUtilization: 0.5, + }, + DropletTemplate: &DropletAutoscaleResourceTemplate{ + Size: "s-1vcpu-512mb-10gb", + Region: "s2r1", + Image: "547864", + Tags: []string{"test-ag-01"}, + SSHKeys: []string{"372862", "367582", "355790"}, + VpcUUID: "72b0812c-7535-4388-8507-5ad29b4487b3", + IPV6: true, + UserData: "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n", + }, + } + + mux.HandleFunc(dropletAutoscaleBasePath, func(w http.ResponseWriter, r *http.Request) { + req := new(DropletAutoscalePoolRequest) + err := json.NewDecoder(r.Body).Decode(req) + if err != nil { + t.Fatal(err) + } + testMethod(t, r, http.MethodPost) + assert.Equal(t, createReq, req) + fmt.Fprintf(w, dropletAutoscaleGetJSONResponse) + }) + + expectedPoolResp := &DropletAutoscalePool{ + ID: "1044bfca-e490-44a1-aa1c-6f002daf6a13", + Name: "test-autoscalergroup-01", + Config: &DropletAutoscaleConfiguration{ + MinInstances: 1, + MaxInstances: 5, + TargetCPUUtilization: 0.5, + CooldownMinutes: 5, + }, + DropletTemplate: &DropletAutoscaleResourceTemplate{ + Size: "s-1vcpu-512mb-10gb", + Region: "s2r1", + Image: "547864", + Tags: []string{"test-ag-01"}, + SSHKeys: []string{"372862", "367582", "355790"}, + VpcUUID: "72b0812c-7535-4388-8507-5ad29b4487b3", + IPV6: true, + UserData: "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n", + }, + CurrentUtilization: &DropletAutoscaleResourceUtilization{ + Memory: 0.35, + CPU: 0.0008, + }, + Status: "active", + } + + createPoolResp, _, err := client.DropletAutoscale.Create(ctx, createReq) + require.NoError(t, err) + require.NotEmpty(t, createPoolResp) + expectedPoolResp.CreatedAt = createPoolResp.CreatedAt + expectedPoolResp.UpdatedAt = createPoolResp.UpdatedAt + assert.Equal(t, expectedPoolResp, createPoolResp) +} + +func TestDropletAutoscaler_Update(t *testing.T) { + setup() + defer teardown() + + updateReq := &DropletAutoscalePoolRequest{ + Name: "test-autoscalergroup-01", + Config: &DropletAutoscaleConfiguration{ + MinInstances: 1, + MaxInstances: 5, + TargetCPUUtilization: 0.5, + }, + DropletTemplate: &DropletAutoscaleResourceTemplate{ + Size: "s-1vcpu-512mb-10gb", + Region: "s2r1", + Image: "547864", + Tags: []string{"test-ag-01"}, + SSHKeys: []string{"372862", "367582", "355790"}, + VpcUUID: "72b0812c-7535-4388-8507-5ad29b4487b3", + IPV6: true, + UserData: "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n", + }, + } + + autoscalePoolID := "d50d8276-ad17-475d-8d2a-26b0acac756c" + mux.HandleFunc(fmt.Sprintf("%s/%s", dropletAutoscaleBasePath, autoscalePoolID), func(w http.ResponseWriter, r *http.Request) { + req := new(DropletAutoscalePoolRequest) + err := json.NewDecoder(r.Body).Decode(req) + if err != nil { + t.Fatal(err) + } + testMethod(t, r, http.MethodPut) + assert.Equal(t, updateReq, req) + fmt.Fprintf(w, dropletAutoscaleGetJSONResponse) + }) + + expectedPoolResp := &DropletAutoscalePool{ + ID: "1044bfca-e490-44a1-aa1c-6f002daf6a13", + Name: "test-autoscalergroup-01", + Config: &DropletAutoscaleConfiguration{ + MinInstances: 1, + MaxInstances: 5, + TargetCPUUtilization: 0.5, + CooldownMinutes: 5, + }, + DropletTemplate: &DropletAutoscaleResourceTemplate{ + Size: "s-1vcpu-512mb-10gb", + Region: "s2r1", + Image: "547864", + Tags: []string{"test-ag-01"}, + SSHKeys: []string{"372862", "367582", "355790"}, + VpcUUID: "72b0812c-7535-4388-8507-5ad29b4487b3", + IPV6: true, + UserData: "\n#cloud-config\nruncmd:\n- apt-get update\n- apt-get install -y stress-ng\n", + }, + CurrentUtilization: &DropletAutoscaleResourceUtilization{ + Memory: 0.35, + CPU: 0.0008, + }, + Status: "active", + } + + updatePoolResp, _, err := client.DropletAutoscale.Update(ctx, autoscalePoolID, updateReq) + require.NoError(t, err) + require.NotEmpty(t, updatePoolResp) + expectedPoolResp.CreatedAt = updatePoolResp.CreatedAt + expectedPoolResp.UpdatedAt = updatePoolResp.UpdatedAt + assert.Equal(t, expectedPoolResp, updatePoolResp) +} + +func TestDropletAutoscaler_Delete(t *testing.T) { + setup() + defer teardown() + + autoscalePoolID := "d50d8276-ad17-475d-8d2a-26b0acac756c" + mux.HandleFunc(fmt.Sprintf("%s/%s", dropletAutoscaleBasePath, autoscalePoolID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.DropletAutoscale.Delete(ctx, autoscalePoolID) + assert.NoError(t, err) +} + +func TestDropletAutoscaler_DeleteDangerous(t *testing.T) { + setup() + defer teardown() + + autoscalePoolID := "d50d8276-ad17-475d-8d2a-26b0acac756c" + mux.HandleFunc(fmt.Sprintf("%s/%s", dropletAutoscaleBasePath, autoscalePoolID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + if expectedHeader, err := strconv.ParseBool(r.Header.Get("X-Dangerous")); err != nil { + t.Fatal(err) + } else if !expectedHeader { + t.Errorf("Request header = %v, expected %v", r.Header.Get("X-Dangerous"), true) + } + }) + + _, err := client.DropletAutoscale.DeleteDangerous(ctx, autoscalePoolID) + assert.NoError(t, err) +} diff --git a/godo.go b/godo.go index 068cdceb..995469e9 100644 --- a/godo.go +++ b/godo.go @@ -65,6 +65,7 @@ type Client struct { Domains DomainsService Droplets DropletsService DropletActions DropletActionsService + DropletAutoscale DropletAutoscaleService Firewalls FirewallsService FloatingIPs FloatingIPsService FloatingIPActions FloatingIPActionsService @@ -275,6 +276,7 @@ func NewClient(httpClient *http.Client) *Client { c.Domains = &DomainsServiceOp{client: c} c.Droplets = &DropletsServiceOp{client: c} c.DropletActions = &DropletActionsServiceOp{client: c} + c.DropletAutoscale = &DropletAutoscaleServiceOp{client: c} c.Firewalls = &FirewallsServiceOp{client: c} c.FloatingIPs = &FloatingIPsServiceOp{client: c} c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}