diff --git a/cloudamqp/data_source_cloudamqp_security_firewall.go b/cloudamqp/data_source_cloudamqp_security_firewall.go new file mode 100644 index 00000000..35d1bd9c --- /dev/null +++ b/cloudamqp/data_source_cloudamqp_security_firewall.go @@ -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 +} diff --git a/cloudamqp/extra.go b/cloudamqp/extra.go new file mode 100644 index 00000000..0266bccd --- /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(ms) + log.Printf("[DEBUG] %s sleep for %d ms...\n", name, n) + time.Sleep(time.Duration(n) * time.Millisecond) +} diff --git a/cloudamqp/provider.go b/cloudamqp/provider.go index 41173877..1733c533 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(), diff --git a/cloudamqp/resource_cloudamqp_security_firewall.go b/cloudamqp/resource_cloudamqp_security_firewall.go index 4382eab1..f5823485 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{ @@ -32,6 +33,12 @@ func resourceSecurityFirewall() *schema.Resource { 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, @@ -102,42 +109,61 @@ 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() + 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) + } else { + err = api.CreateFirewallSettings(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 } 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) @@ -147,37 +173,84 @@ 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() - 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) } - 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) } 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) + } + 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 } 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`. diff --git a/docs/resources/security_firewall.md b/docs/resources/security_firewall.md index 19ae6ff7..480d2200 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. @@ -19,23 +21,16 @@ 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 = ["AMQP","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"] + ip = "10.1.0.0/16" + ports = [4567] + services = ["AMQPS", "HTTPS"] } } ``` @@ -47,7 +42,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 @@ -67,15 +62,58 @@ resource "cloudamqp_security_firewall" "firewall_settings" { instance_id = cloudamqp_instance.instance.id rules { - ip = "192.168.0.0/24" - ports = [4567, 4568] - services = ["AMQP","AMQPS", "HTTPS"] + ip = "10.56.72.0/24" + ports = [] + services = ["AMQPS", "HTTPS"] } rules { - ip = "10.56.72.0/24" + ip = "10.1.0.0/16" + ports = [4567] + 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. 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 +resource "cloudamqp_security_firewall" "mgmt_rule" { + instance_id = cloudamqp_instance.instance.id + patch = true + + rules { + ip = "0.0.0.0/0" + description = "MGMT interface" + ports = [] + 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 = ["AMQP","AMQPS", "HTTPS"] + services = ["AMQPS"] } } ``` @@ -87,6 +125,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 +230,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.