From 6aa84426ecd60eed7bc80431f855fb7d89bc7601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Tue, 1 Aug 2023 09:44:15 +0200 Subject: [PATCH 01/20] Add single firewall rule resource --- cloudamqp/provider.go | 1 + ...source_cloudamqp_security_firewall_rule.go | 161 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 cloudamqp/resource_cloudamqp_security_firewall_rule.go diff --git a/cloudamqp/provider.go b/cloudamqp/provider.go index 41173877..67f18dea 100644 --- a/cloudamqp/provider.go +++ b/cloudamqp/provider.go @@ -65,6 +65,7 @@ func Provider(v string) *schema.Provider { "cloudamqp_privatelink_azure": resourcePrivateLinkAzure(), "cloudamqp_rabbitmq_configuration": resourceRabbitMqConfiguration(), "cloudamqp_security_firewall": resourceSecurityFirewall(), + "cloudamqp_security_firewall_rule": resourceSecurityFirewallRule(), "cloudamqp_upgrade_rabbitmq": resourceUpgradeRabbitMQ(), "cloudamqp_vpc_gcp_peering": resourceVpcGcpPeering(), "cloudamqp_vpc_peering": resourceVpcPeering(), diff --git a/cloudamqp/resource_cloudamqp_security_firewall_rule.go b/cloudamqp/resource_cloudamqp_security_firewall_rule.go new file mode 100644 index 00000000..058400d0 --- /dev/null +++ b/cloudamqp/resource_cloudamqp_security_firewall_rule.go @@ -0,0 +1,161 @@ +package cloudamqp + +import ( + "fmt" + "log" + "net" + + "github.com/84codes/go-api/api" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceSecurityFirewallRule() *schema.Resource { + return &schema.Resource{ + Create: resourceSecurityFirewallRulePatch, + Read: resourceSecurityFirewallRuleRead, + Update: resourceSecurityFirewallRulePatch, + Delete: resourceSecurityFirewallRuleDelete, + Schema: map[string]*schema.Schema{ + "instance_id": { + Type: schema.TypeInt, + Required: true, + Description: "Instance identifier", + }, + "ip": { + Type: schema.TypeString, + Required: true, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + _, _, err := net.ParseCIDR(v) + if err != nil { + errs = append(errs, fmt.Errorf("%v", err)) + } + return + }, + Description: "CIDR address: IP address with CIDR notation (e.g. 10.56.72.0/24)", + }, + "services": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateServices(), + }, + Description: "Pre-defined services 'AMQP', 'AMQPS', 'HTTPS', 'MQTT', 'MQTTS', 'STOMP', 'STOMPS', " + + "'STREAM', 'STREAM_SSL'", + }, + "ports": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(int) + if v < 0 || v > 65554 { + errs = append(errs, fmt.Errorf("%q must be between 0 and 65554, got: %d", key, v)) + } else if validateServicePort(v) { + warns = append(warns, fmt.Sprintf("Port %d found in \"ports\", needs to be added as %q in \"services\" instead", v, portToService(v))) + } + return + }, + }, + Description: "Custom ports between 0 - 65554", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Naming descripton e.g. 'Default'", + }, + "sleep": { + Type: schema.TypeInt, + Optional: true, + Default: 30, + Description: "Configurable sleep time in seconds between retries for firewall configuration", + }, + "timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 1800, + Description: "Configurable timeout time in seconds for firewall configuration", + }, + }, + } +} + +func resourceSecurityFirewallRulePatch(d *schema.ResourceData, meta interface{}) error { + var ( + api = meta.(*api.API) + keys = []string{"services", "ports", "ip", "description"} + rule = make(map[string]interface{}) + params []map[string]interface{} + instanceID = d.Get("instance_id").(int) + sleep = d.Get("sleep").(int) + timeout = d.Get("timeout").(int) + ) + + for _, k := range keys { + if v := d.Get(k); v != nil { + rule[k] = v + } + } + + params = append(params, rule) + err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) + if err != nil { + return fmt.Errorf("error setting security firewall for resource %s: %s", d.Id(), err) + } + + d.SetId(d.Get("ip").(string)) + return nil +} + +func resourceSecurityFirewallRuleRead(d *schema.ResourceData, meta interface{}) error { + var ( + api = meta.(*api.API) + ip = d.Get("ip").(string) + instanceID = d.Get("instance_id").(int) + ) + + data, err := api.ReadFirewallRule(instanceID, ip) + log.Printf("[DEBUG] security firewall rule: %v", data) + if err != nil { + return err + } + + for k, v := range data { + if v != nil { + d.Set(k, v) + } + } + + return nil +} + +func resourceSecurityFirewallRuleDelete(d *schema.ResourceData, meta interface{}) error { + var ( + api = meta.(*api.API) + instanceID = d.Get("instance_id").(int) + rule = make(map[string]interface{}) + params []map[string]interface{} + sleep = d.Get("sleep").(int) + timeout = d.Get("timeout").(int) + ) + + // Skip if faster instance destroy enabled + if enableFasterInstanceDestroy == true { + log.Printf("[DEBUG] cloudamqp::resource::security_firewall::delete skip calling backend.") + return nil + } + + // Only set ip with correct value to make the PATCH request remove the rule + rule["ip"] = d.Id() + rule["services"] = []string{} + rule["ports"] = []int{} + params = append(params, rule) + err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) + if err != nil { + return fmt.Errorf("failed to remove firewall rule for IP %s: %s", d.Id(), err) + } + + return nil +} From edc97d9517c005cae4404327b7622edc8594970e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Tue, 1 Aug 2023 12:34:58 +0200 Subject: [PATCH 02/20] Check if "rules" have any configuration changes. With resource data schema, check if rules have any changes that require API backend request. Otherwise just update state file. --- cloudamqp/resource_cloudamqp_security_firewall.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index 960410fd..5d97cdb7 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -146,9 +146,16 @@ func resourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) erro } func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) error { - api := meta.(*api.API) - var params []map[string]interface{} - localFirewalls := d.Get("rules").(*schema.Set).List() + var ( + api = meta.(*api.API) + params []map[string]interface{} + localFirewalls = d.Get("rules").(*schema.Set).List() + ) + + if !d.HasChange("rules") { + return nil + } + for _, k := range localFirewalls { params = append(params, k.(map[string]interface{})) } From e0e69d16da7793f6eee3826f06b03b14af92fc33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Tue, 1 Aug 2023 12:37:17 +0200 Subject: [PATCH 03/20] Add security firewall as a data source --- ...data_source_cloudamqp_security_firewall.go | 82 +++++++++++++++++++ cloudamqp/provider.go | 1 + 2 files changed, 83 insertions(+) create mode 100644 cloudamqp/data_source_cloudamqp_security_firewall.go diff --git a/cloudamqp/data_source_cloudamqp_security_firewall.go b/cloudamqp/data_source_cloudamqp_security_firewall.go new file mode 100644 index 00000000..fac14540 --- /dev/null +++ b/cloudamqp/data_source_cloudamqp_security_firewall.go @@ -0,0 +1,82 @@ +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.TypeSet, + 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)) + rules := make([]map[string]interface{}, len(data)) + for k, v := range data { + rules[k] = 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 +} diff --git a/cloudamqp/provider.go b/cloudamqp/provider.go index 67f18dea..a2e93a44 100644 --- a/cloudamqp/provider.go +++ b/cloudamqp/provider.go @@ -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(), From 70c7a26eb6e81ba8a2d771997bf7a30c05e90963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Tue, 1 Aug 2023 12:38:39 +0200 Subject: [PATCH 04/20] WIP: Patch multiple firewall rules --- cloudamqp/provider.go | 1 + ...ource_cloudamqp_security_firewall_rules.go | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 cloudamqp/resource_cloudamqp_security_firewall_rules.go diff --git a/cloudamqp/provider.go b/cloudamqp/provider.go index a2e93a44..d77c4039 100644 --- a/cloudamqp/provider.go +++ b/cloudamqp/provider.go @@ -67,6 +67,7 @@ func Provider(v string) *schema.Provider { "cloudamqp_rabbitmq_configuration": resourceRabbitMqConfiguration(), "cloudamqp_security_firewall": resourceSecurityFirewall(), "cloudamqp_security_firewall_rule": resourceSecurityFirewallRule(), + "cloudamqp_security_firewall_rules": resourceSecurityFirewallRules(), "cloudamqp_upgrade_rabbitmq": resourceUpgradeRabbitMQ(), "cloudamqp_vpc_gcp_peering": resourceVpcGcpPeering(), "cloudamqp_vpc_peering": resourceVpcPeering(), diff --git a/cloudamqp/resource_cloudamqp_security_firewall_rules.go b/cloudamqp/resource_cloudamqp_security_firewall_rules.go new file mode 100644 index 00000000..22a78caa --- /dev/null +++ b/cloudamqp/resource_cloudamqp_security_firewall_rules.go @@ -0,0 +1,192 @@ +package cloudamqp + +import ( + "fmt" + "log" + "net" + "strconv" + + "github.com/84codes/go-api/api" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceSecurityFirewallRules() *schema.Resource { + return &schema.Resource{ + Create: resourceSecurityFirewallRulesCreate, + Read: resourceSecurityFirewallRulesRead, + Update: resourceSecurityFirewallRulesUpdate, + Delete: resourceSecurityFirewallRulesDelete, + Schema: map[string]*schema.Schema{ + "instance_id": { + Type: schema.TypeInt, + Required: true, + Description: "Instance identifier", + }, + "rules": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "services": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateServices(), + }, + Description: "Pre-defined services 'AMQP', 'AMQPS', 'HTTPS', 'MQTT', 'MQTTS', 'STOMP', 'STOMPS', " + + "'STREAM', 'STREAM_SSL'", + }, + "ports": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(int) + if v < 0 || v > 65554 { + errs = append(errs, fmt.Errorf("%q must be between 0 and 65554, got: %d", key, v)) + } else if validateServicePort(v) { + warns = append(warns, fmt.Sprintf("Port %d found in \"ports\", needs to be added as %q in \"services\" instead", v, portToService(v))) + } + return + }, + }, + Description: "Custom ports between 0 - 65554", + }, + "ip": { + Type: schema.TypeString, + Required: true, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + _, _, err := net.ParseCIDR(v) + if err != nil { + errs = append(errs, fmt.Errorf("%v", err)) + } + return + }, + Description: "CIDR address: IP address with CIDR notation (e.g. 10.56.72.0/24)", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Naming descripton e.g. 'Default'", + }, + }, + }, + }, + "sleep": { + Type: schema.TypeInt, + Optional: true, + Default: 30, + Description: "Configurable sleep time in seconds between retries for firewall configuration", + }, + "timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 1800, + Description: "Configurable timeout time in seconds for firewall configuration", + }, + }, + } +} + +func resourceSecurityFirewallRulesCreate(d *schema.ResourceData, meta interface{}) error { + var ( + api = meta.(*api.API) + instanceID = d.Get("instance_id").(int) + localFirewalls = d.Get("rules").(*schema.Set).List() + params []map[string]interface{} + sleep = d.Get("sleep").(int) + timeout = d.Get("timeout").(int) + ) + + for _, k := range localFirewalls { + params = append(params, k.(map[string]interface{})) + } + + err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) + if err != nil { + return fmt.Errorf("error setting security firewall for resource %s: %s", d.Id(), err) + } + + d.SetId("na") + return nil +} + +func resourceSecurityFirewallRulesRead(d *schema.ResourceData, meta interface{}) error { + var ( + api = meta.(*api.API) + ip = d.Get("ip").(string) + instanceID = d.Get("instance_id").(int) + ) + + data, err := api.ReadFirewallRule(instanceID, ip) + log.Printf("[DEBUG] security firewall rule: %v", data) + if err != nil { + return err + } + + for k, v := range data { + if v != nil { + d.Set(k, v) + } + } + + return nil +} + +func resourceSecurityFirewallRulesUpdate(d *schema.ResourceData, meta interface{}) error { + var ( + api = meta.(*api.API) + instanceID = d.Get("instance_id").(int) + params []map[string]interface{} + sleep = d.Get("sleep").(int) + timeout = d.Get("timeout").(int) + ) + + // if d.HasChange("rules") { + // localFirewalls := d.Get("rules").(*schema.Set).List() + // for k, v := range localFirewalls { + + // } + + // } + + err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) + if err != nil { + return fmt.Errorf("error setting security firewall for resource %s: %s", d.Id(), err) + } + + d.SetId(strconv.Itoa(instanceID)) + return nil +} + +func resourceSecurityFirewallRulesDelete(d *schema.ResourceData, meta interface{}) error { + var ( + api = meta.(*api.API) + instanceID = d.Get("instance_id").(int) + rule = make(map[string]interface{}) + params []map[string]interface{} + sleep = d.Get("sleep").(int) + timeout = d.Get("timeout").(int) + ) + + // Skip if faster instance destroy enabled + if enableFasterInstanceDestroy == true { + log.Printf("[DEBUG] cloudamqp::resource::security_firewall::delete skip calling backend.") + return nil + } + + // Only set ip with correct value to make the PATCH request remove the rule + rule["ip"] = d.Id() + rule["services"] = []string{} + rule["ports"] = []int{} + params = append(params, rule) + err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) + if err != nil { + return fmt.Errorf("failed to remove firewall rule for IP %s: %s", d.Id(), err) + } + + return nil +} From f5558c4774c3558ced6a6eed6444e9305da626f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Thu, 3 Aug 2023 13:59:25 +0200 Subject: [PATCH 05/20] PATCH: limit which rule to be used from Read --- .../resource_cloudamqp_security_firewall.go | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index 5d97cdb7..84b1bf96 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -31,6 +31,12 @@ func resourceSecurityFirewall() *schema.Resource { Required: true, Description: "Instance identifier", }, + "replace": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Replace all firewall rules or append/update", + }, "rules": { Type: schema.TypeSet, Required: true, @@ -124,19 +130,35 @@ func resourceSecurityFirewallCreate(d *schema.ResourceData, meta interface{}) er } 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 + replace = d.Get("replace").(bool) + rules []map[string]interface{} + ) + + // Filter out rules that are present in localFirewalls... (d.Get("rules").(*schema.Set).List()) + 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 replace { + for _, v := range data { + rules = append(rules, readRule(v)) + } + } else { + // How to handle import? + // localRules := d.Get("rules").(*schema.Set).List() + for _, v := range data { + if d.Get("rules").(*schema.Set).Contains(v) { + 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) From 8aa9fd780294c87c5c4f153a9991042abd4ec9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Thu, 3 Aug 2023 14:00:25 +0200 Subject: [PATCH 06/20] PATCH: append rules to current firewall rules --- .../resource_cloudamqp_security_firewall.go | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index 84b1bf96..bbefe47e 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -107,25 +107,31 @@ func resourceSecurityFirewall() *schema.Resource { } 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() + replace = d.Get("replace").(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 replace { + err = api.CreateFirewallSettings(instanceID, params, sleep, timeout) + } else { + err = api.PatchFirewallSettings(instanceID, params, sleep, timeout) + } + 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 } From 342bb1f1820916dce6b082d3875a7f445c07212b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Thu, 3 Aug 2023 14:02:10 +0200 Subject: [PATCH 07/20] PATCH: Format rules that should be deleted --- .../resource_cloudamqp_security_firewall.go | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index bbefe47e..fca6d3ab 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -204,15 +204,36 @@ func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) er } 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) + replace = d.Get("replace").(bool) + ) + if enableFasterInstanceDestroy == true { 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)) - d.Set("rules", data) + if replace { + data, err := api.DeleteFirewallSettings(instanceID, sleep, timeout) + d.Set("rules", data) + return err + } + + 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) + err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) return err } From ec7654ef298a96ccad4a4fae3d1ecd5a261df794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Thu, 3 Aug 2023 14:02:47 +0200 Subject: [PATCH 08/20] PATCH: Update or remove firewall rules Get changes between old vs. new configuration with d.GetChanges("rules"). Then determine which rules should be removed or updated based on the difference between the two configurations. --- .../resource_cloudamqp_security_firewall.go | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index fca6d3ab..89db84dd 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -175,32 +175,44 @@ func resourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) erro func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) error { var ( - api = meta.(*api.API) - params []map[string]interface{} - localFirewalls = d.Get("rules").(*schema.Set).List() + api = meta.(*api.API) + instanceID = d.Get("instance_id").(int) + replace = d.Get("replace").(bool) + rules []map[string]interface{} + sleep = d.Get("sleep").(int) + timeout = d.Get("timeout").(int) ) if !d.HasChange("rules") { return nil } - 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 + if replace { + for _, k := range d.Get("rules").(*schema.Set).List() { + rules = append(rules, k.(map[string]interface{})) + } + log.Printf("[DEBUG] Firewall update instance id: %v, rules: %v", instanceID, rules) + return api.UpdateFirewallSettings(instanceID, rules, sleep, timeout) } - rules := make([]map[string]interface{}, len(data)) - for k, v := range data { - rules[k] = readRule(v) + + 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) } - if err = d.Set("rules", rules); err != nil { - return fmt.Errorf("error setting rules for resource %s, %s", d.Id(), err) + 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{}))) } - return nil + + log.Printf("[DEBUG] Update firewall, rules: %v", rules) + return api.PatchFirewallSettings(instanceID, rules, sleep, timeout) } func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) error { From 1b9b21eac644812cc28e8feb3ab660ef9391c378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Thu, 3 Aug 2023 14:13:08 +0200 Subject: [PATCH 09/20] Remove firewall rule and rules test resources --- cloudamqp/provider.go | 2 - ...source_cloudamqp_security_firewall_rule.go | 161 --------------- ...ource_cloudamqp_security_firewall_rules.go | 192 ------------------ 3 files changed, 355 deletions(-) delete mode 100644 cloudamqp/resource_cloudamqp_security_firewall_rule.go delete mode 100644 cloudamqp/resource_cloudamqp_security_firewall_rules.go diff --git a/cloudamqp/provider.go b/cloudamqp/provider.go index d77c4039..1733c533 100644 --- a/cloudamqp/provider.go +++ b/cloudamqp/provider.go @@ -66,8 +66,6 @@ func Provider(v string) *schema.Provider { "cloudamqp_privatelink_azure": resourcePrivateLinkAzure(), "cloudamqp_rabbitmq_configuration": resourceRabbitMqConfiguration(), "cloudamqp_security_firewall": resourceSecurityFirewall(), - "cloudamqp_security_firewall_rule": resourceSecurityFirewallRule(), - "cloudamqp_security_firewall_rules": resourceSecurityFirewallRules(), "cloudamqp_upgrade_rabbitmq": resourceUpgradeRabbitMQ(), "cloudamqp_vpc_gcp_peering": resourceVpcGcpPeering(), "cloudamqp_vpc_peering": resourceVpcPeering(), diff --git a/cloudamqp/resource_cloudamqp_security_firewall_rule.go b/cloudamqp/resource_cloudamqp_security_firewall_rule.go deleted file mode 100644 index 058400d0..00000000 --- a/cloudamqp/resource_cloudamqp_security_firewall_rule.go +++ /dev/null @@ -1,161 +0,0 @@ -package cloudamqp - -import ( - "fmt" - "log" - "net" - - "github.com/84codes/go-api/api" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" -) - -func resourceSecurityFirewallRule() *schema.Resource { - return &schema.Resource{ - Create: resourceSecurityFirewallRulePatch, - Read: resourceSecurityFirewallRuleRead, - Update: resourceSecurityFirewallRulePatch, - Delete: resourceSecurityFirewallRuleDelete, - Schema: map[string]*schema.Schema{ - "instance_id": { - Type: schema.TypeInt, - Required: true, - Description: "Instance identifier", - }, - "ip": { - Type: schema.TypeString, - Required: true, - ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { - v := val.(string) - _, _, err := net.ParseCIDR(v) - if err != nil { - errs = append(errs, fmt.Errorf("%v", err)) - } - return - }, - Description: "CIDR address: IP address with CIDR notation (e.g. 10.56.72.0/24)", - }, - "services": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validateServices(), - }, - Description: "Pre-defined services 'AMQP', 'AMQPS', 'HTTPS', 'MQTT', 'MQTTS', 'STOMP', 'STOMPS', " + - "'STREAM', 'STREAM_SSL'", - }, - "ports": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeInt, - ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { - v := val.(int) - if v < 0 || v > 65554 { - errs = append(errs, fmt.Errorf("%q must be between 0 and 65554, got: %d", key, v)) - } else if validateServicePort(v) { - warns = append(warns, fmt.Sprintf("Port %d found in \"ports\", needs to be added as %q in \"services\" instead", v, portToService(v))) - } - return - }, - }, - Description: "Custom ports between 0 - 65554", - }, - "description": { - Type: schema.TypeString, - Optional: true, - Description: "Naming descripton e.g. 'Default'", - }, - "sleep": { - Type: schema.TypeInt, - Optional: true, - Default: 30, - Description: "Configurable sleep time in seconds between retries for firewall configuration", - }, - "timeout": { - Type: schema.TypeInt, - Optional: true, - Default: 1800, - Description: "Configurable timeout time in seconds for firewall configuration", - }, - }, - } -} - -func resourceSecurityFirewallRulePatch(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - keys = []string{"services", "ports", "ip", "description"} - rule = make(map[string]interface{}) - params []map[string]interface{} - instanceID = d.Get("instance_id").(int) - sleep = d.Get("sleep").(int) - timeout = d.Get("timeout").(int) - ) - - for _, k := range keys { - if v := d.Get(k); v != nil { - rule[k] = v - } - } - - params = append(params, rule) - err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) - if err != nil { - return fmt.Errorf("error setting security firewall for resource %s: %s", d.Id(), err) - } - - d.SetId(d.Get("ip").(string)) - return nil -} - -func resourceSecurityFirewallRuleRead(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - ip = d.Get("ip").(string) - instanceID = d.Get("instance_id").(int) - ) - - data, err := api.ReadFirewallRule(instanceID, ip) - log.Printf("[DEBUG] security firewall rule: %v", data) - if err != nil { - return err - } - - for k, v := range data { - if v != nil { - d.Set(k, v) - } - } - - return nil -} - -func resourceSecurityFirewallRuleDelete(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - instanceID = d.Get("instance_id").(int) - rule = make(map[string]interface{}) - params []map[string]interface{} - sleep = d.Get("sleep").(int) - timeout = d.Get("timeout").(int) - ) - - // Skip if faster instance destroy enabled - if enableFasterInstanceDestroy == true { - log.Printf("[DEBUG] cloudamqp::resource::security_firewall::delete skip calling backend.") - return nil - } - - // Only set ip with correct value to make the PATCH request remove the rule - rule["ip"] = d.Id() - rule["services"] = []string{} - rule["ports"] = []int{} - params = append(params, rule) - err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) - if err != nil { - return fmt.Errorf("failed to remove firewall rule for IP %s: %s", d.Id(), err) - } - - return nil -} diff --git a/cloudamqp/resource_cloudamqp_security_firewall_rules.go b/cloudamqp/resource_cloudamqp_security_firewall_rules.go deleted file mode 100644 index 22a78caa..00000000 --- a/cloudamqp/resource_cloudamqp_security_firewall_rules.go +++ /dev/null @@ -1,192 +0,0 @@ -package cloudamqp - -import ( - "fmt" - "log" - "net" - "strconv" - - "github.com/84codes/go-api/api" - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" -) - -func resourceSecurityFirewallRules() *schema.Resource { - return &schema.Resource{ - Create: resourceSecurityFirewallRulesCreate, - Read: resourceSecurityFirewallRulesRead, - Update: resourceSecurityFirewallRulesUpdate, - Delete: resourceSecurityFirewallRulesDelete, - Schema: map[string]*schema.Schema{ - "instance_id": { - Type: schema.TypeInt, - Required: true, - Description: "Instance identifier", - }, - "rules": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "services": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validateServices(), - }, - Description: "Pre-defined services 'AMQP', 'AMQPS', 'HTTPS', 'MQTT', 'MQTTS', 'STOMP', 'STOMPS', " + - "'STREAM', 'STREAM_SSL'", - }, - "ports": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeInt, - ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { - v := val.(int) - if v < 0 || v > 65554 { - errs = append(errs, fmt.Errorf("%q must be between 0 and 65554, got: %d", key, v)) - } else if validateServicePort(v) { - warns = append(warns, fmt.Sprintf("Port %d found in \"ports\", needs to be added as %q in \"services\" instead", v, portToService(v))) - } - return - }, - }, - Description: "Custom ports between 0 - 65554", - }, - "ip": { - Type: schema.TypeString, - Required: true, - ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { - v := val.(string) - _, _, err := net.ParseCIDR(v) - if err != nil { - errs = append(errs, fmt.Errorf("%v", err)) - } - return - }, - Description: "CIDR address: IP address with CIDR notation (e.g. 10.56.72.0/24)", - }, - "description": { - Type: schema.TypeString, - Optional: true, - Description: "Naming descripton e.g. 'Default'", - }, - }, - }, - }, - "sleep": { - Type: schema.TypeInt, - Optional: true, - Default: 30, - Description: "Configurable sleep time in seconds between retries for firewall configuration", - }, - "timeout": { - Type: schema.TypeInt, - Optional: true, - Default: 1800, - Description: "Configurable timeout time in seconds for firewall configuration", - }, - }, - } -} - -func resourceSecurityFirewallRulesCreate(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - instanceID = d.Get("instance_id").(int) - localFirewalls = d.Get("rules").(*schema.Set).List() - params []map[string]interface{} - sleep = d.Get("sleep").(int) - timeout = d.Get("timeout").(int) - ) - - for _, k := range localFirewalls { - params = append(params, k.(map[string]interface{})) - } - - err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) - if err != nil { - return fmt.Errorf("error setting security firewall for resource %s: %s", d.Id(), err) - } - - d.SetId("na") - return nil -} - -func resourceSecurityFirewallRulesRead(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - ip = d.Get("ip").(string) - instanceID = d.Get("instance_id").(int) - ) - - data, err := api.ReadFirewallRule(instanceID, ip) - log.Printf("[DEBUG] security firewall rule: %v", data) - if err != nil { - return err - } - - for k, v := range data { - if v != nil { - d.Set(k, v) - } - } - - return nil -} - -func resourceSecurityFirewallRulesUpdate(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - instanceID = d.Get("instance_id").(int) - params []map[string]interface{} - sleep = d.Get("sleep").(int) - timeout = d.Get("timeout").(int) - ) - - // if d.HasChange("rules") { - // localFirewalls := d.Get("rules").(*schema.Set).List() - // for k, v := range localFirewalls { - - // } - - // } - - err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) - if err != nil { - return fmt.Errorf("error setting security firewall for resource %s: %s", d.Id(), err) - } - - d.SetId(strconv.Itoa(instanceID)) - return nil -} - -func resourceSecurityFirewallRulesDelete(d *schema.ResourceData, meta interface{}) error { - var ( - api = meta.(*api.API) - instanceID = d.Get("instance_id").(int) - rule = make(map[string]interface{}) - params []map[string]interface{} - sleep = d.Get("sleep").(int) - timeout = d.Get("timeout").(int) - ) - - // Skip if faster instance destroy enabled - if enableFasterInstanceDestroy == true { - log.Printf("[DEBUG] cloudamqp::resource::security_firewall::delete skip calling backend.") - return nil - } - - // Only set ip with correct value to make the PATCH request remove the rule - rule["ip"] = d.Id() - rule["services"] = []string{} - rule["ports"] = []int{} - params = append(params, rule) - err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) - if err != nil { - return fmt.Errorf("failed to remove firewall rule for IP %s: %s", d.Id(), err) - } - - return nil -} From e066ab55700e1b1865ef02e0308ca159448c1ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Mon, 7 Aug 2023 10:07:20 +0200 Subject: [PATCH 10/20] Change name on 'replace' to 'patch' and change behaviour --- .../resource_cloudamqp_security_firewall.go | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index 89db84dd..6f7c3b14 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -31,11 +31,11 @@ func resourceSecurityFirewall() *schema.Resource { Required: true, Description: "Instance identifier", }, - "replace": { + "patch": { Type: schema.TypeBool, Optional: true, - Default: true, - Description: "Replace all firewall rules or append/update", + Default: false, + Description: "Patch firewall rules instead of replacing them", }, "rules": { Type: schema.TypeSet, @@ -111,7 +111,7 @@ func resourceSecurityFirewallCreate(d *schema.ResourceData, meta interface{}) er api = meta.(*api.API) instanceID = d.Get("instance_id").(int) localFirewalls = d.Get("rules").(*schema.Set).List() - replace = d.Get("replace").(bool) + patch = d.Get("patch").(bool) params []map[string]interface{} sleep = d.Get("sleep").(int) timeout = d.Get("timeout").(int) @@ -123,10 +123,10 @@ func resourceSecurityFirewallCreate(d *schema.ResourceData, meta interface{}) er params = append(params, k.(map[string]interface{})) } - if replace { - err = api.CreateFirewallSettings(instanceID, params, sleep, timeout) - } else { + if patch { err = api.PatchFirewallSettings(instanceID, params, sleep, timeout) + } else { + err = api.CreateFirewallSettings(instanceID, params, sleep, timeout) } if err != nil { @@ -139,11 +139,10 @@ func resourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) erro var ( api = meta.(*api.API) instanceID, _ = strconv.Atoi(d.Id()) // Needed for import - replace = d.Get("replace").(bool) + patch = d.Get("patch").(bool) rules []map[string]interface{} ) - // Filter out rules that are present in localFirewalls... (d.Get("rules").(*schema.Set).List()) d.Set("instance_id", instanceID) data, err := api.ReadFirewallSettings(instanceID) if err != nil { @@ -151,18 +150,17 @@ func resourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) erro } log.Printf("[DEBUG] Read firewall rules: %v", data) - if replace { - for _, v := range data { - rules = append(rules, readRule(v)) - } - } else { + if patch { // How to handle import? - // localRules := d.Get("rules").(*schema.Set).List() 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) @@ -177,7 +175,7 @@ func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) er var ( api = meta.(*api.API) instanceID = d.Get("instance_id").(int) - replace = d.Get("replace").(bool) + patch = d.Get("patch").(bool) rules []map[string]interface{} sleep = d.Get("sleep").(int) timeout = d.Get("timeout").(int) @@ -187,7 +185,7 @@ func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) er return nil } - if replace { + if !patch { for _, k := range d.Get("rules").(*schema.Set).List() { rules = append(rules, k.(map[string]interface{})) } @@ -195,6 +193,8 @@ func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) er return api.UpdateFirewallSettings(instanceID, rules, sleep, timeout) } + // 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) @@ -221,7 +221,7 @@ func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) er instanceID = d.Get("instance_id").(int) sleep = d.Get("sleep").(int) timeout = d.Get("timeout").(int) - replace = d.Get("replace").(bool) + patch = d.Get("patch").(bool) ) if enableFasterInstanceDestroy == true { @@ -229,12 +229,13 @@ func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) er return nil } - if replace { + if !patch { data, err := api.DeleteFirewallSettings(instanceID, sleep, timeout) d.Set("rules", data) return err } + // 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) @@ -245,8 +246,7 @@ func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) er params = append(params, rule) } log.Printf("[DEBUG] Delete firewall params: %v", params) - err := api.PatchFirewallSettings(instanceID, params, sleep, timeout) - return err + return api.PatchFirewallSettings(instanceID, params, sleep, timeout) } func readRule(data map[string]interface{}) map[string]interface{} { From cd2d3515b5373a4102df5b9a7a260170acb7926f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Mon, 7 Aug 2023 10:07:33 +0200 Subject: [PATCH 11/20] Update docs --- docs/resources/security_firewall.md | 45 ++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/resources/security_firewall.md b/docs/resources/security_firewall.md index 19ae6ff7..afeb281f 100644 --- a/docs/resources/security_firewall.md +++ b/docs/resources/security_firewall.md @@ -9,7 +9,9 @@ description: |- This resource allows you to configure and manage firewall rules for the CloudAMQP instance. -~> **WARNING:** Firewall rules applied with this resource will replace any existing firewall rules. Make sure all wanted rules are present to not lose them. +~> **WARNING:** Firewall rules applied with this resource will replace any existing firewall rules. Make sure all wanted rules are present to not lose them. Unless the arugment patch is set to true. + +-> **NOTE** Using argument `patch = true`, only the given rules will be handled. Either created, updated or removed while leaving all other firewall rules intact. Only available for dedicated subscription plans. @@ -22,20 +24,20 @@ resource "cloudamqp_security_firewall" "firewall_settings" { rules { ip = "192.168.0.0/24" ports = [4567, 4568] - services = ["AMQP","AMQPS", "HTTPS"] + services = ["AMQPS", "HTTPS"] } rules { ip = "10.56.72.0/24" ports = [] - services = ["AMQP","AMQPS", "HTTPS"] + services = ["AMQPS", "HTTPS"] } // Single IP address rules { ip = "192.168.1.10/32" ports = [] - services = ["AMQP","AMQPS", "HTTPS"] + services = ["AMQPS", "HTTPS"] } } ``` @@ -47,7 +49,7 @@ resource "cloudamqp_security_firewall" "firewall_settings" { -CloudAMQP Terraform provider [v1.27.0](https://github.com/cloudamqp/terraform-provider-cloudamqp/releases/tag/v1.27.0) enables faster `cloudamqp_instance` destroy when running `terraform destroy`. +The CloudAMQP Terraform provider [v1.27.0](https://github.com/cloudamqp/terraform-provider-cloudamqp/releases/tag/v1.27.0) enables faster `cloudamqp_instance` destroy when running `terraform destroy`. ```hcl # Configure the CloudAMQP Provider @@ -69,13 +71,39 @@ resource "cloudamqp_security_firewall" "firewall_settings" { rules { ip = "192.168.0.0/24" ports = [4567, 4568] - services = ["AMQP","AMQPS", "HTTPS"] + services = ["AMQPS", "HTTPS"] } rules { ip = "10.56.72.0/24" ports = [] - services = ["AMQP","AMQPS", "HTTPS"] + services = ["AMQPS", "HTTPS"] + } +} +``` + + +
+ + + Only patch one or more firewall rules, instead of replacing them all. From v1.28.0 + + + +The CloudAMQP Terraform provider [v1.28.0](https://github.com/cloudamqp/terraform-provider-cloudamqp/releases/tag/v1.28.0) adds new argument called `patch`. When patch set to true, instead of replacing all firewall rules, only the rules present in the resource will be handled. + +```hcl +// New resource only handling MGMT interface rule +resource "cloudamqp_security_firewall" "patching_rules" { + instance_id = cloudamqp_instance.instance.id + patch = true + +// From previous example, adds a new rule that open up management interface. + rules { + ip = "0.0.0.0/0" + description = "MGMT interface" + ports = [] + services = ["HTTPS"] } } ``` @@ -87,6 +115,7 @@ Top level argument reference * `instance_id` - (Required) The CloudAMQP instance ID. * `rules` - (Required) An array of rules, minimum of 1 needs to be configured. Each `rules` block consists of the field documented below. +* `patch` - (Optional) Patch firewall rules instead of replacing all of them. * `sleep` - (Optional) Configurable sleep time in seconds between retries for firewall configuration. Default set to 30 seconds. * `timeout` - (Optional) Configurable timeout time in seconds for firewall configuration. Default set to 1800 seconds. @@ -191,5 +220,5 @@ resource "cloudamqp_security_firewall" "firewall_settings" { } ``` -The provider from [v1.15.2](https://github.com/cloudamqp/terraform-provider-cloudamqp/releases/tag/v1.16.0) will start to warn about using this. +The provider from [v1.15.2](https://github.com/cloudamqp/terraform-provider-cloudamqp/releases/tag/v1.15.2) will start to warn about using this.
From 62cfa6f2f65f1681b727175d9ad6963f1379ca14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Mon, 7 Aug 2023 10:43:01 +0200 Subject: [PATCH 12/20] Change schema type to populate the rules --- cloudamqp/data_source_cloudamqp_security_firewall.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cloudamqp/data_source_cloudamqp_security_firewall.go b/cloudamqp/data_source_cloudamqp_security_firewall.go index fac14540..35d1bd9c 100644 --- a/cloudamqp/data_source_cloudamqp_security_firewall.go +++ b/cloudamqp/data_source_cloudamqp_security_firewall.go @@ -19,7 +19,7 @@ func datasourceSecurityFirewall() *schema.Resource { Description: "Instance identifier", }, "rules": { - Type: schema.TypeSet, + Type: schema.TypeList, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -61,7 +61,7 @@ func datasourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) er var ( api = meta.(*api.API) instanceID = d.Get("instance_id").(int) - // rules []map[string]interface{} + rules []map[string]interface{} ) data, err := api.ReadFirewallSettings(instanceID) @@ -69,9 +69,8 @@ func datasourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) er return err } d.SetId(strconv.Itoa(instanceID)) - rules := make([]map[string]interface{}, len(data)) - for k, v := range data { - rules[k] = readRule(v) + 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 { From 2b1d0ea91b46cb81c4cb37a60081dca0c6119719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Mon, 7 Aug 2023 10:43:27 +0200 Subject: [PATCH 13/20] Cleanup --- cloudamqp/resource_cloudamqp_security_firewall.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index 6f7c3b14..cd3b99a2 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -23,6 +23,7 @@ func resourceSecurityFirewall() *schema.Resource { Update: resourceSecurityFirewallUpdate, Delete: resourceSecurityFirewallDelete, Importer: &schema.ResourceImporter{ + // Can only import all rules State: schema.ImportStatePassthrough, }, Schema: map[string]*schema.Schema{ @@ -151,7 +152,6 @@ func resourceSecurityFirewallRead(d *schema.ResourceData, meta interface{}) erro log.Printf("[DEBUG] Read firewall rules: %v", data) if patch { - // How to handle import? for _, v := range data { if d.Get("rules").(*schema.Set).Contains(v) { rules = append(rules, readRule(v)) @@ -185,6 +185,7 @@ func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) er return nil } + // Replace all rules if !patch { for _, k := range d.Get("rules").(*schema.Set).List() { rules = append(rules, k.(map[string]interface{})) @@ -229,6 +230,7 @@ func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) er return nil } + // Remove firewall settings and set default 0.0.0.0/0 rule (found in go-api). if !patch { data, err := api.DeleteFirewallSettings(instanceID, sleep, timeout) d.Set("rules", data) From 477b4ed53f1d13276139490ac5c09c67f7e46f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Mon, 7 Aug 2023 10:57:40 +0200 Subject: [PATCH 14/20] Update docs --- docs/resources/security_firewall.md | 46 ++++++++++++++++++----------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/docs/resources/security_firewall.md b/docs/resources/security_firewall.md index afeb281f..480d2200 100644 --- a/docs/resources/security_firewall.md +++ b/docs/resources/security_firewall.md @@ -9,7 +9,7 @@ description: |- This resource allows you to configure and manage firewall rules for the CloudAMQP instance. -~> **WARNING:** Firewall rules applied with this resource will replace any existing firewall rules. Make sure all wanted rules are present to not lose them. Unless the arugment patch is set to true. +~> **WARNING** Firewall rules applied with this resource will replace any existing firewall rules. Make sure all wanted rules are present to not lose them. Unless the arugment patch is set to true. -> **NOTE** Using argument `patch = true`, only the given rules will be handled. Either created, updated or removed while leaving all other firewall rules intact. @@ -21,22 +21,15 @@ Only available for dedicated subscription plans. resource "cloudamqp_security_firewall" "firewall_settings" { instance_id = cloudamqp_instance.instance.id - rules { - ip = "192.168.0.0/24" - ports = [4567, 4568] - services = ["AMQPS", "HTTPS"] - } - rules { ip = "10.56.72.0/24" ports = [] services = ["AMQPS", "HTTPS"] } - // Single IP address rules { - ip = "192.168.1.10/32" - ports = [] + ip = "10.1.0.0/16" + ports = [4567] services = ["AMQPS", "HTTPS"] } } @@ -69,14 +62,14 @@ resource "cloudamqp_security_firewall" "firewall_settings" { instance_id = cloudamqp_instance.instance.id rules { - ip = "192.168.0.0/24" - ports = [4567, 4568] + ip = "10.56.72.0/24" + ports = [] services = ["AMQPS", "HTTPS"] } rules { - ip = "10.56.72.0/24" - ports = [] + ip = "10.1.0.0/16" + ports = [4567] services = ["AMQPS", "HTTPS"] } } @@ -90,15 +83,15 @@ resource "cloudamqp_security_firewall" "firewall_settings" { -The CloudAMQP Terraform provider [v1.28.0](https://github.com/cloudamqp/terraform-provider-cloudamqp/releases/tag/v1.28.0) adds new argument called `patch`. When patch set to true, instead of replacing all firewall rules, only the rules present in the resource will be handled. +The CloudAMQP Terraform provider [v1.28.0](https://github.com/cloudamqp/terraform-provider-cloudamqp/releases/tag/v1.28.0) adds new argument called `patch`. When patch set to true, instead of replacing all firewall rules, only the rules present in the resource will be handled. Multiple patched resource can be used together. + +~> ***WARNING*** Cannot be used together with the original firewall resource. Since every time the patched resource makes changes, this will affect the original firewall resource. ```hcl -// New resource only handling MGMT interface rule -resource "cloudamqp_security_firewall" "patching_rules" { +resource "cloudamqp_security_firewall" "mgmt_rule" { instance_id = cloudamqp_instance.instance.id patch = true -// From previous example, adds a new rule that open up management interface. rules { ip = "0.0.0.0/0" description = "MGMT interface" @@ -106,6 +99,23 @@ resource "cloudamqp_security_firewall" "patching_rules" { services = ["HTTPS"] } } + +resource "cloudamqp_security_firewall" "extra_firewall_rules" { + instance_id = cloudamqp_instance.instance.id + patch = true + + rules { + ip = "10.1.0.0/16" + ports = [] + services = ["AMQPS"] + } + + rules { + ip = "10.2.0.0/16" + ports = [] + services = ["AMQPS"] + } +} ``` From 4c61f50dbd5d93eff51927a87994a2e8193958fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Mon, 7 Aug 2023 13:20:09 +0200 Subject: [PATCH 15/20] Update docs --- docs/data-sources/security_firewall.md | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/data-sources/security_firewall.md diff --git a/docs/data-sources/security_firewall.md b/docs/data-sources/security_firewall.md new file mode 100644 index 00000000..05768f52 --- /dev/null +++ b/docs/data-sources/security_firewall.md @@ -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`. From b1924405904e6a339c9c2beeb287132b33eb019b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Tue, 8 Aug 2023 08:31:47 +0200 Subject: [PATCH 16/20] Guard against unapplied firewall rules --- cloudamqp/resource_cloudamqp_security_firewall.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index cd3b99a2..033a6ef6 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -248,7 +248,10 @@ func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) er params = append(params, rule) } log.Printf("[DEBUG] Delete firewall params: %v", params) - return api.PatchFirewallSettings(instanceID, params, sleep, timeout) + if len(params) > 0 { + return api.PatchFirewallSettings(instanceID, params, sleep, timeout) + } + return nil } func readRule(data map[string]interface{}) map[string]interface{} { From 001b4c0dd5d791620b1dda87d70befa5f3274202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Tue, 8 Aug 2023 08:32:10 +0200 Subject: [PATCH 17/20] Randomize sleep in ms, to avoid fire of action in parallel. Tried out when settings multiple patched firewall rules resources. If their request are executed at the same time, only one will be applied. Randomize sleep, to slightly shift them in order to trigger retry request on latest action. --- cloudamqp/extra.go | 14 ++++++++++++++ cloudamqp/resource_cloudamqp_security_firewall.go | 3 +++ 2 files changed, 17 insertions(+) create mode 100644 cloudamqp/extra.go diff --git a/cloudamqp/extra.go b/cloudamqp/extra.go new file mode 100644 index 00000000..64b2f7bc --- /dev/null +++ b/cloudamqp/extra.go @@ -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(5000) + log.Printf("[DEBUG] %s sleep for %d ms...\n", name, n) + time.Sleep(time.Duration(n) * time.Millisecond) +} diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index 033a6ef6..18d69fe4 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -123,6 +123,7 @@ func resourceSecurityFirewallCreate(d *schema.ResourceData, meta interface{}) er for _, k := range localFirewalls { params = append(params, k.(map[string]interface{})) } + randomSleep(5000, "firewall create") if patch { err = api.PatchFirewallSettings(instanceID, params, sleep, timeout) @@ -185,6 +186,7 @@ func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) er return nil } + randomSleep(5000, "firewall update") // Replace all rules if !patch { for _, k := range d.Get("rules").(*schema.Set).List() { @@ -230,6 +232,7 @@ func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) er return nil } + randomSleep(5000, "firewall delete") // Remove firewall settings and set default 0.0.0.0/0 rule (found in go-api). if !patch { data, err := api.DeleteFirewallSettings(instanceID, sleep, timeout) From 249be3982eb0b9044f3765fbf0696f329f7ab6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Tue, 8 Aug 2023 08:52:55 +0200 Subject: [PATCH 18/20] Use input paramter --- cloudamqp/extra.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudamqp/extra.go b/cloudamqp/extra.go index 64b2f7bc..0266bccd 100644 --- a/cloudamqp/extra.go +++ b/cloudamqp/extra.go @@ -8,7 +8,7 @@ import ( func randomSleep(ms int, name string) { rand.Seed(time.Now().UnixNano()) - n := rand.Intn(5000) + n := rand.Intn(ms) log.Printf("[DEBUG] %s sleep for %d ms...\n", name, n) time.Sleep(time.Duration(n) * time.Millisecond) } From ed9e386a54e6a60d1f3262c3cd53f91834a67c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Mon, 4 Sep 2023 14:54:22 +0200 Subject: [PATCH 19/20] Remove random sleep, backend should take care of this --- cloudamqp/resource_cloudamqp_security_firewall.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index 18d69fe4..033a6ef6 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -123,7 +123,6 @@ func resourceSecurityFirewallCreate(d *schema.ResourceData, meta interface{}) er for _, k := range localFirewalls { params = append(params, k.(map[string]interface{})) } - randomSleep(5000, "firewall create") if patch { err = api.PatchFirewallSettings(instanceID, params, sleep, timeout) @@ -186,7 +185,6 @@ func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) er return nil } - randomSleep(5000, "firewall update") // Replace all rules if !patch { for _, k := range d.Get("rules").(*schema.Set).List() { @@ -232,7 +230,6 @@ func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) er return nil } - randomSleep(5000, "firewall delete") // Remove firewall settings and set default 0.0.0.0/0 rule (found in go-api). if !patch { data, err := api.DeleteFirewallSettings(instanceID, sleep, timeout) From 22f369e813e8670263aecf833ffaf2b1bd664839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Brod=C3=A9n?= Date: Tue, 19 Sep 2023 10:29:07 +0200 Subject: [PATCH 20/20] Avoid negative conditionals --- .../resource_cloudamqp_security_firewall.go | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index f08ee52e..f5823485 100644 --- a/cloudamqp/resource_cloudamqp_security_firewall.go +++ b/cloudamqp/resource_cloudamqp_security_firewall.go @@ -186,35 +186,35 @@ func resourceSecurityFirewallUpdate(d *schema.ResourceData, meta interface{}) er return nil } - // Replace all rules - if !patch { - for _, k := range d.Get("rules").(*schema.Set).List() { - rules = append(rules, k.(map[string]interface{})) + 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) } - log.Printf("[DEBUG] Firewall update instance id: %v, rules: %v", instanceID, rules) - return api.UpdateFirewallSettings(instanceID, rules, sleep, timeout) - } - // 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{}))) + } - 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) } - log.Printf("[DEBUG] Update firewall, rules: %v", rules) - return api.PatchFirewallSettings(instanceID, rules, sleep, timeout) + // Replace all rules + for _, k := range d.Get("rules").(*schema.Set).List() { + rules = append(rules, k.(map[string]interface{})) + } + log.Printf("[DEBUG] Firewall update instance id: %v, rules: %v", instanceID, rules) + return api.UpdateFirewallSettings(instanceID, rules, sleep, timeout) } func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) error { @@ -231,28 +231,28 @@ func resourceSecurityFirewallDelete(d *schema.ResourceData, meta interface{}) er return nil } - // Remove firewall settings and set default 0.0.0.0/0 rule (found in go-api). - if !patch { - data, err := api.DeleteFirewallSettings(instanceID, sleep, timeout) - d.Set("rules", data) - return err + 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) + } + return nil } - // 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) - } - 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 } func readRule(data map[string]interface{}) map[string]interface{} {