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

AUTH-6690 Add more fields to access application destinations #4892

Merged
merged 4 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all 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/4892.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/cloudflare_access_application: add private destination fields to access application
```
10 changes: 6 additions & 4 deletions docs/resources/access_application.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,15 @@ Optional:
<a id="nestedblock--destinations"></a>
### Nested Schema for `destinations`

Required:

- `uri` (String) The URI of the destination. Public destinations can include a domain and path with wildcards. Private destinations are an early access feature and gated behind a feature flag. Private destinations support private IPv4, IPv6, and Server Name Indications (SNI) with optional port ranges.

Optional:

- `cidr` (String) The private CIDR of the destination. Only valid when type=private. IPs are computed as /32 cidr. Private destinations are an early access feature and gated behind a feature flag.
- `hostname` (String) The private hostname of the destination. Only valid when type=private. Private hostnames currently match only Server Name Indications (SNI). Private destinations are an early access feature and gated behind a feature flag.
- `l4_protocol` (String) The l4 protocol that matches this destination. Only valid when type=private. Private destinations are an early access feature and gated behind a feature flag.
- `port_range` (String) The port range of the destination. Only valid when type=private. Single ports are supported. Private destinations are an early access feature and gated behind a feature flag.
- `type` (String) The destination type. Available values: `public`, `private`. Defaults to `public`.
- `uri` (String) The public URI of the destination. Can include a domain and path with wildcards. Only valid when type=public.
- `vnet_id` (String) The VNet ID of the destination. Only valid when type=private. Private destinations are an early access feature and gated behind a feature flag.


<a id="nestedblock--footer_links"></a>
Expand Down
10 changes: 6 additions & 4 deletions docs/resources/zero_trust_access_application.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,15 @@ Optional:
<a id="nestedblock--destinations"></a>
### Nested Schema for `destinations`

Required:

- `uri` (String) The URI of the destination. Public destinations can include a domain and path with wildcards. Private destinations are an early access feature and gated behind a feature flag. Private destinations support private IPv4, IPv6, and Server Name Indications (SNI) with optional port ranges.

Optional:

- `cidr` (String) The private CIDR of the destination. Only valid when type=private. IPs are computed as /32 cidr. Private destinations are an early access feature and gated behind a feature flag.
- `hostname` (String) The private hostname of the destination. Only valid when type=private. Private hostnames currently match only Server Name Indications (SNI). Private destinations are an early access feature and gated behind a feature flag.
- `l4_protocol` (String) The l4 protocol that matches this destination. Only valid when type=private. Private destinations are an early access feature and gated behind a feature flag.
- `port_range` (String) The port range of the destination. Only valid when type=private. Single ports are supported. Private destinations are an early access feature and gated behind a feature flag.
- `type` (String) The destination type. Available values: `public`, `private`. Defaults to `public`.
- `uri` (String) The public URI of the destination. Can include a domain and path with wildcards. Only valid when type=public.
- `vnet_id` (String) The VNet ID of the destination. Only valid when type=private. Private destinations are an early access feature and gated behind a feature flag.


<a id="nestedblock--footer_links"></a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package sdkv2provider
import (
"context"
"fmt"
"github.com/google/uuid"
"log"
"os"
"regexp"
"strings"
"testing"

"github.com/cloudflare/cloudflare-go"
Expand Down Expand Up @@ -954,11 +956,18 @@ func TestAccCloudflareAccessApplication_WithDestinations(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "destinations.#", "2"),
resource.TestCheckResourceAttr(name, "destinations.#", "4"),
resource.TestCheckResourceAttr(name, "destinations.0.type", "public"),
resource.TestCheckResourceAttr(name, "destinations.0.uri", fmt.Sprintf("d1.%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "destinations.1.type", "public"),
resource.TestCheckResourceAttr(name, "destinations.1.uri", fmt.Sprintf("d2.%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "destinations.2.type", "private"),
resource.TestCheckResourceAttr(name, "destinations.2.hostname", fmt.Sprintf("d1.%s.%s.privatenetwork", rnd, domain)),
resource.TestCheckResourceAttr(name, "destinations.2.port_range", "443"),
resource.TestCheckResourceAttr(name, "destinations.2.l4_protocol", "udp"),
resource.TestCheckResourceAttr(name, "destinations.3.type", "private"),
resource.TestCheckResourceAttr(name, "destinations.3.cidr", "127.0.0.2/32"),
resource.TestCheckResourceAttrWith(name, "destinations.3.vnet_id", uuid.Validate),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
Expand Down Expand Up @@ -1248,7 +1257,7 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
name = "rank"
}
}

hybrid_and_implicit_options {
return_id_token_from_authorization_endpoint = true
return_access_token_from_authorization_endpoint = true
Expand Down Expand Up @@ -1485,6 +1494,10 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
}

func testAccCloudflareAccessApplicationWithDestinations(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
// make sure the seed string has at least 16 bytes to fill the UUID
vnetSeed := strings.Repeat(rnd, 16)
vnetID, _ := uuid.FromBytes([]byte(vnetSeed[:16]))

return fmt.Sprintf(`
resource "cloudflare_zero_trust_access_application" "%[1]s" {
%[3]s_id = "%[4]s"
Expand All @@ -1498,8 +1511,19 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
destinations {
uri = "d2.%[1]s.%[2]s"
}
destinations {
type = "private"
hostname = "d1.%[1]s.%[2]s.privatenetwork"
port_range = "443"
l4_protocol = "udp"
}
destinations {
type = "private"
cidr = "127.0.0.2"
vnet_id = "%[5]s"
}
}
`, rnd, domain, identifier.Type, identifier.Identifier)
`, rnd, domain, identifier.Type, identifier.Identifier, vnetID)
}

func testAccCloudflareAccessApplicationWithDestinations2(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
Expand Down
119 changes: 107 additions & 12 deletions internal/sdkv2provider/schema_cloudflare_access_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sdkv2provider
import (
"fmt"
"regexp"
"strings"
"time"

"github.com/cloudflare/cloudflare-go"
Expand Down Expand Up @@ -63,7 +64,7 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.StringInSlice([]string{"public", "private"}, false),
ValidateFunc: validation.StringInSlice([]string{"public"}, false),
Description: fmt.Sprintf("The type of the primary domain. %s", renderAvailableDocumentationValuesStringSlice([]string{"public", "private"})),
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
appType := d.Get("type").(string)
Expand Down Expand Up @@ -91,8 +92,57 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
},
"uri": {
Type: schema.TypeString,
Required: true,
Description: "The URI of the destination. Public destinations can include a domain and path with wildcards. Private destinations are an early access feature and gated behind a feature flag. Private destinations support private IPv4, IPv6, and Server Name Indications (SNI) with optional port ranges.",
Optional: true,
Description: "The public URI of the destination. Can include a domain and path with wildcards. Only valid when type=public",
},
"hostname": {
Type: schema.TypeString,
Optional: true,
Description: "The private hostname of the destination. Only valid when type=private. Private hostnames currently match only Server Name Indications (SNI). Private destinations are an early access feature and gated behind a feature flag.",
},
"cidr": {
Type: schema.TypeString,
Optional: true,
Computed: true,
DiffSuppressOnRefresh: true,
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
// /32 is the same as omitting the mask for an IPV4
// And /128 for an ipv6
oldIsIpv4 := strings.Count(oldValue, ".") == 3
newIsIpv4 := strings.Count(newValue, ".") == 3

if oldIsIpv4 && newIsIpv4 {
return (strings.HasSuffix(oldValue, "/32") && !strings.Contains(newValue, "/")) ||
(strings.HasSuffix(newValue, "/32") && !strings.Contains(oldValue, "/"))
}

return (strings.HasSuffix(oldValue, "/128") && !strings.Contains(newValue, "/")) ||
(strings.HasSuffix(newValue, "/128") && !strings.Contains(oldValue, "/"))
},
Description: "The private CIDR of the destination. Only valid when type=private. IPs are computed as /32 cidr. Private destinations are an early access feature and gated behind a feature flag.",
},
"port_range": {
Type: schema.TypeString,
Optional: true,
Computed: true,
DiffSuppressOnRefresh: true,
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
// Passing a number is the same a range of length 1
// E.g., "443" == "443-443"
return newValue == fmt.Sprintf("%s-%s", oldValue, oldValue) || oldValue == fmt.Sprintf("%s-%s", newValue, newValue)
},
Description: "The port range of the destination. Only valid when type=private. Single ports are supported. Private destinations are an early access feature and gated behind a feature flag.",
},
"vnet_id": {
Type: schema.TypeString,
Optional: true,
Description: "The VNet ID of the destination. Only valid when type=private. Private destinations are an early access feature and gated behind a feature flag.",
},
"l4_protocol": {
Type: schema.TypeString,
ValidateFunc: validation.StringInSlice([]string{"tcp", "udp"}, false),
Optional: true,
Description: "The l4 protocol that matches this destination. Only valid when type=private. Private destinations are an early access feature and gated behind a feature flag.",
},
},
},
Expand Down Expand Up @@ -1030,6 +1080,38 @@ func convertSaasSchemaToStruct(d *schema.ResourceData) *cloudflare.SaasApplicati
}
}

func convertPublicDestinationStruct(payload map[string]any) cloudflare.AccessDestination {
dest := cloudflare.AccessDestination{
Type: cloudflare.AccessDestinationPublic,
}
if uri, ok := payload["uri"].(string); ok {
dest.URI = uri
}
return dest
}

func convertPrivateDestinationStruct(payload map[string]any) cloudflare.AccessDestination {
dest := cloudflare.AccessDestination{
Type: cloudflare.AccessDestinationPrivate,
}
if hostname, ok := payload["hostname"].(string); ok {
dest.Hostname = hostname
}
if ip, ok := payload["cidr"].(string); ok {
dest.CIDR = ip
}
if portRange, ok := payload["port_range"].(string); ok {
dest.PortRange = portRange
}
if l4Protocol, ok := payload["l4_protocol"].(string); ok {
dest.L4Protocol = l4Protocol
}
if vnetID, ok := payload["vnet_id"].(string); ok {
dest.VnetID = vnetID
}
return dest
}

func convertDestinationsToStruct(destinationPayloads []interface{}) ([]cloudflare.AccessDestination, error) {
destinations := make([]cloudflare.AccessDestination, len(destinationPayloads))
for i, dp := range destinationPayloads {
Expand All @@ -1038,17 +1120,13 @@ func convertDestinationsToStruct(destinationPayloads []interface{}) ([]cloudflar
if dType, ok := dpMap["type"].(string); ok {
switch dType {
case "public":
destinations[i].Type = cloudflare.AccessDestinationPublic
destinations[i] = convertPublicDestinationStruct(dpMap)
case "private":
destinations[i].Type = cloudflare.AccessDestinationPrivate
destinations[i] = convertPrivateDestinationStruct(dpMap)
default:
return nil, fmt.Errorf("failed to parse destination type: value must be one of public or private")
}
}

if uri, ok := dpMap["uri"].(string); ok {
destinations[i].URI = uri
}
}

return destinations, nil
Expand Down Expand Up @@ -1558,10 +1636,27 @@ func convertScimConfigMappingsStructsToSchema(mappingsData []*cloudflare.AccessA
func convertDestinationsToSchema(destinations []cloudflare.AccessDestination) []interface{} {
schemas := make([]interface{}, len(destinations))
for i, dest := range destinations {
schemas[i] = map[string]interface{}{
"type": string(dest.Type),
"uri": dest.URI,
resultDest := make(map[string]interface{})
resultDest["type"] = string(dest.Type)
if dest.URI != "" {
resultDest["uri"] = dest.URI
}
if dest.Hostname != "" {
resultDest["hostname"] = dest.Hostname
}
if dest.CIDR != "" {
resultDest["cidr"] = dest.CIDR
}
if dest.PortRange != "" {
resultDest["port_range"] = dest.PortRange
}
if dest.L4Protocol != "" {
resultDest["l4_protocol"] = dest.L4Protocol
}
if dest.VnetID != "" {
resultDest["vnet_id"] = dest.VnetID
}
schemas[i] = resultDest
}
return schemas
}
Loading