-
Notifications
You must be signed in to change notification settings - Fork 632
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 cloudflare_teams_route and acceptance tests #1572
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:new-resource | ||
cloudflare_teams_route | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you have a comment in a resource and then simply delete the line with the intent to remove the comment, will it remove it? Perhaps the comment parameter should have a default of |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This import doesn't seem like it'll work as it splits into 3 parts but looks for only 2 here. Similarly, since the read function only uses the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I originally implemented it with a different API call where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doh. i hadn't noticed you'd responded ... and just submitted a pr myself ... feel free to kill mine if you've got one. |
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of this to read the resource seems fragile. If you are managing two routes, say
10.0.0.0/8
and10.10.0.0/24
via terraform and10.10.0.0/24
gets manually removed on accident ... terraform will scan resources and actually find the route for10.0.0.0/8
for the10.10.0.0/24
resource. Was it not feasible to use the List Tunnel Routes API passing in the tunnel_id parameter to specify an exact tunnel?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, perhaps this should verify the network returned is an exact match for what was requested to prevent this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i guess more than one network could route to the same tunnel, but you could pass in
network_subnet=10.10.0.0/24
andnetwork_superset=10.10.0.0/24
to get the exact route. And probably includeis_deleted=false
too ...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a look at doing that instead. It's a lot of workaround for an endpoint that doesn't follow the normal pattern of the V4 API.