diff --git a/.changelog/1572.txt b/.changelog/1572.txt new file mode 100644 index 0000000000..2c84dbd06b --- /dev/null +++ b/.changelog/1572.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +cloudflare_teams_route +``` diff --git a/cloudflare/provider.go b/cloudflare/provider.go index b16d70a678..1d2242aaa6 100644 --- a/cloudflare/provider.go +++ b/cloudflare/provider.go @@ -173,6 +173,7 @@ func Provider() *schema.Provider { "cloudflare_teams_account": resourceCloudflareTeamsAccount(), "cloudflare_teams_list": resourceCloudflareTeamsList(), "cloudflare_teams_location": resourceCloudflareTeamsLocation(), + "cloudflare_teams_route": resourceCloudflareTeamsRoute(), "cloudflare_teams_rule": resourceCloudflareTeamsRule(), "cloudflare_teams_proxy_endpoint": resourceCloudflareTeamsProxyEndpoint(), "cloudflare_waf_group": resourceCloudflareWAFGroup(), diff --git a/cloudflare/resource_cloudflare_teams_route.go b/cloudflare/resource_cloudflare_teams_route.go new file mode 100644 index 0000000000..84430ea892 --- /dev/null +++ b/cloudflare/resource_cloudflare_teams_route.go @@ -0,0 +1,134 @@ +package cloudflare + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareTeamsRoute() *schema.Resource { + return &schema.Resource{ + Schema: resourceCloudflareTeamsRouteSchema(), + Create: resourceCloudflareTeamsRouteCreate, + Read: resourceCloudflareTeamsRouteRead, + Update: resourceCloudflareTeamsRouteUpdate, + Delete: resourceCloudflareTeamsRouteDelete, + Importer: &schema.ResourceImporter{ + State: resourceCloudflareTeamsRouteImport, + }, + } +} + +func resourceCloudflareTeamsRouteRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + + tunnelRoute, err := client.GetTunnelRouteForIP(context.Background(), cloudflare.TunnelRoutesForIPParams{ + AccountID: d.Get("account_id").(string), + Network: d.Get("network").(string), + }) + if err != nil { + // FIXME(2022-04-21): Until the API returns a valid v4 compatible envelope, we need to + // check if the error message is related to problems unmarshalling the response _or_ + // an expected not found error. + var notFoundError *cloudflare.NotFoundError + if strings.Contains(err.Error(), "error unmarshalling the JSON response error body") || errors.As(err, ¬FoundError) { + d.SetId("") + return nil + } + + return fmt.Errorf("error reading Tunnel Route for Network %q: %w", d.Id(), err) + } + + d.Set("tunnel_id", tunnelRoute.TunnelID) + d.Set("network", tunnelRoute.Network) + + if len(tunnelRoute.Comment) > 0 { + d.Set("comment", tunnelRoute.Comment) + } + + return nil +} + +func resourceCloudflareTeamsRouteCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + + resource := cloudflare.TunnelRoutesCreateParams{ + AccountID: d.Get("account_id").(string), + TunnelID: d.Get("tunnel_id").(string), + Network: d.Get("network").(string), + } + + if comment, ok := d.Get("comment").(string); ok { + resource.Comment = comment + } + + newTunnelRoute, err := client.CreateTunnelRoute(context.Background(), resource) + if err != nil { + return fmt.Errorf("error creating Tunnel Route for Network %q: %w", d.Get("network").(string), err) + } + + d.SetId(newTunnelRoute.Network) + + return resourceCloudflareTeamsRouteRead(d, meta) +} + +func resourceCloudflareTeamsRouteUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + + resource := cloudflare.TunnelRoutesUpdateParams{ + AccountID: d.Get("account_id").(string), + TunnelID: d.Get("tunnel_id").(string), + Network: d.Get("network").(string), + } + + if comment, ok := d.Get("comment").(string); ok { + resource.Comment = comment + } + + _, err := client.UpdateTunnelRoute(context.Background(), resource) + if err != nil { + return fmt.Errorf("error updating Tunnel Route for Network %q: %w", d.Get("network").(string), err) + } + + return resourceCloudflareTeamsRouteRead(d, meta) +} + +func resourceCloudflareTeamsRouteDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*cloudflare.API) + + err := client.DeleteTunnelRoute(context.Background(), cloudflare.TunnelRoutesDeleteParams{ + AccountID: d.Get("account_id").(string), + Network: d.Get("network").(string), + }) + if err != nil { + return fmt.Errorf("error deleting Tunnel Route for Network %q: %w", d.Get("network").(string), err) + } + + return nil +} + +func resourceCloudflareTeamsRouteImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + attributes := strings.SplitN(d.Id(), "/", 3) + + if len(attributes) != 2 { + return nil, fmt.Errorf(`invalid id (%q) specified, should be in format "accountID/tunnelID/network"`, d.Id()) + } + + accountID, tunnelID, network := attributes[0], attributes[1], attributes[2] + + d.SetId(network) + d.Set("account_id", accountID) + d.Set("tunnel_id", tunnelID) + d.Set("network", network) + + err := resourceCloudflareTeamsRouteRead(d, meta) + if err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/cloudflare/resource_cloudflare_teams_route_test.go b/cloudflare/resource_cloudflare_teams_route_test.go new file mode 100644 index 0000000000..60f93ac7a1 --- /dev/null +++ b/cloudflare/resource_cloudflare_teams_route_test.go @@ -0,0 +1,109 @@ +package cloudflare + +import ( + "context" + "errors" + "fmt" + "os" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccCloudflareTeamsRouteExists(t *testing.T) { + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_teams_route.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var TunnelRoute cloudflare.TunnelRoute + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckAccount(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareTeamsRouteSimple(rnd, rnd, accountID, "10.0.0.20/32"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareTeamsRouteExists(name, &TunnelRoute), + resource.TestCheckResourceAttr(name, "account_id", accountID), + resource.TestCheckResourceAttrSet(name, "tunnel_id"), + resource.TestCheckResourceAttr(name, "network", "10.0.0.20/32"), + resource.TestCheckResourceAttr(name, "comment", rnd), + ), + }, + }, + }) +} + +func testAccCheckCloudflareTeamsRouteExists(name string, route *cloudflare.TunnelRoute) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return errors.New("No Teams route is set") + } + + client := testAccProvider.Meta().(*cloudflare.API) + foundTunnelRoute, err := client.GetTunnelRouteForIP(context.Background(), cloudflare.TunnelRoutesForIPParams{ + Network: rs.Primary.ID, + }) + + if err != nil { + return err + } + + *route = foundTunnelRoute + + return nil + } +} + +func TestAccCloudflareTeamsRouteUpdateComment(t *testing.T) { + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_teams_route.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var TunnelRoute cloudflare.TunnelRoute + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckAccount(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareTeamsRouteSimple(rnd, rnd, accountID, "10.0.0.10/32"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareTeamsRouteExists(name, &TunnelRoute), + resource.TestCheckResourceAttr(name, "comment", rnd), + ), + }, + { + Config: testAccCloudflareTeamsRouteSimple(rnd, rnd+"-updated", accountID, "10.0.0.10/32"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareTeamsRouteExists(name, &TunnelRoute), + resource.TestCheckResourceAttr(name, "comment", rnd+"-updated"), + ), + }, + }, + }) +} + +func testAccCloudflareTeamsRouteSimple(ID, comment, accountID, network string) string { + return fmt.Sprintf(` +resource "cloudflare_argo_tunnel" "%[1]s" { + account_id = "%[3]s" + name = "%[1]s" + secret = "AQIDBAUGBwgBAgMEBQYHCAECAwQFBgcIAQIDBAUGBwg=" +} + +resource "cloudflare_teams_route" "%[1]s" { + account_id = "%[3]s" + tunnel_id = cloudflare_argo_tunnel.%[1]s.id + network = "%[4]s" + comment = "%[2]s" +}`, ID, comment, accountID, network) +} diff --git a/cloudflare/schema_cloudflare_teams_route.go b/cloudflare/schema_cloudflare_teams_route.go new file mode 100644 index 0000000000..357bfa43ed --- /dev/null +++ b/cloudflare/schema_cloudflare_teams_route.go @@ -0,0 +1,27 @@ +package cloudflare + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareTeamsRouteSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Required: true, + }, + "tunnel_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "network": { + Type: schema.TypeString, + Required: true, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + }, + } +} diff --git a/website/cloudflare.erb b/website/cloudflare.erb index 8c418c7f06..5878bcc62b 100644 --- a/website/cloudflare.erb +++ b/website/cloudflare.erb @@ -190,6 +190,9 @@ > cloudflare_teams_location + > + cloudflare_teams_route + > cloudflare_teams_rule diff --git a/website/docs/r/teams_route.html.markdown b/website/docs/r/teams_route.html.markdown new file mode 100644 index 0000000000..cd9fecd892 --- /dev/null +++ b/website/docs/r/teams_route.html.markdown @@ -0,0 +1,39 @@ +--- +layout: "cloudflare" +page_title: "Cloudflare: cloudflare_teams_route" +sidebar_content: "docs-cloudflare-teams-route" +description: |- + Provides a resource which manages Cloudflare Tunnel Routes for Zero Trust +--- + +# cloudflare_teams_route + +Provides a resource, that manages Cloudflare tunnel routes for Zero Trust. Tunnel +routes are used to direct IP traffic through Cloudflare Tunnels. + +## Example Usage + +```hcl +resource "cloudflare_teams_route" "example" + account_id = "c4a7362d577a6c3019a474fd6f485821" + tunnel_id = "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415" + network = "192.0.2.24/32" + comment = "New tunnel route for documentation" +``` + +## Argument Reference + +The following arguments are supported: + +* `account_id` - (Required) The ID of the account where the tunnel route is being created +* `tunnel_id` - (Required) The ID of the tunnel that will service the tunnel route +* `network` - (Required) The IPv4 or IPv6 network that should use this tunnel route, in CIDR notation +* `comment` - (Optional) Description of the tunnel route + +## Import + +An existing tunnel route can be imported using the account ID, tunnel ID, and network CIDR. + +``` +$ terraform import cloudflare_teams_route c4a7362d577a6c3019a474fd6f485821/f70ff985-a4ef-4643-bbbc-4a0ed4fc8415/192.0.2.24/32 +```