Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for Magic Transit IPsec tunnels #1404

Merged
merged 10 commits into from
Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/1404.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_ipsec_tunnel
```
1 change: 1 addition & 0 deletions cloudflare/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ func Provider() *schema.Provider {
"cloudflare_firewall_rule": resourceCloudflareFirewallRule(),
"cloudflare_healthcheck": resourceCloudflareHealthcheck(),
"cloudflare_ip_list": resourceCloudflareIPList(),
"cloudflare_ipsec_tunnel": resourceCloudflareIPsecTunnel(),
"cloudflare_load_balancer_monitor": resourceCloudflareLoadBalancerMonitor(),
"cloudflare_load_balancer_pool": resourceCloudflareLoadBalancerPool(),
"cloudflare_load_balancer": resourceCloudflareLoadBalancer(),
Expand Down
128 changes: 128 additions & 0 deletions cloudflare/resource_cloudflare_ipsec_tunnel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cloudflare

import (
"context"
"fmt"
"log"
"strings"

"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/pkg/errors"
)

func resourceCloudflareIPsecTunnel() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareIPsecTunnelSchema(),
Create: resourceCloudflareIPsecTunnelCreate,
Read: resourceCloudflareIPsecTunnelRead,
Update: resourceCloudflareIPsecTunnelUpdate,
Delete: resourceCloudflareIPsecTunnelDelete,
Importer: &schema.ResourceImporter{
State: resourceCloudflareIPsecTunnelImport,
},
}
}

func resourceCloudflareIPsecTunnelCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
client.AccountID = d.Get("account_id").(string)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will now need to update that we are not using the api.AccountID field and instead, pull it from the schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, I hope I didn't miss any. I am actually not sure, how does meta behave when run in parallel on different resources with different account IDs? I think meta is not really protected from this, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think so but i'd need to confirm the internals. it shouldn't be an issue though as in the Tenant use case, the client will be the same (authn/z) but the URLs will need to swap based on the account.


newTunnel, err := client.CreateMagicTransitIPsecTunnels(context.Background(), []cloudflare.MagicTransitIPsecTunnel{
IPsecTunnelFromResource(d),
})

if err != nil {
return errors.Wrap(err, fmt.Sprintf("error creating IPSec tunnel %s", d.Get("name").(string)))
}

d.SetId(newTunnel[0].ID)

return resourceCloudflareIPsecTunnelRead(d, meta)
}

func resourceCloudflareIPsecTunnelImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
client := meta.(*cloudflare.API)
attributes := strings.SplitN(d.Id(), "/", 2)

if len(attributes) != 2 {
return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/tunnelID\"", d.Id())
}

accountID, tunnelID := attributes[0], attributes[1]
d.SetId(tunnelID)
d.Set("account_id", accountID)
client.AccountID = accountID

resourceCloudflareIPsecTunnelRead(d, meta)

return []*schema.ResourceData{d}, nil
}

func resourceCloudflareIPsecTunnelRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
client.AccountID = d.Get("account_id").(string)

tunnel, err := client.GetMagicTransitIPsecTunnel(context.Background(), d.Id())
if err != nil {
if strings.Contains(err.Error(), "IPsec tunnel not found") {
log.Printf("[INFO] IPsec tunnel %s not found", d.Id())
d.SetId("")
return nil
}
return errors.Wrap(err, fmt.Sprintf("error reading IPsec tunnel ID %q", d.Id()))
}

d.Set("name", tunnel.Name)
d.Set("customer_endpoint", tunnel.CustomerEndpoint)
d.Set("cloudflare_endpoint", tunnel.CloudflareEndpoint)
d.Set("interface_address", tunnel.InterfaceAddress)

if len(tunnel.Description) > 0 {
d.Set("description", tunnel.Description)
}

return nil
}

func resourceCloudflareIPsecTunnelUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
client.AccountID = d.Get("account_id").(string)

_, err := client.UpdateMagicTransitIPsecTunnel(context.Background(), d.Id(), IPsecTunnelFromResource(d))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("error updating IPsec tunnel %q", d.Id()))
}

return resourceCloudflareIPsecTunnelRead(d, meta)
}

func resourceCloudflareIPsecTunnelDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
client.AccountID = d.Get("account_id").(string)

log.Printf("[INFO] Deleting IPsec tunnel: %s", d.Id())

_, err := client.DeleteMagicTransitIPsecTunnel(context.Background(), d.Id())
if err != nil {
return fmt.Errorf("error deleting IPsec tunnel: %s", err)
}

return nil
}

func IPsecTunnelFromResource(d *schema.ResourceData) cloudflare.MagicTransitIPsecTunnel {
tunnel := cloudflare.MagicTransitIPsecTunnel{
Name: d.Get("name").(string),
CustomerEndpoint: d.Get("customer_endpoint").(string),
CloudflareEndpoint: d.Get("cloudflare_endpoint").(string),
InterfaceAddress: d.Get("interface_address").(string),
}

description, descriptionOk := d.GetOk("description")
if descriptionOk {
tunnel.Description = description.(string)
}

return tunnel
}
107 changes: 107 additions & 0 deletions cloudflare/resource_cloudflare_ipsec_tunnel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package cloudflare

import (
"context"
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"

"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccCloudflareIPsecTunnelExists(t *testing.T) {
skipMagicTransitTestForNonConfiguredDefaultZone(t)

rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_ipsec_tunnel.%s", rnd)
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")

var Tunnel cloudflare.MagicTransitIPsecTunnel

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheckAccount(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareIPsecTunnelSimple(rnd, rnd, accountID),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudflareIPsecTunnelExists(name, &Tunnel),
resource.TestCheckResourceAttr(name, "description", rnd),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "customer_endpoint", "203.0.113.1"),
resource.TestCheckResourceAttr(name, "cloudflare_endpoint", "162.159.64.41"),
resource.TestCheckResourceAttr(name, "interface_address", "10.212.0.9/31"),
),
},
},
})
}

func testAccCheckCloudflareIPsecTunnelExists(n string, tunnel *cloudflare.MagicTransitIPsecTunnel) 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 IPsec tunnel is set")
}

client := testAccProvider.Meta().(*cloudflare.API)
foundIPsecTunnel, err := client.GetMagicTransitIPsecTunnel(context.Background(), rs.Primary.ID)
if err != nil {
return err
}

*tunnel = foundIPsecTunnel

return nil
}
}

func TestAccCloudflareIPsecTunnelUpdateDescription(t *testing.T) {
skipMagicTransitTestForNonConfiguredDefaultZone(t)

rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_ipsec_tunnel.%s", rnd)
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")

var Tunnel cloudflare.MagicTransitIPsecTunnel

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheckAccount(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckCloudflareIPsecTunnelSimple(rnd, rnd, accountID),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudflareIPsecTunnelExists(name, &Tunnel),
resource.TestCheckResourceAttr(name, "description", rnd),
),
},
{
Config: testAccCheckCloudflareIPsecTunnelSimple(rnd, rnd+"-updated", accountID),
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudflareIPsecTunnelExists(name, &Tunnel),
resource.TestCheckResourceAttr(name, "description", rnd+"-updated"),
),
},
},
})
}

func testAccCheckCloudflareIPsecTunnelSimple(ID, description, accountID string) string {
return fmt.Sprintf(`
resource "cloudflare_ipsec_tunnel" "%[1]s" {
account_id = "%[3]s"
name = "%[2]s"
customer_endpoint = "203.0.113.1"
cloudflare_endpoint = "162.159.64.41"
interface_address = "10.212.0.9/31"
description = "%[2]s"
}`, ID, description, accountID)
}
33 changes: 33 additions & 0 deletions cloudflare/schema_cloudflare_ipsec_tunnel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cloudflare

import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

func resourceCloudflareIPsecTunnelSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"name": {
Type: schema.TypeString,
Required: true,
},
"customer_endpoint": {
Type: schema.TypeString,
Required: true,
},
"cloudflare_endpoint": {
Type: schema.TypeString,
Required: true,
},
"interface_address": {
Type: schema.TypeString,
Required: true,
},
"description": {
Type: schema.TypeString,
Optional: true,
},
}
}
3 changes: 3 additions & 0 deletions website/cloudflare.erb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@
<li<%= sidebar_current("docs-cloudflare-resource-ip-list") %>>
<a href="/docs/providers/cloudflare/r/ip_list.html">cloudflare_ip_list</a>
</li>
<li<%= sidebar_current("docs-cloudflare-resource-ipsec-tunnel") %>>
<a href="/docs/providers/cloudflare/r/ipsec_tunnel.html">cloudflare_ipsec_tunnel</a>
</li>
<li<%= sidebar_current("docs-cloudflare-resource-load-balancer") %>>
<a href="/docs/providers/cloudflare/r/load_balancer.html">cloudflare_load_balancer</a>
</li>
Expand Down
43 changes: 43 additions & 0 deletions website/docs/r/ipsec_tunnel.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
layout: "cloudflare"
page_title: "Cloudflare: cloudflare_ipsec_tunnel"
sidebar_current: "docs-cloudflare-resource-ipsec-tunnel"
description: |-
Provides a resource which manages IPsec tunnels for Magic Transit.
---

# cloudflare_ipsec_tunnel

Provides a resource, that manages IPsec tunnels for Magic Transit.

## Example Usage

```hcl
resource "cloudflare_ipsec_tunnel" "example" {
account_id = "c4a7362d577a6c3019a474fd6f485821"
name = "IPsec_1"
customer_endpoint = "203.0.113.1"
cloudflare_endpoint = "203.0.113.1"
interface_address = "192.0.2.0/31"
description = "Tunnel for ISP X"
}
```

## Argument Reference

The following arguments are supported:

* `account_id` - (Required) The ID of the account where the tunnel is being created.
* `name` - (Required) Name of the IPsec tunnel.
* `customer_endpoint` - (Required) IP address assigned to the customer side of the IPsec tunnel.
* `cloudflare_endpoint` - (Required) IP address assigned to the Cloudflare side of the IPsec tunnel.
* `interface_address` - (Required) 31-bit prefix (/31 in CIDR notation) supporting 2 hosts, one for each side of the tunnel.
* `description` - (Optional) An optional description of the IPsec tunnel.

## Import

An existing IPsec tunnel can be imported using the account ID and tunnel ID

```
$ terraform import cloudflare_ipsec_tunnel.example d41d8cd98f00b204e9800998ecf8427e/cb029e245cfdd66dc8d2e570d5dd3322
```