Skip to content

Commit

Permalink
Merge pull request linode#131 from Jskobos/feature/cloud-firewalls
Browse files Browse the repository at this point in the history
Adds support for cloud firewalls
  • Loading branch information
phillc committed Mar 13, 2020
2 parents 492e1bd + 058fc0e commit 9a2e9a1
Show file tree
Hide file tree
Showing 9 changed files with 506 additions and 2 deletions.
3 changes: 3 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type Client struct {
DomainRecords *Resource
Domains *Resource
Events *Resource
Firewalls *Resource
IPAddresses *Resource
IPv6Pools *Resource
IPv6Ranges *Resource
Expand Down Expand Up @@ -257,6 +258,7 @@ func addResources(client *Client) {
domainRecordsName: NewResource(client, domainRecordsName, domainRecordsEndpoint, true, DomainRecord{}, DomainRecordsPagedResponse{}),
domainsName: NewResource(client, domainsName, domainsEndpoint, false, Domain{}, DomainsPagedResponse{}),
eventsName: NewResource(client, eventsName, eventsEndpoint, false, Event{}, EventsPagedResponse{}),
firewallsName: NewResource(client, firewallsName, firewallsEndpoint, false, Firewall{}, FirewallsPagedResponse{}),
imagesName: NewResource(client, imagesName, imagesEndpoint, false, Image{}, ImagesPagedResponse{}),
instanceConfigsName: NewResource(client, instanceConfigsName, instanceConfigsEndpoint, true, InstanceConfig{}, InstanceConfigsPagedResponse{}),
instanceDisksName: NewResource(client, instanceDisksName, instanceDisksEndpoint, true, InstanceDisk{}, InstanceDisksPagedResponse{}),
Expand Down Expand Up @@ -306,6 +308,7 @@ func addResources(client *Client) {
client.DomainRecords = resources[domainRecordsName]
client.Domains = resources[domainsName]
client.Events = resources[eventsName]
client.Firewalls = resources[firewallsName]
client.IPAddresses = resources[ipaddressesName]
client.IPv6Pools = resources[ipv6poolsName]
client.IPv6Ranges = resources[ipv6rangesName]
Expand Down
30 changes: 30 additions & 0 deletions firewall_rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package linodego

// NetworkProtocol enum type
type NetworkProtocol string

// NetworkProtocol enum values
const (
TCP NetworkProtocol = "TCP"
UDP NetworkProtocol = "UDP"
ICMP NetworkProtocol = "ALL"
)

// NetworkAddresses are arrays of ipv4 and v6 addresses
type NetworkAddresses struct {
IPv4 []string `json:"ipv4"`
IPv6 []string `json:"ipv6"`
}

// A FirewallRule is a whitelist of ports, protocols, and addresses for which traffic should be allowed.
type FirewallRule struct {
Ports string `json:"ports"`
Protocol NetworkProtocol `json:"protocol"`
Addresses NetworkAddresses `json:"addresses"`
}

// FirewallRuleSet is a pair of inbound and outbound rules that specify what network traffic should be allowed.
type FirewallRuleSet struct {
Inbound []FirewallRule `json:"inbound,omitempty"`
Outbound []FirewallRule `json:"outbound,omitempty"`
}
138 changes: 138 additions & 0 deletions firewalls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package linodego

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/linode/linodego/internal/parseabletime"
)

// FirewallStatus enum type
type FirewallStatus string

// FirewallStatus enums start with Firewall
const (
FirewallEnabled FirewallStatus = "enabled"
FirewallDisabled FirewallStatus = "disabled"
FirewallDeleted FirewallStatus = "deleted"
)

// A Firewall is a set of networking rules (iptables) applied to Devices with which it is associated
type Firewall struct {
ID int `json:"id"`
Label string `json:"label"`
Status FirewallStatus `json:"status"`
Tags []string `json:"tags,omitempty"`
Rules FirewallRuleSet `json:"rules"`
Created *time.Time `json:"-"`
Updated *time.Time `json:"-"`
}

// DevicesCreationOptions fields are used when adding devices during the Firewall creation process.
type DevicesCreationOptions struct {
Linodes []string `json:"linodes,omitempty"`
NodeBalancers []string `json:"nodebalancers,omitempty"`
}

// FirewallCreateOptions fields are those accepted by CreateFirewall
type FirewallCreateOptions struct {
Label string `json:"label,omitempty"`
Rules FirewallRuleSet `json:"rules"`
Tags []string `json:"tags,omitempty"`
Devices DevicesCreationOptions `json:"devices,omitempty"`
}

// UnmarshalJSON for Firewall responses
func (i *Firewall) UnmarshalJSON(b []byte) error {
type Mask Firewall

p := struct {
*Mask
Created *parseabletime.ParseableTime `json:"created"`
Updated *parseabletime.ParseableTime `json:"updated"`
}{
Mask: (*Mask)(i),
}

if err := json.Unmarshal(b, &p); err != nil {
return err
}

i.Created = (*time.Time)(p.Created)
i.Updated = (*time.Time)(p.Updated)

return nil
}

// FirewallsPagedResponse represents a Linode API response for listing of Cloud Firewalls
type FirewallsPagedResponse struct {
*PageOptions
Data []Firewall `json:"data"`
}

func (FirewallsPagedResponse) endpoint(c *Client) string {
endpoint, err := c.Firewalls.Endpoint()
if err != nil {
panic(err)
}
return endpoint
}

func (resp *FirewallsPagedResponse) appendData(r *FirewallsPagedResponse) {
resp.Data = append(resp.Data, r.Data...)
}

// ListFirewalls returns a paginated list of Cloud Firewalls
func (c *Client) ListFirewalls(ctx context.Context, opts *ListOptions) ([]Firewall, error) {
response := FirewallsPagedResponse{}

err := c.listHelper(ctx, &response, opts)

if err != nil {
return nil, err
}

return response.Data, nil
}

// CreateFirewall creates a single Firewall with at least one set of inbound or outbound rules
func (c *Client) CreateFirewall(ctx context.Context, createOpts FirewallCreateOptions) (*Firewall, error) {
var body string
e, err := c.Firewalls.Endpoint()
if err != nil {
return nil, err
}

req := c.R(ctx).SetResult(&Firewall{})

if bodyData, err := json.Marshal(createOpts); err == nil {
body = string(bodyData)
} else {
return nil, NewError(err)
}

r, err := coupleAPIErrors(req.
SetBody(body).
Post(e))

if err != nil {
return nil, err
}
return r.Result().(*Firewall), nil
}

// DeleteFirewall deletes a single Firewall with the provided ID
func (c *Client) DeleteFirewall(ctx context.Context, id int) error {
e, err := c.Firewalls.Endpoint()
if err != nil {
return err
}

req := c.R(ctx)

e = fmt.Sprintf("%s/%d", e, id)
_, err = coupleAPIErrors(req.Delete(e))
return err
}
3 changes: 1 addition & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42Tnys***REMOVED***UPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
6 changes: 6 additions & 0 deletions pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ func (c *Client) listHelper(ctx context.Context, i interface{}, opts *ListOption
results = r.Result().(*EventsPagedResponse).Results
v.appendData(r.Result().(*EventsPagedResponse))
}
case *FirewallsPagedResponse:
if r, err = coupleAPIErrors(req.SetResult(FirewallsPagedResponse{}).Get(v.endpoint(c))); err == nil {
pages = r.Result().(*FirewallsPagedResponse).Pages
results = r.Result().(*FirewallsPagedResponse).Results
v.appendData(r.Result().(*FirewallsPagedResponse))
}
case *LKEClustersPagedResponse:
if r, err = coupleAPIErrors(req.SetResult(LKEClustersPagedResponse{}).Get(v.endpoint(c))); err == nil {
pages = r.Result().(*LKEClustersPagedResponse).Pages
Expand Down
2 changes: 2 additions & 0 deletions resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
domainRecordsName = "records"
domainsName = "domains"
eventsName = "events"
firewallsName = "firewalls"
imagesName = "images"
instanceConfigsName = "configs"
instanceDisksName = "disks"
Expand Down Expand Up @@ -62,6 +63,7 @@ const (
domainRecordsEndpoint = "domains/{{ .ID }}/records"
domainsEndpoint = "domains"
eventsEndpoint = "account/events"
firewallsEndpoint = "networking/firewalls"
imagesEndpoint = "images"
instanceConfigsEndpoint = "linode/instances/{{ .ID }}/configs"
instanceDisksEndpoint = "linode/instances/{{ .ID }}/disks"
Expand Down
21 changes: 21 additions & 0 deletions test/integration/firewall_rules_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package integration

import (
"github.com/linode/linodego"
)

var (
testFirewallRule = linodego.FirewallRule{
Ports: "22",
Protocol: "TCP",
Addresses: linodego.NetworkAddresses{
IPv4: []string{"0.0.0.0/0"},
IPv6: []string{"::0/0"},
},
}

testFirewallRuleSet = linodego.FirewallRuleSet{
Inbound: []linodego.FirewallRule{testFirewallRule},
Outbound: []linodego.FirewallRule{testFirewallRule},
}
)
64 changes: 64 additions & 0 deletions test/integration/firewalls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package integration

import (
"context"
"testing"

"github.com/linode/linodego"
)

var (
testFirewallCreateOpts = linodego.FirewallCreateOptions{
Label: "label",
Rules: testFirewallRuleSet, // borrowed from firewall_rules.test.go
Tags: []string{"testing"},
}
)

// TestListFirewalls should return a paginated list of Firewalls
func TestListFirewalls(t *testing.T) {
client, _, teardown, err := setupFirewall(t, []firewallModifier{
func(createOpts *linodego.FirewallCreateOptions) {
createOpts.Label = randString(12, lowerBytes, digits) + "-linodego-testing"
},
}, "fixtures/TestListFirewalls")
if err != nil {
t.Error(err)
}
defer teardown()

result, err := client.ListFirewalls(context.Background(), nil)
if err != nil {
t.Errorf("Error listing Firewalls, expected struct, got error %v", err)
}

if len(result) == 0 {
t.Errorf("Expected a list of Firewalls, but got none: %v", err)
}
}

type firewallModifier func(*linodego.FirewallCreateOptions)

func setupFirewall(t *testing.T, firewallModifiers []firewallModifier, fixturesYaml string) (*linodego.Client, *linodego.Firewall, func(), error) {
t.Helper()
var fixtureTeardown func()
client, fixtureTeardown := createTestClient(t, fixturesYaml)

createOpts := testFirewallCreateOpts
for _, modifier := range firewallModifiers {
modifier(&createOpts)
}

firewall, err := client.CreateFirewall(context.Background(), createOpts)
if err != nil {
t.Errorf("Error creating Firewall, expected struct, got error %v", err)
}

teardown := func() {
if err := client.DeleteFirewall(context.Background(), firewall.ID); err != nil {
t.Errorf("Expected to delete a Firewall, but got %v", err)
}
fixtureTeardown()
}
return client, firewall, teardown, err
}
Loading

0 comments on commit 9a2e9a1

Please sign in to comment.