diff --git a/digitalocean/import_digitalocean_firewall_test.go b/digitalocean/import_digitalocean_firewall_test.go new file mode 100644 index 00000000..f82f1317 --- /dev/null +++ b/digitalocean/import_digitalocean_firewall_test.go @@ -0,0 +1,140 @@ +package digitalocean + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDigitalOceanFirewall_importBasic(t *testing.T) { + tests := []struct { + description string + firewallName string + config string + }{ + { + description: "only allow inbound SSH(TCP/22)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + inbound_rules = [ + { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + }`, + }, + { + description: "only allow outbound SSH(TCP/22)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + outbound_rules = [ + { + protocol = "tcp" + port_range = "22" + destination_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + }`, + }, + { + description: "only allow inbound SSH(TCP/22) and HTTP(TCP/80)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + inbound_rules = [ + { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + }, + { + protocol = "tcp" + port_range = "80" + source_addresses = ["1.2.3.0/24", "2002::/16"] + }, + ] + }`, + }, + { + description: "only allow outbound SSH(TCP/22) and DNS(UDP/53)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + outbound_rules = [ + { + protocol = "tcp" + port_range = "22" + destination_addresses = ["192.168.1.0/24", "2002:1001::/48"] + }, + { + protocol = "udp" + port_range = "53" + destination_addresses = ["1.2.3.0/24", "2002::/16"] + }, + ] + }`, + }, + { + description: "allow inbound and outbound HTTPS(TCP/443), inbound SSH(TCP/22), and outbound DNS(UDP/53)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + inbound_rules = [ + { + protocol = "tcp" + port_range = "443" + source_addresses = ["192.168.1.0/24", "2002:1001:1:2::/64"] + }, + { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + outbound_rules = [ + { + protocol = "tcp" + port_range = "443" + destination_addresses = ["192.168.1.0/24", "2002:1001:1:2::/64"] + }, + { + protocol = "udp" + port_range = "53" + destination_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + }`, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + config := fmt.Sprintf(tt.config, tt.firewallName) + resourceName := fmt.Sprintf("digitalocean_firewall.%s", tt.firewallName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanFirewallDestroy, + Steps: []resource.TestStep{ + { + Config: config, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + } +} diff --git a/digitalocean/provider.go b/digitalocean/provider.go index 9d0b9af1..b394b54e 100644 --- a/digitalocean/provider.go +++ b/digitalocean/provider.go @@ -25,6 +25,7 @@ func Provider() terraform.ResourceProvider { "digitalocean_certificate": resourceDigitalOceanCertificate(), "digitalocean_domain": resourceDigitalOceanDomain(), "digitalocean_droplet": resourceDigitalOceanDroplet(), + "digitalocean_firewall": resourceDigitalOceanFirewall(), "digitalocean_floating_ip": resourceDigitalOceanFloatingIp(), "digitalocean_loadbalancer": resourceDigitalOceanLoadbalancer(), "digitalocean_record": resourceDigitalOceanRecord(), diff --git a/digitalocean/resource_digitalocean_firewall.go b/digitalocean/resource_digitalocean_firewall.go new file mode 100644 index 00000000..4061a9c6 --- /dev/null +++ b/digitalocean/resource_digitalocean_firewall.go @@ -0,0 +1,574 @@ +package digitalocean + +import ( + "bytes" + "context" + "fmt" + "log" + "strconv" + "strings" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceDigitalOceanFirewall() *schema.Resource { + return &schema.Resource{ + Create: resourceDigitalOceanFirewallCreate, + Read: resourceDigitalOceanFirewallRead, + Update: resourceDigitalOceanFirewallUpdate, + Delete: resourceDigitalOceanFirewallDelete, + Exists: resourceDigitalOceanFirewallExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "status": { + Type: schema.TypeString, + Computed: true, + }, + + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + + "pending_changes": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "droplet_id": { + Type: schema.TypeInt, + Optional: true, + }, + "removing": { + Type: schema.TypeBool, + Optional: true, + }, + "status": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "name": { + Type: schema.TypeString, + Required: true, + }, + + "droplet_ids": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + + "tags": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + + "inbound_rules": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "protocol": { + Type: schema.TypeString, + Optional: true, + }, + "port_range": { + Type: schema.TypeString, + Optional: true, + }, + "source_addresses": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "source_tags": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "source_droplet_ids": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeInt}, + Optional: true, + }, + "source_load_balancer_uids": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + }, + }, + }, + + "outbound_rules": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "protocol": { + Type: schema.TypeString, + Optional: true, + }, + "port_range": { + Type: schema.TypeString, + Optional: true, + }, + "destination_addresses": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "destination_tags": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "destination_droplet_ids": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeInt}, + Optional: true, + }, + "destination_load_balancer_uids": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func resourceDigitalOceanFirewallCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + opts, err := firewallRequest(d, client) + if err != nil { + return fmt.Errorf("Error in firewall request: %s", err) + } + + log.Printf("[DEBUG] Firewall create configuration: %#v", opts) + + firewall, _, err := client.Firewalls.Create(context.Background(), opts) + if err != nil { + return fmt.Errorf("Error creating firewall: %s", err) + } + + // Assign the firewall id + d.SetId(firewall.ID) + + log.Printf("[INFO] Firewall ID: %s", d.Id()) + + return resourceDigitalOceanFirewallRead(d, meta) +} + +func resourceDigitalOceanFirewallRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + // Retrieve the firewall properties for updating the state + firewall, resp, err := client.Firewalls.Get(context.Background(), d.Id()) + if err != nil { + // check if the firewall no longer exists. + if resp != nil && resp.StatusCode == 404 { + log.Printf("[WARN] DigitalOcean Firewall (%s) not found", d.Id()) + d.SetId("") + return nil + } + + return fmt.Errorf("Error retrieving firewall: %s", err) + } + + d.Set("status", firewall.Status) + d.Set("create_at", firewall.Created) + d.Set("pending_changes", firewallPendingChanges(d, firewall)) + d.Set("name", firewall.Name) + d.Set("droplet_ids", firewall.DropletIDs) + d.Set("tags", firewall.Tags) + d.Set("inbound_rules", matchFirewallInboundRules(d, firewall)) + d.Set("outbound_rules", matchFirewallOutboundRules(d, firewall)) + + return nil +} + +func resourceDigitalOceanFirewallUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + opts, err := firewallRequest(d, client) + if err != nil { + return fmt.Errorf("Error in firewall request: %s", err) + } + + log.Printf("[DEBUG] Firewall update configuration: %#v", opts) + + _, _, err = client.Firewalls.Update(context.Background(), d.Id(), opts) + if err != nil { + return fmt.Errorf("Error updating firewall: %s", err) + } + + return resourceDigitalOceanFirewallRead(d, meta) +} + +func resourceDigitalOceanFirewallDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + log.Printf("[INFO] Deleting firewall: %s", d.Id()) + + // Destroy the droplet + _, err := client.Firewalls.Delete(context.Background(), d.Id()) + + // Handle remotely destroyed droplets + if err != nil && strings.Contains(err.Error(), "404 Not Found") { + return nil + } + + if err != nil { + return fmt.Errorf("Error deleting firewall: %s", err) + } + + return nil +} + +func resourceDigitalOceanFirewallExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*godo.Client) + + log.Printf("[INFO] Exists firewall: %s", d.Id()) + + // Retrieve the firewall properties for updating the state + _, resp, err := client.Firewalls.Get(context.Background(), d.Id()) + if err != nil { + // check if the firewall no longer exists. + if resp != nil && resp.StatusCode == 404 { + log.Printf("[WARN] DigitalOcean Firewall (%s) not found", d.Id()) + d.SetId("") + return false, nil + } + + return false, fmt.Errorf("Error retrieving firewall: %s", err) + } + + return true, nil +} + +func firewallRequest(d *schema.ResourceData, client *godo.Client) (*godo.FirewallRequest, error) { + // Build up our firewall request + opts := &godo.FirewallRequest{ + Name: d.Get("name").(string), + } + + if v, ok := d.GetOk("droplet_ids"); ok { + var droplets []int + for _, id := range v.([]interface{}) { + i, err := strconv.Atoi(id.(string)) + if err != nil { + return nil, err + } + droplets = append(droplets, i) + } + opts.DropletIDs = droplets + } + + if v, ok := d.GetOk("tags"); ok { + var tags []string + for _, tag := range v.([]interface{}) { + tags = append(tags, tag.(string)) + } + opts.Tags = tags + } + + // Get inbound_rules + opts.InboundRules = firewallInboundRules(d) + + // Get outbound_rules + opts.OutboundRules = firewallOutboundRules(d) + + return opts, nil +} + +func firewallInboundRules(d *schema.ResourceData) []godo.InboundRule { + rules := make([]godo.InboundRule, 0, len(d.Get("inbound_rules").([]interface{}))) + for i, rawRule := range d.Get("inbound_rules").([]interface{}) { + var src godo.Sources + + rule := rawRule.(map[string]interface{}) + key := fmt.Sprintf("inbound_rules.%d.source_addresses", i) + if vv, ok := d.GetOk(key); ok { + for _, v := range vv.([]interface{}) { + src.Addresses = append(src.Addresses, v.(string)) + } + } + key = fmt.Sprintf("inbound_rules.%d.source_tags", i) + if vv, ok := d.GetOk(key); ok { + for _, v := range vv.([]interface{}) { + src.Tags = append(src.Tags, v.(string)) + } + } + key = fmt.Sprintf("inbound_rules.%d.source_droplet_ids", i) + if vv, ok := d.GetOk(key); ok { + for _, v := range vv.([]interface{}) { + src.DropletIDs = append(src.DropletIDs, v.(int)) + } + } + key = fmt.Sprintf("inbound_rules.%d.source_load_balancer_uids", i) + if vv, ok := d.GetOk(key); ok { + for _, v := range vv.([]interface{}) { + src.LoadBalancerUIDs = append(src.LoadBalancerUIDs, v.(string)) + } + } + r := godo.InboundRule{ + Protocol: rule["protocol"].(string), + PortRange: rule["port_range"].(string), + Sources: &src, + } + rules = append(rules, r) + } + return rules +} + +func firewallOutboundRules(d *schema.ResourceData) []godo.OutboundRule { + rules := make([]godo.OutboundRule, 0, len(d.Get("outbound_rules").([]interface{}))) + for i, rawRule := range d.Get("outbound_rules").([]interface{}) { + var dest godo.Destinations + + rule := rawRule.(map[string]interface{}) + key := fmt.Sprintf("outbound_rules.%d.destination_addresses", i) + if vv, ok := d.GetOk(key); ok { + for _, v := range vv.([]interface{}) { + dest.Addresses = append(dest.Addresses, v.(string)) + } + } + key = fmt.Sprintf("outbound_rules.%d.destination_tags", i) + if vv, ok := d.GetOk(key); ok { + for _, v := range vv.([]interface{}) { + dest.Tags = append(dest.Tags, v.(string)) + } + } + key = fmt.Sprintf("outbound_rules.%d.destination_droplet_ids", i) + if vv, ok := d.GetOk(key); ok { + for _, v := range vv.([]interface{}) { + dest.DropletIDs = append(dest.DropletIDs, v.(int)) + } + } + key = fmt.Sprintf("outbound_rules.%d.destination_load_balancer_uids", i) + if vv, ok := d.GetOk(key); ok { + for _, v := range vv.([]interface{}) { + dest.LoadBalancerUIDs = append(dest.LoadBalancerUIDs, v.(string)) + } + } + r := godo.OutboundRule{ + Protocol: rule["protocol"].(string), + PortRange: rule["port_range"].(string), + Destinations: &dest, + } + rules = append(rules, r) + } + return rules +} + +func firewallPendingChanges(d *schema.ResourceData, firewall *godo.Firewall) []interface{} { + remote := make([]interface{}, 0, len(firewall.PendingChanges)) + for _, change := range firewall.PendingChanges { + rawChange := map[string]interface{}{ + "droplet_id": change.DropletID, + "removing": change.Removing, + "status": change.Status, + } + remote = append(remote, rawChange) + } + return remote +} + +func matchFirewallInboundRules(d *schema.ResourceData, firewall *godo.Firewall) []interface{} { + // Prepare the data. + local := d.Get("inbound_rules").([]interface{}) + remote := make([]interface{}, 0, len(firewall.InboundRules)) + remoteMap := make(map[int]map[string]interface{}) + for _, rule := range firewall.InboundRules { + rawRule := map[string]interface{}{ + "protocol": rule.Protocol, + "port_range": rule.PortRange, + "source_droplet_ids": rule.Sources.DropletIDs, + "source_tags": rule.Sources.Tags, + "source_addresses": rule.Sources.Addresses, + "source_load_balancer_uids": rule.Sources.LoadBalancerUIDs, + } + remote = append(remote, rawRule) + hash := hashFirewallRule(rule.Protocol, rule.PortRange) + remoteMap[hash] = rawRule + } + + // Handle special cases, both using the remote rules. + if len(remote) == 0 || len(local) == 0 { + return remote + } + + // Update the local rules to only contains rules match + // to the remote rules. + match := make([]interface{}, 0, len(firewall.InboundRules)) + for _, rawRule := range local { + local := rawRule.(map[string]interface{}) + protocol := local["protocol"].(string) + portRange := local["port_range"].(string) + hash := hashFirewallRule(protocol, portRange) + remote, ok := remoteMap[hash] + if !ok { + // No entry in the remote, remove it. + continue + } + + // matches source lists. + key := "source_droplet_ids" + local[key] = matchFirewallIntLists(key, local, remote) + keys := []string{ + "source_tags", + "source_addresses", + "source_load_balancer_uids", + } + for _, key := range keys { + local[key] = matchFirewallStringLists(key, local, remote) + } + + match = append(match, local) + delete(remoteMap, hash) + } + + // Append the remaining remote rules. + for _, rawRule := range remoteMap { + match = append(match, rawRule) + } + + return match +} + +func matchFirewallOutboundRules(d *schema.ResourceData, firewall *godo.Firewall) []interface{} { + // Prepare the data. + local := d.Get("outbound_rules").([]interface{}) + remote := make([]interface{}, 0, len(firewall.OutboundRules)) + remoteMap := make(map[int]map[string]interface{}) + for _, rule := range firewall.OutboundRules { + rawRule := map[string]interface{}{ + "protocol": rule.Protocol, + "port_range": rule.PortRange, + "destination_droplet_ids": rule.Destinations.DropletIDs, + "destination_tags": rule.Destinations.Tags, + "destination_addresses": rule.Destinations.Addresses, + "destination_load_balancer_uids": rule.Destinations.LoadBalancerUIDs, + } + remote = append(remote, rawRule) + hash := hashFirewallRule(rule.Protocol, rule.PortRange) + remoteMap[hash] = rawRule + } + + // Handle special cases, both using the remote rules. + if len(remote) == 0 || len(local) == 0 { + return remote + } + + // Update the local rules to only contains rules match + // to the remote rules. + match := make([]interface{}, 0, len(firewall.OutboundRules)) + for _, rawRule := range local { + local := rawRule.(map[string]interface{}) + protocol := local["protocol"].(string) + portRange := local["port_range"].(string) + hash := hashFirewallRule(protocol, portRange) + remote, ok := remoteMap[hash] + if !ok { + // No entry in the remote, remove it. + continue + } + + // matches destination lists. + key := "destination_droplet_ids" + local[key] = matchFirewallIntLists(key, local, remote) + keys := []string{ + "destination_tags", + "destination_addresses", + "destination_load_balancer_uids", + } + for _, key := range keys { + local[key] = matchFirewallStringLists(key, local, remote) + } + + match = append(match, local) + delete(remoteMap, hash) + } + + // Append the remaining remote rules. + for _, rawRule := range remoteMap { + match = append(match, rawRule) + } + + return match +} + +func matchFirewallIntLists(key string, local, remote map[string]interface{}) []interface{} { + remoteSize := len(remote[key].([]int)) + remoteSet := make(map[int]bool) + matchedList := make([]interface{}, 0, remoteSize) + + // Create a remote set out of the list for the quick comparison. + for _, i := range remote[key].([]int) { + remoteSet[i] = true + } + + // Add only the item which exists in the remote list. + for _, i := range local[key].([]interface{}) { + if _, ok := remoteSet[i.(int)]; !ok { + continue + } + matchedList = append(matchedList, i) + delete(remoteSet, i.(int)) + } + + // Append items only exists in the remote list. + for i := range remoteSet { + matchedList = append(matchedList, i) + } + + return matchedList +} + +func matchFirewallStringLists(key string, local, remote map[string]interface{}) []interface{} { + remoteSize := len(remote[key].([]string)) + remoteList := make([]interface{}, 0, remoteSize) + matchedList := make([]interface{}, 0, remoteSize) + + // Create a remote set out of the list for the quick comparison. + for _, s := range remote[key].([]string) { + remoteList = append(remoteList, s) + } + remoteSet := schema.NewSet(schema.HashString, remoteList) + + // Add only the item which exists in the remote list. + for _, s := range local[key].([]interface{}) { + if !remoteSet.Contains(s.(string)) { + continue + } + matchedList = append(matchedList, s) + remoteSet.Remove(s) + } + + // Append items only exists in the remote list. + for _, s := range remoteSet.List() { + matchedList = append(matchedList, s) + } + + return matchedList +} + +func hashFirewallRule(protocol, portRange string) int { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("%s-%s", protocol, portRange)) + return hashcode.String(buf.String()) +} diff --git a/digitalocean/resource_digitalocean_firewall_test.go b/digitalocean/resource_digitalocean_firewall_test.go new file mode 100644 index 00000000..b1e7b5ad --- /dev/null +++ b/digitalocean/resource_digitalocean_firewall_test.go @@ -0,0 +1,261 @@ +package digitalocean + +import ( + "context" + "fmt" + "testing" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDigitalOceanFirewall_Basic(t *testing.T) { + tests := []struct { + description string + firewallName string + config string + checkers []resource.TestCheckFunc + }{ + { + description: "only allow inbound SSH(TCP/22)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + inbound_rules = [ + { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + }`, + checkers: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.#", "1"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.port_range", "22"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.protocol", "tcp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.1", "::/0"), + }, + }, + { + description: "only allow outbound SSH(TCP/22)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + outbound_rules = [ + { + protocol = "tcp" + port_range = "22" + destination_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + }`, + checkers: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.#", "1"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.port_range", "22"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.protocol", "tcp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.1", "::/0"), + }, + }, + { + description: "only allow inbound SSH(TCP/22) and HTTP(TCP/80)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + inbound_rules = [ + { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + }, + { + protocol = "tcp" + port_range = "80" + source_addresses = ["1.2.3.0/24", "2002::/16"] + }, + ] + }`, + checkers: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.port_range", "22"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.protocol", "tcp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.1", "::/0"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.port_range", "80"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.protocol", "tcp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.source_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.source_addresses.0", "1.2.3.0/24"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.source_addresses.1", "2002::/16"), + }, + }, + { + description: "only allow outbound SSH(TCP/22) and DNS(UDP/53)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + outbound_rules = [ + { + protocol = "tcp" + port_range = "22" + destination_addresses = ["192.168.1.0/24", "2002:1001::/48"] + }, + { + protocol = "udp" + port_range = "53" + destination_addresses = ["1.2.3.0/24", "2002::/16"] + }, + ] + }`, + checkers: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.port_range", "22"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.protocol", "tcp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.0", "192.168.1.0/24"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.1", "2002:1001::/48"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.port_range", "53"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.protocol", "udp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.destination_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.destination_addresses.0", "1.2.3.0/24"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.destination_addresses.1", "2002::/16"), + }, + }, + { + description: "allow inbound and outbound HTTPS(TCP/443), inbound SSH(TCP/22), and outbound DNS(UDP/53)", + firewallName: fmt.Sprintf("foobar-test-terraform-firewall-%s", acctest.RandString(10)), + config: ` + resource "digitalocean_firewall" "foobar" { + name = "%s" + inbound_rules = [ + { + protocol = "tcp" + port_range = "443" + source_addresses = ["192.168.1.0/24", "2002:1001:1:2::/64"] + }, + { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + outbound_rules = [ + { + protocol = "tcp" + port_range = "443" + destination_addresses = ["192.168.1.0/24", "2002:1001:1:2::/64"] + }, + { + protocol = "udp" + port_range = "53" + destination_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + }`, + checkers: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.port_range", "443"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.protocol", "tcp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.0", "192.168.1.0/24"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.0.source_addresses.1", "2002:1001:1:2::/64"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.port_range", "22"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.protocol", "tcp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.source_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.source_addresses.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "inbound_rules.1.source_addresses.1", "::/0"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.port_range", "443"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.protocol", "tcp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.0", "192.168.1.0/24"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.0.destination_addresses.1", "2002:1001:1:2::/64"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.port_range", "53"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.protocol", "udp"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.destination_addresses.#", "2"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.destination_addresses.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "outbound_rules.1.destination_addresses.1", "::/0"), + }, + }, + } + + var firewall godo.Firewall + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + checkers := []resource.TestCheckFunc{ + testAccCheckDigitalOceanFirewallExists("digitalocean_firewall.foobar", &firewall), + resource.TestCheckResourceAttr("digitalocean_firewall.foobar", "name", tt.firewallName), + } + checkers = append(checkers, tt.checkers...) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanFirewallDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(tt.config, tt.firewallName), + Check: resource.ComposeTestCheckFunc(checkers...), + }, + }, + }) + }) + } +} + +func testAccCheckDigitalOceanFirewallDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*godo.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "digitalocean_firewall" { + continue + } + + // Try to find the firewall + _, _, err := client.Firewalls.Get(context.Background(), rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Firewall still exists") + } + } + + return nil +} + +func testAccCheckDigitalOceanFirewallExists(n string, firewall *godo.Firewall) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + client := testAccProvider.Meta().(*godo.Client) + + foundFirewall, _, err := client.Firewalls.Get(context.Background(), rs.Primary.ID) + + if err != nil { + return err + } + + if foundFirewall.ID != rs.Primary.ID { + return fmt.Errorf("Record not found") + } + + *firewall = *foundFirewall + + return nil + } +} diff --git a/website/digitalocean.erb b/website/digitalocean.erb index 1daea7eb..5e05816a 100644 --- a/website/digitalocean.erb +++ b/website/digitalocean.erb @@ -33,6 +33,10 @@ digitalocean_droplet + > + digitalocean_firewall + + > digitalocean_floating_ip diff --git a/website/docs/r/firewall.html.markdown b/website/docs/r/firewall.html.markdown new file mode 100644 index 00000000..ef71d159 --- /dev/null +++ b/website/docs/r/firewall.html.markdown @@ -0,0 +1,134 @@ +--- +layout: "digitalocean" +page_title: "DigitalOcean: digitalocean_firewall" +sidebar_current: "docs-do-resource-firewall" +description: |- + Provides a DigitalOcean Cloud Firewall resource. This can be used to create, modify, and delete Firewalls. +--- + +# digitalocean\_firewall + +Provides a DigitalOcean Cloud Firewall resource. This can be used to create, +modify, and delete Firewalls. + +## Example Usage + +```hcl +resource "digitalocean_droplet" "web" { + name = "web-1" + size = "512mb" + image = "centos-7-x64" + region = "nyc3" +} + +resource "digitalocean_firewall" "web" { + name = "only-22-80-and-443" + + droplet_ids = ["${digitalocean_droplet.web.id}"] + + inbound_rules = [ + { + protocol = "tcp" + port_range = "22" + source_addresses = ["192.168.1.0/24", "2002:1:2::/48"] + }, + { + protocol = "tcp" + port_range = "80" + source_addresses = ["0.0.0.0/0", "::/0"] + }, + { + protocol = "tcp" + port_range = "443" + source_addresses = ["0.0.0.0/0", "::/0"] + }, + ] + + outbound_rules = [ + { + protocol = "udp" + port_range = "53" + destination_addresses = ["0.0.0.0/0", "::/0"] + }, + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The Firewall name +* `droplet_ids` (Optional) - The list of the IDs of the Droplets assigned + to the Firewall. +* `tags` (Optional) - The names of the Tags assigned to the Firewall. +* `inbound_rules` - (Optional) The inbound access rule block for the Firewall. + The `inbound_rules` block is documented below. +* `outbound_rules` - (Optional) The outbound access rule block for the Firewall. + The `outbound_rules` block is documented below. + +`inbound_rule` supports the following: + +* `protocol` - (Optional) The type of traffic to be allowed. + This may be one of "tcp", "udp", or "icmp". +* `port_range` - (Optional) The ports on which traffic will be allowed + specified as a string containing a single port, a range (e.g. "8000-9000"), + or "all" to open all ports for a protocol. +* `source_addresses` - (Optional) An array of strings containing the IPv4 + addresses, IPv6 addresses, IPv4 CIDRs, and/or IPv6 CIDRs from which the + inbound traffic will be accepted. +* `source_droplet_ids` - (Optional) An array containing the IDs of + the Droplets from which the inbound traffic will be accepted. +* `source_tags` - (Optional) An array containing the names of Tags + corresponding to groups of Droplets from which the inbound traffic + will be accepted. +* `source_load_balancer_uids` - (Optional) An array containing the IDs + of the Load Balancers from which the inbound traffic will be accepted. + +`outbound_rule` supports the following: + +* `protocol` - (Optional) The type of traffic to be allowed. + This may be one of "tcp", "udp", or "icmp". +* `port_range` - (Optional) The ports on which traffic will be allowed + specified as a string containing a single port, a range (e.g. "8000-9000"), + or "all" to open all ports for a protocol. +* `destination_addresses` - (Optional) An array of strings containing the IPv4 + addresses, IPv6 addresses, IPv4 CIDRs, and/or IPv6 CIDRs to which the + outbound traffic will be allowed. +* `destination_droplet_ids` - (Optional) An array containing the IDs of + the Droplets to which the outbound traffic will be allowed. +* `destination_tags` - (Optional) An array containing the names of Tags + corresponding to groups of Droplets to which the outbound traffic will + be allowed. + traffic. +* `destination_load_balancer_uids` - (Optional) An array containing the IDs + of the Load Balancers to which the outbound traffic will be allowed. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - A unique ID that can be used to identify and reference a Firewall. +* `status` - A status string indicating the current state of the Firewall. + This can be "waiting", "succeeded", or "failed". +* `created_at` - A time value given in ISO8601 combined date and time format + that represents when the Firewall was created. +* `pending_changes` - An list of object containing the fields, "droplet_id", + "removing", and "status". It is provided to detail exactly which Droplets + are having their security policies updated. When empty, all changes + have been successfully applied. +* `name` - The name of the Firewall. +* `droplet_ids` - The list of the IDs of the Droplets assigned to + the Firewall. +* `tags` - The names of the Tags assigned to the Firewall. +* `inbound_rules` - The inbound access rule block for the Firewall. +* `outbound_rules` - The outbound access rule block for the Firewall. + +## Import + +Firewalls can be imported using the firewall `id`, e.g. + +``` +terraform import digitalocean_firewall.myfirewall b8ecd2ab-2267-4a5e-8692-cbf1d32583e3 +```