diff --git a/go.mod b/go.mod index 7b30d85503..8eef33dc69 100644 --- a/go.mod +++ b/go.mod @@ -238,3 +238,6 @@ require ( // Use custom version that supports configuration as flags and newer tfexec // until it lands upstream. replace github.com/hashicorp/terraform-plugin-docs v0.5.1 => github.com/jacobbednarz/terraform-plugin-docs v0.5.1-0.20220314024219-d04ad37d2ee8 + +// TODO: Remove when fix is released +replace github.com/cloudflare/cloudflare-go => github.com/mapped/cloudflare-go v0.40.1-0.20220603175700-567edc574e2c diff --git a/go.sum b/go.sum index 3d060cef84..8472357c3a 100644 --- a/go.sum +++ b/go.sum @@ -186,8 +186,6 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.40.0 h1:OjW+SYY7+NVSTj+/6kORqvu33LH7uZ0hUd/0qOqucxU= -github.com/cloudflare/cloudflare-go v0.40.0/go.mod h1:MmAqiRfD8rjKEuUe4MYNHfHjYhFWfW7PNe12CCQWqPY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -703,6 +701,8 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mapped/cloudflare-go v0.40.1-0.20220603175700-567edc574e2c h1:v0hceaBjJoeUHe6/Vd2SsDfK+EShdmX+Y7GutLYY+zs= +github.com/mapped/cloudflare-go v0.40.1-0.20220603175700-567edc574e2c/go.mod h1:MmAqiRfD8rjKEuUe4MYNHfHjYhFWfW7PNe12CCQWqPY= github.com/maratori/testpackage v1.0.1 h1:QtJ5ZjqapShm0w5DosRjg0PRlSdAdlx+W6cCKoALdbQ= github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 h1:pWxk9e//NbPwfxat7RXkts09K+dEBJWakUWwICVqYbA= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f27aaf8a54..6dc002c404 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -183,6 +183,7 @@ func New(version string) func() *schema.Provider { "cloudflare_teams_rule": resourceCloudflareTeamsRule(), "cloudflare_teams_proxy_endpoint": resourceCloudflareTeamsProxyEndpoint(), "cloudflare_tunnel_route": resourceCloudflareTunnelRoute(), + "cloudflare_tunnel_virtual_network": resourceCloudflareTunnelVirtualNetwork(), "cloudflare_waf_group": resourceCloudflareWAFGroup(), "cloudflare_waf_override": resourceCloudflareWAFOverride(), "cloudflare_waf_package": resourceCloudflareWAFPackage(), diff --git a/internal/provider/resource_cloudflare_tunnel_virtual_network.go b/internal/provider/resource_cloudflare_tunnel_virtual_network.go new file mode 100644 index 0000000000..436c78b94c --- /dev/null +++ b/internal/provider/resource_cloudflare_tunnel_virtual_network.go @@ -0,0 +1,137 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareTunnelVirtualNetwork() *schema.Resource { + return &schema.Resource{ + Schema: resourceCloudflareTunnelVirtualNetworkSchema(), + CreateContext: resourceCloudflareTunnelVirtualNetworkCreate, + ReadContext: resourceCloudflareTunnelVirtualNetworkRead, + UpdateContext: resourceCloudflareTunnelVirtualNetworkUpdate, + DeleteContext: resourceCloudflareTunnelVirtualNetworkDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceCloudflareTunnelVirtualNetworkImport, + }, + } +} + +func resourceCloudflareTunnelVirtualNetworkRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + accountID := d.Get("account_id").(string) + + tunnelVirtualNetworks, err := client.ListTunnelVirtualNetworks(ctx, cloudflare.TunnelVirtualNetworksListParams{ + AccountID: accountID, + IsDeleted: cloudflare.BoolPtr(false), + ID: d.Id(), + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("failed to fetch Tunnel Virtual Network: %w", err)) + } + + if len(tunnelVirtualNetworks) < 1 { + tflog.Info(ctx, fmt.Sprintf("Tunnel Virtual Network for ID %s in account %s not found", d.Id(), accountID)) + d.SetId("") + return nil + } + + tunnelVirtualNetwork := tunnelVirtualNetworks[0] + + d.Set("name", tunnelVirtualNetwork.Name) + d.Set("is_default_network", tunnelVirtualNetwork.IsDefaultNetwork) + + if len(tunnelVirtualNetwork.Comment) > 0 { + d.Set("comment", tunnelVirtualNetwork.Comment) + } + + return nil +} + +func resourceCloudflareTunnelVirtualNetworkCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + name := d.Get("name").(string) + + resource := cloudflare.TunnelVirtualNetworkCreateParams{ + AccountID: d.Get("account_id").(string), + Name: name, + IsDefault: d.Get("is_default_network").(bool), + } + + if comment, ok := d.Get("comment").(string); ok { + resource.Comment = comment + } + + newTunnelVirtualNetwork, err := client.CreateTunnelVirtualNetwork(ctx, resource) + if err != nil { + return diag.FromErr(fmt.Errorf("error creating Tunnel Virtual Network %q: %w", name, err)) + } + + d.SetId(newTunnelVirtualNetwork.ID) + + return resourceCloudflareTunnelVirtualNetworkRead(ctx, d, meta) +} + +func resourceCloudflareTunnelVirtualNetworkUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + + resource := cloudflare.TunnelVirtualNetworkUpdateParams{ + AccountID: d.Get("account_id").(string), + Name: d.Get("name").(string), + IsDefaultNetwork: cloudflare.BoolPtr(d.Get("is_default_network").(bool)), + } + + if comment, ok := d.Get("comment").(string); ok { + resource.Comment = comment + } + + _, err := client.UpdateTunnelVirtualNetwork(ctx, resource) + if err != nil { + return diag.FromErr(fmt.Errorf("error updating Tunnel Virtual Network %q: %w", d.Id(), err)) + } + + return resourceCloudflareTunnelVirtualNetworkRead(ctx, d, meta) +} + +func resourceCloudflareTunnelVirtualNetworkDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*cloudflare.API) + + err := client.DeleteTunnelVirtualNetwork(ctx, cloudflare.TunnelVirtualNetworkDeleteParams{ + AccountID: d.Get("account_id").(string), + VnetID: d.Id(), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting Tunnel Virtual Network %q: %w", d.Id(), err)) + } + + return nil +} + +func resourceCloudflareTunnelVirtualNetworkImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + attributes := strings.SplitN(d.Id(), "/", 2) + + if len(attributes) != 2 { + return nil, fmt.Errorf(`invalid id (%q) specified, should be in format "accountID/vnetID"`, d.Id()) + } + + accountID, vnetID := attributes[0], attributes[1] + + d.SetId(vnetID) + d.Set("account_id", accountID) + + err := resourceCloudflareTunnelVirtualNetworkRead(ctx, d, meta) + if err != nil { + return nil, errors.New("failed to read Tunnel Virtual Network state") + } + + return []*schema.ResourceData{d}, nil +} diff --git a/internal/provider/resource_cloudflare_tunnel_virtual_network_test.go b/internal/provider/resource_cloudflare_tunnel_virtual_network_test.go new file mode 100644 index 0000000000..866b504eb0 --- /dev/null +++ b/internal/provider/resource_cloudflare_tunnel_virtual_network_test.go @@ -0,0 +1,152 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "testing" + + "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func init() { + resource.AddTestSweepers("cloudflare_tunnel_virtual_network", &resource.Sweeper{ + Name: "cloudflare_tunnel_virtual_network", + F: testSweepCloudflareTunnelVirtualNetwork, + }) +} + +func testSweepCloudflareTunnelVirtualNetwork(r string) error { + ctx := context.Background() + client, clientErr := sharedClient() + if clientErr != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to create Cloudflare client: %s", clientErr)) + } + + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + if accountID == "" { + return errors.New("CLOUDFLARE_ACCOUNT_ID must be set") + } + + tunnelVirtualNetworks, err := client.ListTunnelVirtualNetworks(context.Background(), cloudflare.TunnelVirtualNetworksListParams{AccountID: accountID}) + if err != nil { + tflog.Error(ctx, fmt.Sprintf("Failed to fetch Cloudflare Tunnel Virtual Networks: %s", err)) + } + + if len(tunnelVirtualNetworks) == 0 { + log.Print("[DEBUG] No Cloudflare Tunnel Virtual Networks to sweep") + return nil + } + + for _, vnet := range tunnelVirtualNetworks { + tflog.Info(ctx, fmt.Sprintf("Deleting Cloudflare Tunnel Virtual Network %s", vnet.ID)) + //nolint:errcheck + client.DeleteTunnelVirtualNetwork(context.Background(), cloudflare.TunnelVirtualNetworkDeleteParams{AccountID: accountID, VnetID: vnet.ID}) + } + + return nil +} + +func TestAccCloudflareTunnelVirtualNetwork_Exists(t *testing.T) { + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_tunnel_virtual_network.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var TunnelVirtualNetwork cloudflare.TunnelVirtualNetwork + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareTunnelVirtualNetworkSimple(rnd, rnd, accountID, rnd, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareTunnelVirtualNetworkExists(name, &TunnelVirtualNetwork), + resource.TestCheckResourceAttr(name, "account_id", accountID), + resource.TestCheckResourceAttr(name, "name", rnd), + resource.TestCheckResourceAttr(name, "comment", rnd), + resource.TestCheckResourceAttr(name, "is_default_network", "false"), + ), + }, + }, + }) +} + +func testAccCheckCloudflareTunnelVirtualNetworkExists(name string, virtualNetwork *cloudflare.TunnelVirtualNetwork) 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 Tunnel Virtual Network is set") + } + + client := testAccProvider.Meta().(*cloudflare.API) + foundTunnelVirtualNetworks, err := client.ListTunnelVirtualNetworks(context.Background(), cloudflare.TunnelVirtualNetworksListParams{ + AccountID: rs.Primary.Attributes["account_id"], + IsDeleted: cloudflare.BoolPtr(false), + ID: rs.Primary.ID, + }) + + if err != nil { + return err + } + + *virtualNetwork = foundTunnelVirtualNetworks[0] + + return nil + } +} + +func TestAccCloudflareTunnelVirtualNetwork_UpdateComment(t *testing.T) { + rnd := generateRandomResourceName() + name := fmt.Sprintf("cloudflare_tunnel_virtual_network.%s", rnd) + accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID") + + var TunnelVirtualNetwork cloudflare.TunnelVirtualNetwork + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAccount(t) + }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testAccCloudflareTunnelVirtualNetworkSimple(rnd, rnd, accountID, rnd, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareTunnelVirtualNetworkExists(name, &TunnelVirtualNetwork), + resource.TestCheckResourceAttr(name, "comment", rnd), + ), + }, + { + Config: testAccCloudflareTunnelVirtualNetworkSimple(rnd, rnd+"-updated", accountID, rnd, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareTunnelVirtualNetworkExists(name, &TunnelVirtualNetwork), + resource.TestCheckResourceAttr(name, "comment", rnd+"-updated"), + ), + }, + }, + }) +} + +func testAccCloudflareTunnelVirtualNetworkSimple(ID, comment, accountID, name string, isDefault bool) string { + return fmt.Sprintf(` +resource "cloudflare_tunnel_virtual_network" "%[1]s" { + account_id = "%[3]s" + name = "%[4]s" + comment = "%[2]s" + is_default_network = "%[5]t" +}`, ID, comment, accountID, name, isDefault) +} diff --git a/internal/provider/schema_cloudflare_tunnel_virtual_network.go b/internal/provider/schema_cloudflare_tunnel_virtual_network.go new file mode 100644 index 0000000000..215d5b09db --- /dev/null +++ b/internal/provider/schema_cloudflare_tunnel_virtual_network.go @@ -0,0 +1,27 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudflareTunnelVirtualNetworkSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "account_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "is_default_network": { + Type: schema.TypeBool, + Optional: true, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + }, + } +} diff --git a/website/cloudflare.erb b/website/cloudflare.erb index cd5e793983..ed1fbd854e 100644 --- a/website/cloudflare.erb +++ b/website/cloudflare.erb @@ -199,6 +199,9 @@ > cloudflare_tunnel_route + > + cloudflare_tunnel_virtual_network + > cloudflare_waf_group diff --git a/website/docs/r/tunnel_virtual_network.html.markdown b/website/docs/r/tunnel_virtual_network.html.markdown new file mode 100644 index 0000000000..df58ce37b8 --- /dev/null +++ b/website/docs/r/tunnel_virtual_network.html.markdown @@ -0,0 +1,40 @@ +--- +layout: "cloudflare" +page_title: "Cloudflare: cloudflare_tunnel_virtual_network" +sidebar_content: "docs-cloudflare-tunnel-virtual_network" +description: |- + Provides a resource which manages Cloudflare Tunnel Virtual Networks for Zero Trust +--- + +# cloudflare_tunnel_virtual_network + +Provides a resource, that manages Cloudflare tunnel virtual networks for Zero Trust. Tunnel +virtual networks are used for segregation of Tunnel IP Routes via Virtualized Networks to +handle overlapping private IPs in your origins.. + +## Example Usage + +```hcl +resource "cloudflare_tunnel_virtual_network" "example" { + account_id = "c4a7362d577a6c3019a474fd6f485821" + name = "vnet-for-documentation" + comment = "New tunnel virtual network for documentation" +} +``` + +## Argument Reference + +The following arguments are supported: + +- `account_id` - (Required) The ID of the account where the tunnel virtual network is being created. +- `name` - (Required) A user-friendly name chosen when the virtual network is created. +- `is_default_network` - (Optional) Whether this virtual network is the default one for the account. This means IP Routes belong to this virtual network and Teams Clients in the account route through this virtual network, unless specified otherwise for each case. +- `comment` - (Optional) Description of the tunnel virtual network. + +## Import + +An existing tunnel virtual networks can be imported using the account ID and virtual network ID. + +``` +$ terraform import cloudflare_tunnel_virtual_network c4a7362d577a6c3019a474fd6f485821/3c8ff8af-b487-45bd-89e3-4c85a1532600 +```