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

Patching firewall rules #216

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6aa8442
Add single firewall rule resource
tbroden84 Aug 1, 2023
edc97d9
Check if "rules" have any configuration changes.
tbroden84 Aug 1, 2023
e0e69d1
Add security firewall as a data source
tbroden84 Aug 1, 2023
70c7a26
WIP: Patch multiple firewall rules
tbroden84 Aug 1, 2023
f5558c4
PATCH: limit which rule to be used from Read
tbroden84 Aug 3, 2023
8aa9fd7
PATCH: append rules to current firewall rules
tbroden84 Aug 3, 2023
342bb1f
PATCH: Format rules that should be deleted
tbroden84 Aug 3, 2023
ec7654e
PATCH: Update or remove firewall rules
tbroden84 Aug 3, 2023
1b9b21e
Remove firewall rule and rules test resources
tbroden84 Aug 3, 2023
e066ab5
Change name on 'replace' to 'patch' and change behaviour
tbroden84 Aug 7, 2023
cd2d351
Update docs
tbroden84 Aug 7, 2023
62cfa6f
Change schema type to populate the rules
tbroden84 Aug 7, 2023
2b1d0ea
Cleanup
tbroden84 Aug 7, 2023
477b4ed
Update docs
tbroden84 Aug 7, 2023
4c61f50
Update docs
tbroden84 Aug 7, 2023
b192440
Guard against unapplied firewall rules
tbroden84 Aug 8, 2023
001b4c0
Randomize sleep in ms, to avoid fire of action in parallel.
tbroden84 Aug 8, 2023
249be39
Use input paramter
tbroden84 Aug 8, 2023
8128add
Merge branch 'main' into single-firewall-rule
tbroden84 Aug 11, 2023
fd34231
Merge branch 'main' into single-firewall-rule
tbroden84 Sep 4, 2023
ed9e386
Remove random sleep, backend should take care of this
tbroden84 Sep 4, 2023
3a81f84
Merge branch 'main' into single-firewall-rule
tbroden84 Sep 11, 2023
8b30c8f
Merge branch 'main' into single-firewall-rule
tbroden84 Sep 19, 2023
22f369e
Avoid negative conditionals
tbroden84 Sep 19, 2023
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
81 changes: 81 additions & 0 deletions cloudamqp/data_source_cloudamqp_security_firewall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package cloudamqp

import (
"fmt"
"log"
"strconv"

"github.com/84codes/go-api/api"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func datasourceSecurityFirewall() *schema.Resource {
return &schema.Resource{
Read: datasourceSecurityFirewallRead,
Schema: map[string]*schema.Schema{
"instance_id": {
Type: schema.TypeInt,
Required: true,
Description: "Instance identifier",
},
"rules": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"services": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Description: "Pre-defined services 'AMQP', 'AMQPS', 'HTTPS', 'MQTT', 'MQTTS', 'STOMP', 'STOMPS', " +
"'STREAM', 'STREAM_SSL'",
},
"ports": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeInt,
},
Description: "Custom ports between 0 - 65554",
},
"ip": {
Type: schema.TypeString,
Computed: true,
Description: "CIDR address: IP address with CIDR notation (e.g. 10.56.72.0/24)",
},
"description": {
Type: schema.TypeString,
Computed: true,
Description: "Naming descripton e.g. 'Default'",
},
},
},
},
},
}
}

func datasourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) error {
var (
api = meta.(*api.API)
instanceID = d.Get("instance_id").(int)
rules []map[string]interface{}
)

data, err := api.ReadFirewallSettings(instanceID)
if err != nil {
return err
}
d.SetId(strconv.Itoa(instanceID))
for _, v := range data {
rules = append(rules, readRule(v))
}
log.Printf("[DEBUG] data-cloudamqp-security-firewall appended rules: %v", rules)
if err = d.Set("rules", rules); err != nil {
return fmt.Errorf("error setting rules for resource %s, %s", d.Id(), err)
}

return nil
}
14 changes: 14 additions & 0 deletions cloudamqp/extra.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cloudamqp

import (
"log"
"math/rand"
"time"
)

func randomSleep(ms int, name string) {
rand.Seed(time.Now().UnixNano())
n := rand.Intn(ms)
log.Printf("[DEBUG] %s sleep for %d ms...\n", name, n)
time.Sleep(time.Duration(n) * time.Millisecond)
}
1 change: 1 addition & 0 deletions cloudamqp/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func Provider(v string) *schema.Provider {
"cloudamqp_notification": dataSourceNotification(),
"cloudamqp_plugins_community": dataSourcePluginsCommunity(),
"cloudamqp_plugins": dataSourcePlugins(),
"cloudamqp_security_firewall": datasourceSecurityFirewall(),
"cloudamqp_upgradable_versions": dataSourceUpgradableVersions(),
"cloudamqp_vpc_gcp_info": dataSourceVpcGcpInfo(),
"cloudamqp_vpc_info": dataSourceVpcInfo(),
Expand Down
149 changes: 111 additions & 38 deletions cloudamqp/resource_cloudamqp_security_firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
Update: resourceSecurityFirewallUpdate,
Delete: resourceSecurityFirewallDelete,
Importer: &schema.ResourceImporter{
// Can only import all rules
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
Expand All @@ -32,6 +33,12 @@
ForceNew: true,
Description: "Instance identifier",
},
"patch": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Patch firewall rules instead of replacing them",
},
"rules": {
Type: schema.TypeSet,
Required: true,
Expand Down Expand Up @@ -102,42 +109,61 @@
}

func resourceSecurityFirewallCreate(d *schema.ResourceData, meta interface{}) error {
api := meta.(*api.API)
var params []map[string]interface{}
localFirewalls := d.Get("rules").(*schema.Set).List()
log.Printf("[DEBUG] cloudamqp::resource::security_firewall::create localFirewalls: %v", localFirewalls)
var (
api = meta.(*api.API)
instanceID = d.Get("instance_id").(int)
localFirewalls = d.Get("rules").(*schema.Set).List()
patch = d.Get("patch").(bool)
params []map[string]interface{}
sleep = d.Get("sleep").(int)
timeout = d.Get("timeout").(int)
err error
)

d.SetId(strconv.Itoa(instanceID))
for _, k := range localFirewalls {
params = append(params, k.(map[string]interface{}))
}

instanceID := d.Get("instance_id").(int)
log.Printf("[DEBUG] cloudamqp::resource::security_firewall::create instance id: %v", instanceID)
data, err := api.CreateFirewallSettings(instanceID, params, d.Get("sleep").(int), d.Get("timeout").(int))
if patch {
err = api.PatchFirewallSettings(instanceID, params, sleep, timeout)

Check failure on line 129 in cloudamqp/resource_cloudamqp_security_firewall.go

View workflow job for this annotation

GitHub Actions / build

api.PatchFirewallSettings undefined (type *"github.com/84codes/go-api/api".API has no field or method PatchFirewallSettings)
} else {
err = api.CreateFirewallSettings(instanceID, params, sleep, timeout)

Check failure on line 131 in cloudamqp/resource_cloudamqp_security_firewall.go

View workflow job for this annotation

GitHub Actions / build

assignment mismatch: 1 variable but api.CreateFirewallSettings returns 2 values
}

if err != nil {
return fmt.Errorf("error setting security firewall for resource %s: %s", d.Id(), err)
}
d.SetId(strconv.Itoa(instanceID))
log.Printf("[DEBUG] cloudamqp::resource::security_firewall::create id set: %v", d.Id())
d.Set("rules", data)

return nil
}

func resourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) error {
api := meta.(*api.API)
instanceID, _ := strconv.Atoi(d.Id())
log.Printf("[DEBUG] cloudamqp::resource::security_firewall::read instance id: %v", instanceID)
var (
api = meta.(*api.API)
instanceID, _ = strconv.Atoi(d.Id()) // Needed for import
patch = d.Get("patch").(bool)
rules []map[string]interface{}
)

d.Set("instance_id", instanceID)
data, err := api.ReadFirewallSettings(instanceID)
log.Printf("[DEBUG] cloudamqp::resource::security_firewall::read data: %v", data)
if err != nil {
return err
}
d.Set("instance_id", instanceID)
rules := make([]map[string]interface{}, len(data))
for k, v := range data {
rules[k] = readRule(v)
log.Printf("[DEBUG] Read firewall rules: %v", data)

if patch {
for _, v := range data {
if d.Get("rules").(*schema.Set).Contains(v) {
rules = append(rules, readRule(v))
}
}
} else {
for _, v := range data {
rules = append(rules, readRule(v))
}
}

log.Printf("[DEBUG] cloudamqp::resource::security_firewall::read rules: %v", rules)
if err = d.Set("rules", rules); err != nil {
return fmt.Errorf("error setting rules for resource %s, %s", d.Id(), err)
Expand All @@ -147,37 +173,84 @@
}

func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) error {
api := meta.(*api.API)
var params []map[string]interface{}
localFirewalls := d.Get("rules").(*schema.Set).List()
for _, k := range localFirewalls {
params = append(params, k.(map[string]interface{}))
}
log.Printf("[DEBUG] cloudamqp::resource::security_firewall::update instance id: %v, params: %v", d.Get("instance_id"), params)
data, err := api.UpdateFirewallSettings(d.Get("instance_id").(int), params, d.Get("sleep").(int), d.Get("timeout").(int))
if err != nil {
return err
var (
api = meta.(*api.API)
instanceID = d.Get("instance_id").(int)
patch = d.Get("patch").(bool)
rules []map[string]interface{}
sleep = d.Get("sleep").(int)
timeout = d.Get("timeout").(int)
)

if !d.HasChange("rules") {
return nil
}
rules := make([]map[string]interface{}, len(data))
for k, v := range data {
rules[k] = readRule(v)

if patch {
// Patch rules: Determine the difference between old and new sets
// Check which rules that should be deleted and which should be updated
oldRules, newRules := d.GetChange("rules")
deleteRules := oldRules.(*schema.Set).Difference(newRules.(*schema.Set)).List()
log.Printf("[DEBUG] Update firewall, remove rules: %v", deleteRules)
for _, v := range deleteRules {
rule := v.(map[string]interface{})
rule["services"] = []string{}
rule["ports"] = []int{}
rules = append(rules, rule)
}

updateRules := newRules.(*schema.Set).Difference(oldRules.(*schema.Set)).List()
log.Printf("[DEBUG] Update firewall, patch rules: %v", updateRules)
for _, v := range updateRules {
rules = append(rules, readRule(v.(map[string]interface{})))
}

log.Printf("[DEBUG] Update firewall, rules: %v", rules)
return api.PatchFirewallSettings(instanceID, rules, sleep, timeout)

Check failure on line 209 in cloudamqp/resource_cloudamqp_security_firewall.go

View workflow job for this annotation

GitHub Actions / build

api.PatchFirewallSettings undefined (type *"github.com/84codes/go-api/api".API has no field or method PatchFirewallSettings)
}

if err = d.Set("rules", rules); err != nil {
return fmt.Errorf("error setting rules for resource %s, %s", d.Id(), err)
// Replace all rules
for _, k := range d.Get("rules").(*schema.Set).List() {
rules = append(rules, k.(map[string]interface{}))
}
return nil
log.Printf("[DEBUG] Firewall update instance id: %v, rules: %v", instanceID, rules)
return api.UpdateFirewallSettings(instanceID, rules, sleep, timeout)

Check failure on line 217 in cloudamqp/resource_cloudamqp_security_firewall.go

View workflow job for this annotation

GitHub Actions / build

too many return values
}

func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) error {
var (
api = meta.(*api.API)
instanceID = d.Get("instance_id").(int)
sleep = d.Get("sleep").(int)
timeout = d.Get("timeout").(int)
patch = d.Get("patch").(bool)
)

if enableFasterInstanceDestroy {
log.Printf("[DEBUG] cloudamqp::resource::security_firewall::delete skip calling backend.")
return nil
}

api := meta.(*api.API)
log.Printf("[DEBUG] cloudamqp::resource::security_firewall::delete instance id: %v", d.Get("instance_id"))
data, err := api.DeleteFirewallSettings(d.Get("instance_id").(int), d.Get("sleep").(int), d.Get("timeout").(int))
if patch {
// Set services and port to empty arrays, this will remove rules when patching.
var params []map[string]interface{}
localFirewalls := d.Get("rules").(*schema.Set).List()
log.Printf("[DEBUG] Delete firewall rules: %v", localFirewalls)
for _, k := range localFirewalls {
rule := k.(map[string]interface{})
rule["services"] = []string{}
rule["ports"] = []int{}
params = append(params, rule)
}
log.Printf("[DEBUG] Delete firewall params: %v", params)
if len(params) > 0 {
return api.PatchFirewallSettings(instanceID, params, sleep, timeout)

Check failure on line 247 in cloudamqp/resource_cloudamqp_security_firewall.go

View workflow job for this annotation

GitHub Actions / build

api.PatchFirewallSettings undefined (type *"github.com/84codes/go-api/api".API has no field or method PatchFirewallSettings)
}
return nil
}

// Remove firewall settings and set default 0.0.0.0/0 rule (found in go-api).
data, err := api.DeleteFirewallSettings(instanceID, sleep, timeout)
d.Set("rules", data)
return err
}
Expand Down
63 changes: 63 additions & 0 deletions docs/data-sources/security_firewall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
layout: "cloudamqp"
page_title: "CloudAMQP: data source cloudamqp_security_firewall"
description: |-
Get information of firewall rules
---

# cloudamqp_security_firewall

Use this data source to retrieve information about the firewall rules that are open.

## Example Usage

```hcl
data "cloudamqp_security_firewall" "firewall" {
instance_id = cloudamqp_instance.instance.id
}
```

## Argument reference

* `instance_id` - (Required) The CloudAMQP instance identifier.

## Attributes reference

All attributes reference are computed

* `rules` - An array of firewall rules, each `rules` block consists of the field documented below.

___

The `rules` block consists of:

* `ip` - CIDR address: IP address with CIDR notation (e.g. 10.56.72.0/24)
* `ports` - Custom ports opened
* `services` - Pre-defined service ports, see table below
* `description` - Description name of the rule. e.g. Default.

Pre-defined services for RabbitMQ:

| Service name | Port |
|--------------|-------|
| AMQP | 5672 |
| AMQPS | 5671 |
| HTTPS | 443 |
| MQTT | 1883 |
| MQTTS | 8883 |
| STOMP | 61613 |
| STOMPS | 61614 |
| STREAM | 5552 |
| STREAM_SSL | 5551 |

Pre-defined services for LavinMQ:

| Service name | Port |
|--------------|-------|
| AMQP | 5672 |
| AMQPS | 5671 |
| HTTPS | 443 |

## Dependency

This data source depends on CloudAMQP instance identifier, `cloudamqp_instance.instance.id`.
Loading
Loading