Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for cloud firewalls #131

Merged
merged 3 commits into from
Mar 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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+E42TnysNCUPdjciGhY=
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