diff --git a/CHANGELOG.md b/CHANGELOG.md index 71886a35..b89def70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [v1.1.0] - 2017-06-06 + +### Added +- #145 Add FirewallsService for managing Firewalls with the DigitalOcean API. - @viola +- #139 Add TTL field to the Domains. - @xmudrii + +### Fixed +- #143 Fix oauth2.NoContext depreciation. - @jbowens +- #141 Fix DropletActions on tagged resources. - @xmudrii + ## [v1.0.0] - 2017-03-10 ### Added diff --git a/firewalls.go b/firewalls.go new file mode 100644 index 00000000..661f944f --- /dev/null +++ b/firewalls.go @@ -0,0 +1,263 @@ +package godo + +import ( + "path" + "strconv" + + "github.com/digitalocean/godo/context" +) + +const firewallsBasePath = "/v2/firewalls" + +// FirewallsService is an interface for managing Firewalls with the DigitalOcean API. +// See: https://developers.digitalocean.com/documentation/documentation/v2/#firewalls +type FirewallsService interface { + Get(context.Context, string) (*Firewall, *Response, error) + Create(context.Context, *FirewallRequest) (*Firewall, *Response, error) + Update(context.Context, string, *FirewallRequest) (*Firewall, *Response, error) + Delete(context.Context, string) (*Response, error) + List(context.Context, *ListOptions) ([]Firewall, *Response, error) + ListByDroplet(context.Context, int, *ListOptions) ([]Firewall, *Response, error) + AddDroplets(context.Context, string, ...int) (*Response, error) + RemoveDroplets(context.Context, string, ...int) (*Response, error) + AddTags(context.Context, string, ...string) (*Response, error) + RemoveTags(context.Context, string, ...string) (*Response, error) + AddRules(context.Context, string, *FirewallRulesRequest) (*Response, error) + RemoveRules(context.Context, string, *FirewallRulesRequest) (*Response, error) +} + +// FirewallsServiceOp handles communication with Firewalls methods of the DigitalOcean API. +type FirewallsServiceOp struct { + client *Client +} + +// Firewall represents a DigitalOcean Firewall configuration. +type Firewall struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + InboundRules []InboundRule `json:"inbound_rules"` + OutboundRules []OutboundRule `json:"outbound_rules"` + DropletIDs []int `json:"droplet_ids"` + Tags []string `json:"tags"` + Created string `json:"created_at"` + PendingChanges []PendingChange `json:"pending_changes"` +} + +// String creates a human-readable description of a Firewall. +func (fw Firewall) String() string { + return Stringify(fw) +} + +// FirewallRequest represents the configuration to be applied to an existing or a new Firewall. +type FirewallRequest struct { + Name string `json:"name"` + InboundRules []InboundRule `json:"inbound_rules"` + OutboundRules []OutboundRule `json:"outbound_rules"` + DropletIDs []int `json:"droplet_ids"` + Tags []string `json:"tags"` +} + +// FirewallRulesRequest represents rules configuration to be applied to an existing Firewall. +type FirewallRulesRequest struct { + InboundRules []InboundRule `json:"inbound_rules"` + OutboundRules []OutboundRule `json:"outbound_rules"` +} + +// InboundRule represents a DigitalOcean Firewall inbound rule. +type InboundRule struct { + Protocol string `json:"protocol,omitempty"` + PortRange string `json:"ports,omitempty"` + Sources *Sources `json:"sources"` +} + +// OutboundRule represents a DigitalOcean Firewall outbound rule. +type OutboundRule struct { + Protocol string `json:"protocol,omitempty"` + PortRange string `json:"ports,omitempty"` + Destinations *Destinations `json:"destinations"` +} + +// Sources represents a DigitalOcean Firewall InboundRule sources. +type Sources struct { + Addresses []string `json:"addresses,omitempty"` + Tags []string `json:"tags,omitempty"` + DropletIDs []int `json:"droplet_ids,omitempty"` + LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"` +} + +// PendingChange represents a DigitalOcean Firewall status details. +type PendingChange struct { + DropletID int `json:"droplet_id,omitempty"` + Removing bool `json:"removing,omitempty"` + Status string `json:"status,omitempty"` +} + +// Destinations represents a DigitalOcean Firewall OutboundRule destinations. +type Destinations struct { + Addresses []string `json:"addresses,omitempty"` + Tags []string `json:"tags,omitempty"` + DropletIDs []int `json:"droplet_ids,omitempty"` + LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"` +} + +var _ FirewallsService = &FirewallsServiceOp{} + +// Get an existing Firewall by its identifier. +func (fw *FirewallsServiceOp) Get(ctx context.Context, fID string) (*Firewall, *Response, error) { + path := path.Join(firewallsBasePath, fID) + + req, err := fw.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + root := new(firewallRoot) + resp, err := fw.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Firewall, resp, err +} + +// Create a new Firewall with a given configuration. +func (fw *FirewallsServiceOp) Create(ctx context.Context, fr *FirewallRequest) (*Firewall, *Response, error) { + req, err := fw.client.NewRequest(ctx, "POST", firewallsBasePath, fr) + if err != nil { + return nil, nil, err + } + + root := new(firewallRoot) + resp, err := fw.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Firewall, resp, err +} + +// Update an existing Firewall with new configuration. +func (fw *FirewallsServiceOp) Update(ctx context.Context, fID string, fr *FirewallRequest) (*Firewall, *Response, error) { + path := path.Join(firewallsBasePath, fID) + + req, err := fw.client.NewRequest(ctx, "PUT", path, fr) + if err != nil { + return nil, nil, err + } + + root := new(firewallRoot) + resp, err := fw.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Firewall, resp, err +} + +// Delete a Firewall by its identifier. +func (fw *FirewallsServiceOp) Delete(ctx context.Context, fID string) (*Response, error) { + path := path.Join(firewallsBasePath, fID) + return fw.createAndDoReq(ctx, "DELETE", path, nil) +} + +// List Firewalls. +func (fw *FirewallsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Firewall, *Response, error) { + path, err := addOptions(firewallsBasePath, opt) + if err != nil { + return nil, nil, err + } + + return fw.listHelper(ctx, path) +} + +// ListByDroplet Firewalls. +func (fw *FirewallsServiceOp) ListByDroplet(ctx context.Context, dID int, opt *ListOptions) ([]Firewall, *Response, error) { + basePath := path.Join(dropletBasePath, strconv.Itoa(dID), "firewalls") + path, err := addOptions(basePath, opt) + if err != nil { + return nil, nil, err + } + + return fw.listHelper(ctx, path) +} + +// AddDroplets to a Firewall. +func (fw *FirewallsServiceOp) AddDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "droplets") + return fw.createAndDoReq(ctx, "POST", path, &dropletsRequest{IDs: dropletIDs}) +} + +// RemoveDroplets from a Firewall. +func (fw *FirewallsServiceOp) RemoveDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "droplets") + return fw.createAndDoReq(ctx, "DELETE", path, &dropletsRequest{IDs: dropletIDs}) +} + +// AddTags to a Firewall. +func (fw *FirewallsServiceOp) AddTags(ctx context.Context, fID string, tags ...string) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "tags") + return fw.createAndDoReq(ctx, "POST", path, &tagsRequest{Tags: tags}) +} + +// RemoveTags from a Firewall. +func (fw *FirewallsServiceOp) RemoveTags(ctx context.Context, fID string, tags ...string) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "tags") + return fw.createAndDoReq(ctx, "DELETE", path, &tagsRequest{Tags: tags}) +} + +// AddRules to a Firewall. +func (fw *FirewallsServiceOp) AddRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "rules") + return fw.createAndDoReq(ctx, "POST", path, rr) +} + +// RemoveRules from a Firewall. +func (fw *FirewallsServiceOp) RemoveRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "rules") + return fw.createAndDoReq(ctx, "DELETE", path, rr) +} + +type dropletsRequest struct { + IDs []int `json:"droplet_ids"` +} + +type tagsRequest struct { + Tags []string `json:"tags"` +} + +type firewallRoot struct { + Firewall *Firewall `json:"firewall"` +} + +type firewallsRoot struct { + Firewalls []Firewall `json:"firewalls"` + Links *Links `json:"links"` +} + +func (fw *FirewallsServiceOp) createAndDoReq(ctx context.Context, method, path string, v interface{}) (*Response, error) { + req, err := fw.client.NewRequest(ctx, method, path, v) + if err != nil { + return nil, err + } + + return fw.client.Do(ctx, req, nil) +} + +func (fw *FirewallsServiceOp) listHelper(ctx context.Context, path string) ([]Firewall, *Response, error) { + req, err := fw.client.NewRequest(ctx, "GET", path, nil) + if err != nil { + return nil, nil, err + } + + root := new(firewallsRoot) + resp, err := fw.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Firewalls, resp, err +} diff --git a/firewalls_test.go b/firewalls_test.go new file mode 100644 index 00000000..cd6d94f1 --- /dev/null +++ b/firewalls_test.go @@ -0,0 +1,839 @@ +package godo + +import ( + "encoding/json" + "fmt" + "net/http" + "path" + "reflect" + "testing" +) + +var ( + firewallCreateJSONBody = ` +{ + "name": "f-i-r-e-w-a-l-l", + "inbound_rules": [ + { + "protocol": "icmp", + "sources": { + "addresses": ["0.0.0.0/0"], + "tags": ["frontend"], + "droplet_ids": [123, 456], + "load_balancer_uids": ["lb-uid"] + } + }, + { + "protocol": "tcp", + "ports": "8000-9000", + "sources": { + "addresses": ["0.0.0.0/0"] + } + } + ], + "outbound_rules": [ + { + "protocol": "icmp", + "destinations": { + "tags": ["frontend"] + } + }, + { + "protocol": "tcp", + "ports": "8000-9000", + "destinations": { + "addresses": ["::/1"] + } + } + ], + "droplet_ids": [123], + "tags": ["frontend"] +} +` + firewallRulesJSONBody = ` +{ + "inbound_rules": [ + { + "protocol": "tcp", + "ports": "22", + "sources": { + "addresses": ["0.0.0.0/0"] + } + } + ], + "outbound_rules": [ + { + "protocol": "tcp", + "ports": "443", + "destinations": { + "addresses": ["0.0.0.0/0"] + } + } + ] +} +` + firewallUpdateJSONBody = ` +{ + "name": "f-i-r-e-w-a-l-l", + "inbound_rules": [ + { + "protocol": "tcp", + "ports": "443", + "sources": { + "addresses": ["10.0.0.0/8"] + } + } + ], + "droplet_ids": [123], + "tags": [] +} +` + firewallUpdateJSONResponse = ` +{ + "firewall": { + "id": "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", + "name": "f-i-r-e-w-a-l-l", + "inbound_rules": [ + { + "protocol": "tcp", + "ports": "443", + "sources": { + "addresses": ["10.0.0.0/8"] + } + } + ], + "outbound_rules": [], + "created_at": "2017-04-06T13:07:27Z", + "droplet_ids": [ + 123 + ], + "tags": [] + } +} +` + firewallJSONResponse = ` +{ + "firewall": { + "id": "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", + "name": "f-i-r-e-w-a-l-l", + "status": "waiting", + "inbound_rules": [ + { + "protocol": "icmp", + "ports": "0", + "sources": { + "tags": ["frontend"] + } + }, + { + "protocol": "tcp", + "ports": "8000-9000", + "sources": { + "addresses": ["0.0.0.0/0"] + } + } + ], + "outbound_rules": [ + { + "protocol": "icmp", + "ports": "0" + }, + { + "protocol": "tcp", + "ports": "8000-9000", + "destinations": { + "addresses": ["::/1"] + } + } + ], + "created_at": "2017-04-06T13:07:27Z", + "droplet_ids": [ + 123 + ], + "tags": [ + "frontend" + ], + "pending_changes": [ + { + "droplet_id": 123, + "removing": false, + "status": "waiting" + } + ] + } +} +` + firewallListJSONResponse = ` +{ + "firewalls": [ + { + "id": "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", + "name": "f-i-r-e-w-a-l-l", + "inbound_rules": [ + { + "protocol": "icmp", + "ports": "0", + "sources": { + "tags": ["frontend"] + } + }, + { + "protocol": "tcp", + "ports": "8000-9000", + "sources": { + "addresses": ["0.0.0.0/0"] + } + } + ], + "outbound_rules": [ + { + "protocol": "icmp", + "ports": "0" + }, + { + "protocol": "tcp", + "ports": "8000-9000", + "destinations": { + "addresses": ["::/1"] + } + } + ], + "created_at": "2017-04-06T13:07:27Z", + "droplet_ids": [ + 123 + ], + "tags": [ + "frontend" + ] + } + ], + "links": {}, + "meta": { + "total": 1 + } +} +` +) + +func TestFirewalls_Get(t *testing.T) { + setup() + defer teardown() + + urlStr := "/v2/firewalls" + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr = path.Join(urlStr, fID) + + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, firewallJSONResponse) + }) + + actualFirewall, _, err := client.Firewalls.Get(ctx, fID) + if err != nil { + t.Errorf("Firewalls.Get returned error: %v", err) + } + + expectedFirewall := &Firewall{ + ID: "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", + Name: "f-i-r-e-w-a-l-l", + Status: "waiting", + InboundRules: []InboundRule{ + { + Protocol: "icmp", + PortRange: "0", + Sources: &Sources{ + Tags: []string{"frontend"}, + }, + }, + { + Protocol: "tcp", + PortRange: "8000-9000", + Sources: &Sources{ + Addresses: []string{"0.0.0.0/0"}, + }, + }, + }, + OutboundRules: []OutboundRule{ + { + Protocol: "icmp", + PortRange: "0", + }, + { + Protocol: "tcp", + PortRange: "8000-9000", + Destinations: &Destinations{ + Addresses: []string{"::/1"}, + }, + }, + }, + Created: "2017-04-06T13:07:27Z", + DropletIDs: []int{123}, + Tags: []string{"frontend"}, + PendingChanges: []PendingChange{ + { + DropletID: 123, + Removing: false, + Status: "waiting", + }, + }, + } + + if !reflect.DeepEqual(actualFirewall, expectedFirewall) { + t.Errorf("Firewalls.Get returned %+v, expected %+v", actualFirewall, expectedFirewall) + } +} + +func TestFirewalls_Create(t *testing.T) { + setup() + defer teardown() + + expectedFirewallRequest := &FirewallRequest{ + Name: "f-i-r-e-w-a-l-l", + InboundRules: []InboundRule{ + { + Protocol: "icmp", + Sources: &Sources{ + Addresses: []string{"0.0.0.0/0"}, + Tags: []string{"frontend"}, + DropletIDs: []int{123, 456}, + LoadBalancerUIDs: []string{"lb-uid"}, + }, + }, + { + Protocol: "tcp", + PortRange: "8000-9000", + Sources: &Sources{ + Addresses: []string{"0.0.0.0/0"}, + }, + }, + }, + OutboundRules: []OutboundRule{ + { + Protocol: "icmp", + Destinations: &Destinations{ + Tags: []string{"frontend"}, + }, + }, + { + Protocol: "tcp", + PortRange: "8000-9000", + Destinations: &Destinations{ + Addresses: []string{"::/1"}, + }, + }, + }, + DropletIDs: []int{123}, + Tags: []string{"frontend"}, + } + + mux.HandleFunc("/v2/firewalls", func(w http.ResponseWriter, r *http.Request) { + v := new(FirewallRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "POST") + if !reflect.DeepEqual(v, expectedFirewallRequest) { + t.Errorf("Request body = %+v, expected %+v", v, expectedFirewallRequest) + } + + var actualFirewallRequest *FirewallRequest + json.Unmarshal([]byte(firewallCreateJSONBody), &actualFirewallRequest) + if !reflect.DeepEqual(actualFirewallRequest, expectedFirewallRequest) { + t.Errorf("Request body = %+v, expected %+v", actualFirewallRequest, expectedFirewallRequest) + } + + fmt.Fprint(w, firewallJSONResponse) + }) + + actualFirewall, _, err := client.Firewalls.Create(ctx, expectedFirewallRequest) + if err != nil { + t.Errorf("Firewalls.Create returned error: %v", err) + } + + expectedFirewall := &Firewall{ + ID: "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", + Name: "f-i-r-e-w-a-l-l", + Status: "waiting", + InboundRules: []InboundRule{ + { + Protocol: "icmp", + PortRange: "0", + Sources: &Sources{ + Tags: []string{"frontend"}, + }, + }, + { + Protocol: "tcp", + PortRange: "8000-9000", + Sources: &Sources{ + Addresses: []string{"0.0.0.0/0"}, + }, + }, + }, + OutboundRules: []OutboundRule{ + { + Protocol: "icmp", + PortRange: "0", + }, + { + Protocol: "tcp", + PortRange: "8000-9000", + Destinations: &Destinations{ + Addresses: []string{"::/1"}, + }, + }, + }, + Created: "2017-04-06T13:07:27Z", + DropletIDs: []int{123}, + Tags: []string{"frontend"}, + PendingChanges: []PendingChange{ + { + DropletID: 123, + Removing: false, + Status: "waiting", + }, + }, + } + + if !reflect.DeepEqual(actualFirewall, expectedFirewall) { + t.Errorf("Firewalls.Create returned %+v, expected %+v", actualFirewall, expectedFirewall) + } +} + +func TestFirewalls_Update(t *testing.T) { + setup() + defer teardown() + + expectedFirewallRequest := &FirewallRequest{ + Name: "f-i-r-e-w-a-l-l", + InboundRules: []InboundRule{ + { + Protocol: "tcp", + PortRange: "443", + Sources: &Sources{ + Addresses: []string{"10.0.0.0/8"}, + }, + }, + }, + DropletIDs: []int{123}, + Tags: []string{}, + } + + urlStr := "/v2/firewalls" + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr = path.Join(urlStr, fID) + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + v := new(FirewallRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "PUT") + if !reflect.DeepEqual(v, expectedFirewallRequest) { + t.Errorf("Request body = %+v, expected %+v", v, expectedFirewallRequest) + } + + var actualFirewallRequest *FirewallRequest + json.Unmarshal([]byte(firewallUpdateJSONBody), &actualFirewallRequest) + if !reflect.DeepEqual(actualFirewallRequest, expectedFirewallRequest) { + t.Errorf("Request body = %+v, expected %+v", actualFirewallRequest, expectedFirewallRequest) + } + + fmt.Fprint(w, firewallUpdateJSONResponse) + }) + + actualFirewall, _, err := client.Firewalls.Update(ctx, fID, expectedFirewallRequest) + if err != nil { + t.Errorf("Firewalls.Update returned error: %v", err) + } + + expectedFirewall := &Firewall{ + ID: "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", + Name: "f-i-r-e-w-a-l-l", + InboundRules: []InboundRule{ + { + Protocol: "tcp", + PortRange: "443", + Sources: &Sources{ + Addresses: []string{"10.0.0.0/8"}, + }, + }, + }, + OutboundRules: []OutboundRule{}, + Created: "2017-04-06T13:07:27Z", + DropletIDs: []int{123}, + Tags: []string{}, + } + + if !reflect.DeepEqual(actualFirewall, expectedFirewall) { + t.Errorf("Firewalls.Update returned %+v, expected %+v", actualFirewall, expectedFirewall) + } +} + +func TestFirewalls_Delete(t *testing.T) { + setup() + defer teardown() + + urlStr := "/v2/firewalls" + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr = path.Join(urlStr, fID) + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Firewalls.Delete(ctx, fID) + + if err != nil { + t.Errorf("Firewalls.Delete returned error: %v", err) + } +} + +func TestFirewalls_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/firewalls", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, firewallListJSONResponse) + }) + + actualFirewalls, _, err := client.Firewalls.List(ctx, nil) + + if err != nil { + t.Errorf("Firewalls.List returned error: %v", err) + } + + expectedFirewalls := makeExpectedFirewalls() + if !reflect.DeepEqual(actualFirewalls, expectedFirewalls) { + t.Errorf("Firewalls.List returned %+v, expected %+v", actualFirewalls, expectedFirewalls) + } +} + +func TestFirewalls_ListByDroplet(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/droplets/123/firewalls", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, firewallListJSONResponse) + }) + + actualFirewalls, _, err := client.Firewalls.ListByDroplet(ctx, 123, nil) + + if err != nil { + t.Errorf("Firewalls.List returned error: %v", err) + } + + expectedFirewalls := makeExpectedFirewalls() + if !reflect.DeepEqual(actualFirewalls, expectedFirewalls) { + t.Errorf("Firewalls.List returned %+v, expected %+v", actualFirewalls, expectedFirewalls) + } +} + +func TestFirewalls_AddDroplets(t *testing.T) { + setup() + defer teardown() + + dRequest := &dropletsRequest{ + IDs: []int{123}, + } + + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr := path.Join("/v2/firewalls", fID, "droplets") + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + v := new(dropletsRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "POST") + if !reflect.DeepEqual(v, dRequest) { + t.Errorf("Request body = %+v, expected %+v", v, dRequest) + } + + expectedJSONBody := `{"droplet_ids": [123]}` + var actualDropletsRequest *dropletsRequest + json.Unmarshal([]byte(expectedJSONBody), &actualDropletsRequest) + if !reflect.DeepEqual(actualDropletsRequest, dRequest) { + t.Errorf("Request body = %+v, expected %+v", actualDropletsRequest, dRequest) + } + + fmt.Fprint(w, nil) + }) + + _, err := client.Firewalls.AddDroplets(ctx, fID, dRequest.IDs...) + + if err != nil { + t.Errorf("Firewalls.AddDroplets returned error: %v", err) + } +} + +func TestFirewalls_RemoveDroplets(t *testing.T) { + setup() + defer teardown() + + dRequest := &dropletsRequest{ + IDs: []int{123, 345}, + } + + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr := path.Join("/v2/firewalls", fID, "droplets") + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + v := new(dropletsRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "DELETE") + if !reflect.DeepEqual(v, dRequest) { + t.Errorf("Request body = %+v, expected %+v", v, dRequest) + } + + expectedJSONBody := `{"droplet_ids": [123, 345]}` + var actualDropletsRequest *dropletsRequest + json.Unmarshal([]byte(expectedJSONBody), &actualDropletsRequest) + if !reflect.DeepEqual(actualDropletsRequest, dRequest) { + t.Errorf("Request body = %+v, expected %+v", actualDropletsRequest, dRequest) + } + + fmt.Fprint(w, nil) + }) + + _, err := client.Firewalls.RemoveDroplets(ctx, fID, dRequest.IDs...) + + if err != nil { + t.Errorf("Firewalls.RemoveDroplets returned error: %v", err) + } +} + +func TestFirewalls_AddTags(t *testing.T) { + setup() + defer teardown() + + tRequest := &tagsRequest{ + Tags: []string{"frontend"}, + } + + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr := path.Join("/v2/firewalls", fID, "tags") + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + v := new(tagsRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "POST") + if !reflect.DeepEqual(v, tRequest) { + t.Errorf("Request body = %+v, expected %+v", v, tRequest) + } + + var actualTagsRequest *tagsRequest + json.Unmarshal([]byte(`{"tags": ["frontend"]}`), &actualTagsRequest) + if !reflect.DeepEqual(actualTagsRequest, tRequest) { + t.Errorf("Request body = %+v, expected %+v", actualTagsRequest, tRequest) + } + + fmt.Fprint(w, nil) + }) + + _, err := client.Firewalls.AddTags(ctx, fID, tRequest.Tags...) + + if err != nil { + t.Errorf("Firewalls.AddTags returned error: %v", err) + } +} + +func TestFirewalls_RemoveTags(t *testing.T) { + setup() + defer teardown() + + tRequest := &tagsRequest{ + Tags: []string{"frontend", "backend"}, + } + + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr := path.Join("/v2/firewalls", fID, "tags") + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + v := new(tagsRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "DELETE") + if !reflect.DeepEqual(v, tRequest) { + t.Errorf("Request body = %+v, expected %+v", v, tRequest) + } + + var actualTagsRequest *tagsRequest + json.Unmarshal([]byte(`{"tags": ["frontend", "backend"]}`), &actualTagsRequest) + if !reflect.DeepEqual(actualTagsRequest, tRequest) { + t.Errorf("Request body = %+v, expected %+v", actualTagsRequest, tRequest) + } + + fmt.Fprint(w, nil) + }) + + _, err := client.Firewalls.RemoveTags(ctx, fID, tRequest.Tags...) + + if err != nil { + t.Errorf("Firewalls.RemoveTags returned error: %v", err) + } +} + +func TestFirewalls_AddRules(t *testing.T) { + setup() + defer teardown() + + rr := &FirewallRulesRequest{ + InboundRules: []InboundRule{ + { + Protocol: "tcp", + PortRange: "22", + Sources: &Sources{ + Addresses: []string{"0.0.0.0/0"}, + }, + }, + }, + OutboundRules: []OutboundRule{ + { + Protocol: "tcp", + PortRange: "443", + Destinations: &Destinations{ + Addresses: []string{"0.0.0.0/0"}, + }, + }, + }, + } + + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr := path.Join("/v2/firewalls", fID, "rules") + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + v := new(FirewallRulesRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "POST") + if !reflect.DeepEqual(v, rr) { + t.Errorf("Request body = %+v, expected %+v", v, rr) + } + + var actualFirewallRulesRequest *FirewallRulesRequest + json.Unmarshal([]byte(firewallRulesJSONBody), &actualFirewallRulesRequest) + if !reflect.DeepEqual(actualFirewallRulesRequest, rr) { + t.Errorf("Request body = %+v, expected %+v", actualFirewallRulesRequest, rr) + } + + fmt.Fprint(w, nil) + }) + + _, err := client.Firewalls.AddRules(ctx, fID, rr) + + if err != nil { + t.Errorf("Firewalls.AddRules returned error: %v", err) + } +} + +func TestFirewalls_RemoveRules(t *testing.T) { + setup() + defer teardown() + + rr := &FirewallRulesRequest{ + InboundRules: []InboundRule{ + { + Protocol: "tcp", + PortRange: "22", + Sources: &Sources{ + Addresses: []string{"0.0.0.0/0"}, + }, + }, + }, + OutboundRules: []OutboundRule{ + { + Protocol: "tcp", + PortRange: "443", + Destinations: &Destinations{ + Addresses: []string{"0.0.0.0/0"}, + }, + }, + }, + } + + fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" + urlStr := path.Join("/v2/firewalls", fID, "rules") + mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { + v := new(FirewallRulesRequest) + err := json.NewDecoder(r.Body).Decode(v) + if err != nil { + t.Fatal(err) + } + + testMethod(t, r, "DELETE") + if !reflect.DeepEqual(v, rr) { + t.Errorf("Request body = %+v, expected %+v", v, rr) + } + + var actualFirewallRulesRequest *FirewallRulesRequest + json.Unmarshal([]byte(firewallRulesJSONBody), &actualFirewallRulesRequest) + if !reflect.DeepEqual(actualFirewallRulesRequest, rr) { + t.Errorf("Request body = %+v, expected %+v", actualFirewallRulesRequest, rr) + } + + fmt.Fprint(w, nil) + }) + + _, err := client.Firewalls.RemoveRules(ctx, fID, rr) + + if err != nil { + t.Errorf("Firewalls.RemoveRules returned error: %v", err) + } +} + +func makeExpectedFirewalls() []Firewall { + return []Firewall{ + Firewall{ + ID: "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", + Name: "f-i-r-e-w-a-l-l", + InboundRules: []InboundRule{ + { + Protocol: "icmp", + PortRange: "0", + Sources: &Sources{ + Tags: []string{"frontend"}, + }, + }, + { + Protocol: "tcp", + PortRange: "8000-9000", + Sources: &Sources{ + Addresses: []string{"0.0.0.0/0"}, + }, + }, + }, + OutboundRules: []OutboundRule{ + { + Protocol: "icmp", + PortRange: "0", + }, + { + Protocol: "tcp", + PortRange: "8000-9000", + Destinations: &Destinations{ + Addresses: []string{"::/1"}, + }, + }, + }, + DropletIDs: []int{123}, + Tags: []string{"frontend"}, + Created: "2017-04-06T13:07:27Z", + }, + } +} diff --git a/godo.go b/godo.go index 5c87c8c0..9dfef2a3 100644 --- a/godo.go +++ b/godo.go @@ -19,7 +19,7 @@ import ( ) const ( - libraryVersion = "1.0.0" + libraryVersion = "1.1.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" @@ -63,6 +63,7 @@ type Client struct { Tags TagsService LoadBalancers LoadBalancersService Certificates CertificatesService + Firewalls FirewallsService // Optional function called after every successful request made to the DO APIs onRequestCompleted RequestCompletionCallback @@ -173,6 +174,7 @@ func NewClient(httpClient *http.Client) *Client { c.Tags = &TagsServiceOp{client: c} c.LoadBalancers = &LoadBalancersServiceOp{client: c} c.Certificates = &CertificatesServiceOp{client: c} + c.Firewalls = &FirewallsServiceOp{client: c} return c }