Skip to content

Commit 6e645f8

Browse files
authored
Merge pull request #131 from Jskobos/feature/cloud-firewalls
Adds support for cloud firewalls
2 parents 413af6d + 90fe5f3 commit 6e645f8

File tree

9 files changed

+506
-2
lines changed

9 files changed

+506
-2
lines changed

client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type Client struct {
5757
DomainRecords *Resource
5858
Domains *Resource
5959
Events *Resource
60+
Firewalls *Resource
6061
IPAddresses *Resource
6162
IPv6Pools *Resource
6263
IPv6Ranges *Resource
@@ -257,6 +258,7 @@ func addResources(client *Client) {
257258
domainRecordsName: NewResource(client, domainRecordsName, domainRecordsEndpoint, true, DomainRecord{}, DomainRecordsPagedResponse{}),
258259
domainsName: NewResource(client, domainsName, domainsEndpoint, false, Domain{}, DomainsPagedResponse{}),
259260
eventsName: NewResource(client, eventsName, eventsEndpoint, false, Event{}, EventsPagedResponse{}),
261+
firewallsName: NewResource(client, firewallsName, firewallsEndpoint, false, Firewall{}, FirewallsPagedResponse{}),
260262
imagesName: NewResource(client, imagesName, imagesEndpoint, false, Image{}, ImagesPagedResponse{}),
261263
instanceConfigsName: NewResource(client, instanceConfigsName, instanceConfigsEndpoint, true, InstanceConfig{}, InstanceConfigsPagedResponse{}),
262264
instanceDisksName: NewResource(client, instanceDisksName, instanceDisksEndpoint, true, InstanceDisk{}, InstanceDisksPagedResponse{}),
@@ -306,6 +308,7 @@ func addResources(client *Client) {
306308
client.DomainRecords = resources[domainRecordsName]
307309
client.Domains = resources[domainsName]
308310
client.Events = resources[eventsName]
311+
client.Firewalls = resources[firewallsName]
309312
client.IPAddresses = resources[ipaddressesName]
310313
client.IPv6Pools = resources[ipv6poolsName]
311314
client.IPv6Ranges = resources[ipv6rangesName]

firewall_rules.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package linodego
2+
3+
// NetworkProtocol enum type
4+
type NetworkProtocol string
5+
6+
// NetworkProtocol enum values
7+
const (
8+
TCP NetworkProtocol = "TCP"
9+
UDP NetworkProtocol = "UDP"
10+
ICMP NetworkProtocol = "ALL"
11+
)
12+
13+
// NetworkAddresses are arrays of ipv4 and v6 addresses
14+
type NetworkAddresses struct {
15+
IPv4 []string `json:"ipv4"`
16+
IPv6 []string `json:"ipv6"`
17+
}
18+
19+
// A FirewallRule is a whitelist of ports, protocols, and addresses for which traffic should be allowed.
20+
type FirewallRule struct {
21+
Ports string `json:"ports"`
22+
Protocol NetworkProtocol `json:"protocol"`
23+
Addresses NetworkAddresses `json:"addresses"`
24+
}
25+
26+
// FirewallRuleSet is a pair of inbound and outbound rules that specify what network traffic should be allowed.
27+
type FirewallRuleSet struct {
28+
Inbound []FirewallRule `json:"inbound,omitempty"`
29+
Outbound []FirewallRule `json:"outbound,omitempty"`
30+
}

firewalls.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package linodego
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"time"
8+
9+
"github.com/linode/linodego/internal/parseabletime"
10+
)
11+
12+
// FirewallStatus enum type
13+
type FirewallStatus string
14+
15+
// FirewallStatus enums start with Firewall
16+
const (
17+
FirewallEnabled FirewallStatus = "enabled"
18+
FirewallDisabled FirewallStatus = "disabled"
19+
FirewallDeleted FirewallStatus = "deleted"
20+
)
21+
22+
// A Firewall is a set of networking rules (iptables) applied to Devices with which it is associated
23+
type Firewall struct {
24+
ID int `json:"id"`
25+
Label string `json:"label"`
26+
Status FirewallStatus `json:"status"`
27+
Tags []string `json:"tags,omitempty"`
28+
Rules FirewallRuleSet `json:"rules"`
29+
Created *time.Time `json:"-"`
30+
Updated *time.Time `json:"-"`
31+
}
32+
33+
// DevicesCreationOptions fields are used when adding devices during the Firewall creation process.
34+
type DevicesCreationOptions struct {
35+
Linodes []string `json:"linodes,omitempty"`
36+
NodeBalancers []string `json:"nodebalancers,omitempty"`
37+
}
38+
39+
// FirewallCreateOptions fields are those accepted by CreateFirewall
40+
type FirewallCreateOptions struct {
41+
Label string `json:"label,omitempty"`
42+
Rules FirewallRuleSet `json:"rules"`
43+
Tags []string `json:"tags,omitempty"`
44+
Devices DevicesCreationOptions `json:"devices,omitempty"`
45+
}
46+
47+
// UnmarshalJSON for Firewall responses
48+
func (i *Firewall) UnmarshalJSON(b []byte) error {
49+
type Mask Firewall
50+
51+
p := struct {
52+
*Mask
53+
Created *parseabletime.ParseableTime `json:"created"`
54+
Updated *parseabletime.ParseableTime `json:"updated"`
55+
}{
56+
Mask: (*Mask)(i),
57+
}
58+
59+
if err := json.Unmarshal(b, &p); err != nil {
60+
return err
61+
}
62+
63+
i.Created = (*time.Time)(p.Created)
64+
i.Updated = (*time.Time)(p.Updated)
65+
66+
return nil
67+
}
68+
69+
// FirewallsPagedResponse represents a Linode API response for listing of Cloud Firewalls
70+
type FirewallsPagedResponse struct {
71+
*PageOptions
72+
Data []Firewall `json:"data"`
73+
}
74+
75+
func (FirewallsPagedResponse) endpoint(c *Client) string {
76+
endpoint, err := c.Firewalls.Endpoint()
77+
if err != nil {
78+
panic(err)
79+
}
80+
return endpoint
81+
}
82+
83+
func (resp *FirewallsPagedResponse) appendData(r *FirewallsPagedResponse) {
84+
resp.Data = append(resp.Data, r.Data...)
85+
}
86+
87+
// ListFirewalls returns a paginated list of Cloud Firewalls
88+
func (c *Client) ListFirewalls(ctx context.Context, opts *ListOptions) ([]Firewall, error) {
89+
response := FirewallsPagedResponse{}
90+
91+
err := c.listHelper(ctx, &response, opts)
92+
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
return response.Data, nil
98+
}
99+
100+
// CreateFirewall creates a single Firewall with at least one set of inbound or outbound rules
101+
func (c *Client) CreateFirewall(ctx context.Context, createOpts FirewallCreateOptions) (*Firewall, error) {
102+
var body string
103+
e, err := c.Firewalls.Endpoint()
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
req := c.R(ctx).SetResult(&Firewall{})
109+
110+
if bodyData, err := json.Marshal(createOpts); err == nil {
111+
body = string(bodyData)
112+
} else {
113+
return nil, NewError(err)
114+
}
115+
116+
r, err := coupleAPIErrors(req.
117+
SetBody(body).
118+
Post(e))
119+
120+
if err != nil {
121+
return nil, err
122+
}
123+
return r.Result().(*Firewall), nil
124+
}
125+
126+
// DeleteFirewall deletes a single Firewall with the provided ID
127+
func (c *Client) DeleteFirewall(ctx context.Context, id int) error {
128+
e, err := c.Firewalls.Endpoint()
129+
if err != nil {
130+
return err
131+
}
132+
133+
req := c.R(ctx)
134+
135+
e = fmt.Sprintf("%s/%d", e, id)
136+
_, err = coupleAPIErrors(req.Delete(e))
137+
return err
138+
}

go.sum

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8
1414
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
1515
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
1616
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
17+
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
1718
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
18-
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
19-
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
2019
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
2120
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
2221
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

pagination.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ func (c *Client) listHelper(ctx context.Context, i interface{}, opts *ListOption
114114
results = r.Result().(*EventsPagedResponse).Results
115115
v.appendData(r.Result().(*EventsPagedResponse))
116116
}
117+
case *FirewallsPagedResponse:
118+
if r, err = coupleAPIErrors(req.SetResult(FirewallsPagedResponse{}).Get(v.endpoint(c))); err == nil {
119+
pages = r.Result().(*FirewallsPagedResponse).Pages
120+
results = r.Result().(*FirewallsPagedResponse).Results
121+
v.appendData(r.Result().(*FirewallsPagedResponse))
122+
}
117123
case *LKEClustersPagedResponse:
118124
if r, err = coupleAPIErrors(req.SetResult(LKEClustersPagedResponse{}).Get(v.endpoint(c))); err == nil {
119125
pages = r.Result().(*LKEClustersPagedResponse).Pages

resources.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const (
1515
domainRecordsName = "records"
1616
domainsName = "domains"
1717
eventsName = "events"
18+
firewallsName = "firewalls"
1819
imagesName = "images"
1920
instanceConfigsName = "configs"
2021
instanceDisksName = "disks"
@@ -62,6 +63,7 @@ const (
6263
domainRecordsEndpoint = "domains/{{ .ID }}/records"
6364
domainsEndpoint = "domains"
6465
eventsEndpoint = "account/events"
66+
firewallsEndpoint = "networking/firewalls"
6567
imagesEndpoint = "images"
6668
instanceConfigsEndpoint = "linode/instances/{{ .ID }}/configs"
6769
instanceDisksEndpoint = "linode/instances/{{ .ID }}/disks"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package integration
2+
3+
import (
4+
"github.com/linode/linodego"
5+
)
6+
7+
var (
8+
testFirewallRule = linodego.FirewallRule{
9+
Ports: "22",
10+
Protocol: "TCP",
11+
Addresses: linodego.NetworkAddresses{
12+
IPv4: []string{"0.0.0.0/0"},
13+
IPv6: []string{"::0/0"},
14+
},
15+
}
16+
17+
testFirewallRuleSet = linodego.FirewallRuleSet{
18+
Inbound: []linodego.FirewallRule{testFirewallRule},
19+
Outbound: []linodego.FirewallRule{testFirewallRule},
20+
}
21+
)

test/integration/firewalls_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package integration
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/linode/linodego"
8+
)
9+
10+
var (
11+
testFirewallCreateOpts = linodego.FirewallCreateOptions{
12+
Label: "label",
13+
Rules: testFirewallRuleSet, // borrowed from firewall_rules.test.go
14+
Tags: []string{"testing"},
15+
}
16+
)
17+
18+
// TestListFirewalls should return a paginated list of Firewalls
19+
func TestListFirewalls(t *testing.T) {
20+
client, _, teardown, err := setupFirewall(t, []firewallModifier{
21+
func(createOpts *linodego.FirewallCreateOptions) {
22+
createOpts.Label = randString(12, lowerBytes, digits) + "-linodego-testing"
23+
},
24+
}, "fixtures/TestListFirewalls")
25+
if err != nil {
26+
t.Error(err)
27+
}
28+
defer teardown()
29+
30+
result, err := client.ListFirewalls(context.Background(), nil)
31+
if err != nil {
32+
t.Errorf("Error listing Firewalls, expected struct, got error %v", err)
33+
}
34+
35+
if len(result) == 0 {
36+
t.Errorf("Expected a list of Firewalls, but got none: %v", err)
37+
}
38+
}
39+
40+
type firewallModifier func(*linodego.FirewallCreateOptions)
41+
42+
func setupFirewall(t *testing.T, firewallModifiers []firewallModifier, fixturesYaml string) (*linodego.Client, *linodego.Firewall, func(), error) {
43+
t.Helper()
44+
var fixtureTeardown func()
45+
client, fixtureTeardown := createTestClient(t, fixturesYaml)
46+
47+
createOpts := testFirewallCreateOpts
48+
for _, modifier := range firewallModifiers {
49+
modifier(&createOpts)
50+
}
51+
52+
firewall, err := client.CreateFirewall(context.Background(), createOpts)
53+
if err != nil {
54+
t.Errorf("Error creating Firewall, expected struct, got error %v", err)
55+
}
56+
57+
teardown := func() {
58+
if err := client.DeleteFirewall(context.Background(), firewall.ID); err != nil {
59+
t.Errorf("Expected to delete a Firewall, but got %v", err)
60+
}
61+
fixtureTeardown()
62+
}
63+
return client, firewall, teardown, err
64+
}

0 commit comments

Comments
 (0)