Skip to content

Commit 056a37e

Browse files
author
Eduardo Gomes
committed
AUTH-6690 Add more fields to access application destinations
1 parent 8a39731 commit 056a37e

File tree

5 files changed

+143
-17
lines changed

5 files changed

+143
-17
lines changed

.changelog/4892.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/cloudflare_access_application: add private destination fields to access application
3+
```

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.23.3
44

55
require (
66
github.com/agext/levenshtein v1.2.3 // indirect
7-
github.com/cloudflare/cloudflare-go v0.113.0
7+
github.com/cloudflare/cloudflare-go v0.114.0
88
github.com/fatih/color v1.16.0 // indirect
99
github.com/google/uuid v1.6.0
1010
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -28,7 +28,7 @@ require (
2828
golang.org/x/net v0.34.0
2929
golang.org/x/sys v0.29.0 // indirect
3030
golang.org/x/text v0.21.0 // indirect
31-
golang.org/x/time v0.8.0 // indirect
31+
golang.org/x/time v0.9.0 // indirect
3232
)
3333

3434
require (

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc
5353
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
5454
github.com/cloudflare/cloudflare-go v0.113.0 h1:qnOXmA6RbgZ4rg5gNBK5QGk0Pzbv8pnUYV3C4+8CU6w=
5555
github.com/cloudflare/cloudflare-go v0.113.0/go.mod h1:Dlm4BAnycHc0i8yLxQZb9b+OlMwYOAoDJsUOEFgpVvo=
56+
github.com/cloudflare/cloudflare-go v0.114.0 h1:ucoti4/7Exo0XQ+rzpn1H+IfVVe++zgiM+tyKtf0HUA=
57+
github.com/cloudflare/cloudflare-go v0.114.0/go.mod h1:O7fYfFfA6wKqKFn2QIR9lhj7FDw6VQCGOY6hd2TBtd0=
5658
github.com/cloudflare/cloudflare-go/v2 v2.4.0 h1:gys/26GoVDklgfq8NYV39WgvOEwzK/XAqYObmnI6iFg=
5759
github.com/cloudflare/cloudflare-go/v2 v2.4.0/go.mod h1:AoIzb05z/rvdJLztPct4tSa+3IqXJJ6c+pbUFMOlTr8=
5860
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -266,6 +268,8 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
266268
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
267269
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
268270
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
271+
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
272+
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
269273
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
270274
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
271275
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

internal/sdkv2provider/resource_cloudflare_access_application_test.go

+27-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package sdkv2provider
33
import (
44
"context"
55
"fmt"
6+
"github.com/google/uuid"
67
"log"
78
"os"
89
"regexp"
10+
"strings"
911
"testing"
1012

1113
"github.com/cloudflare/cloudflare-go"
@@ -954,11 +956,18 @@ func TestAccCloudflareAccessApplication_WithDestinations(t *testing.T) {
954956
Check: resource.ComposeTestCheckFunc(
955957
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
956958
resource.TestCheckResourceAttr(name, "name", rnd),
957-
resource.TestCheckResourceAttr(name, "destinations.#", "2"),
959+
resource.TestCheckResourceAttr(name, "destinations.#", "4"),
958960
resource.TestCheckResourceAttr(name, "destinations.0.type", "public"),
959961
resource.TestCheckResourceAttr(name, "destinations.0.uri", fmt.Sprintf("d1.%s.%s", rnd, domain)),
960962
resource.TestCheckResourceAttr(name, "destinations.1.type", "public"),
961963
resource.TestCheckResourceAttr(name, "destinations.1.uri", fmt.Sprintf("d2.%s.%s", rnd, domain)),
964+
resource.TestCheckResourceAttr(name, "destinations.2.type", "private"),
965+
resource.TestCheckResourceAttr(name, "destinations.2.hostname", fmt.Sprintf("d1.%s.%s.privatenetwork", rnd, domain)),
966+
resource.TestCheckResourceAttr(name, "destinations.2.port_range", "443"),
967+
resource.TestCheckResourceAttr(name, "destinations.2.l4_protocol", "udp"),
968+
resource.TestCheckResourceAttr(name, "destinations.3.type", "private"),
969+
resource.TestCheckResourceAttr(name, "destinations.3.cidr", "127.0.0.2/32"),
970+
resource.TestCheckResourceAttrWith(name, "destinations.3.vnet_id", uuid.Validate),
962971
resource.TestCheckResourceAttr(name, "name", rnd),
963972
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
964973
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
@@ -1248,7 +1257,7 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
12481257
name = "rank"
12491258
}
12501259
}
1251-
1260+
12521261
hybrid_and_implicit_options {
12531262
return_id_token_from_authorization_endpoint = true
12541263
return_access_token_from_authorization_endpoint = true
@@ -1485,6 +1494,10 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
14851494
}
14861495

14871496
func testAccCloudflareAccessApplicationWithDestinations(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {
1497+
// make sure the seed string has at least 16 bytes to fill the UUID
1498+
vnetSeed := strings.Repeat(rnd, 16)
1499+
vnetID, _ := uuid.FromBytes([]byte(vnetSeed[:16]))
1500+
14881501
return fmt.Sprintf(`
14891502
resource "cloudflare_zero_trust_access_application" "%[1]s" {
14901503
%[3]s_id = "%[4]s"
@@ -1498,8 +1511,19 @@ resource "cloudflare_zero_trust_access_application" "%[1]s" {
14981511
destinations {
14991512
uri = "d2.%[1]s.%[2]s"
15001513
}
1514+
destinations {
1515+
type = "private"
1516+
hostname = "d1.%[1]s.%[2]s.privatenetwork"
1517+
port_range = "443"
1518+
l4_protocol = "udp"
1519+
}
1520+
destinations {
1521+
type = "private"
1522+
cidr = "127.0.0.2"
1523+
vnet_id = "%[5]s"
1524+
}
15011525
}
1502-
`, rnd, domain, identifier.Type, identifier.Identifier)
1526+
`, rnd, domain, identifier.Type, identifier.Identifier, vnetID)
15031527
}
15041528

15051529
func testAccCloudflareAccessApplicationWithDestinations2(rnd string, domain string, identifier *cloudflare.ResourceContainer) string {

internal/sdkv2provider/schema_cloudflare_access_application.go

+107-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sdkv2provider
33
import (
44
"fmt"
55
"regexp"
6+
"strings"
67
"time"
78

89
"github.com/cloudflare/cloudflare-go"
@@ -63,7 +64,7 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
6364
Type: schema.TypeString,
6465
Optional: true,
6566
Computed: true,
66-
ValidateFunc: validation.StringInSlice([]string{"public", "private"}, false),
67+
ValidateFunc: validation.StringInSlice([]string{"public"}, false),
6768
Description: fmt.Sprintf("The type of the primary domain. %s", renderAvailableDocumentationValuesStringSlice([]string{"public", "private"})),
6869
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
6970
appType := d.Get("type").(string)
@@ -91,8 +92,57 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
9192
},
9293
"uri": {
9394
Type: schema.TypeString,
94-
Required: true,
95-
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.",
95+
Optional: true,
96+
Description: "The public URI of the destination. Can include a domain and path with wildcards. Only valid when type=public",
97+
},
98+
"hostname": {
99+
Type: schema.TypeString,
100+
Optional: true,
101+
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.",
102+
},
103+
"cidr": {
104+
Type: schema.TypeString,
105+
Optional: true,
106+
Computed: true,
107+
DiffSuppressOnRefresh: true,
108+
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
109+
// /32 is the same as ommiting the mask for an IPV4
110+
// And /128 for an ipv6
111+
oldIsIpv4 := strings.Count(oldValue, ".") == 3
112+
newIsIpv4 := strings.Count(newValue, ".") == 3
113+
114+
if oldIsIpv4 && newIsIpv4 {
115+
return (strings.HasSuffix(oldValue, "/32") && !strings.Contains(newValue, "/")) ||
116+
(strings.HasSuffix(newValue, "/32") && !strings.Contains(oldValue, "/"))
117+
}
118+
119+
return (strings.HasSuffix(oldValue, "/128") && !strings.Contains(newValue, "/")) ||
120+
(strings.HasSuffix(newValue, "/128") && !strings.Contains(oldValue, "/"))
121+
},
122+
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.",
123+
},
124+
"port_range": {
125+
Type: schema.TypeString,
126+
Optional: true,
127+
Computed: true,
128+
DiffSuppressOnRefresh: true,
129+
DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
130+
// Passing a number is the same a range of length 1
131+
// E.g., "443" == "443-443"
132+
return newValue == fmt.Sprintf("%s-%s", oldValue, oldValue) || oldValue == fmt.Sprintf("%s-%s", newValue, newValue)
133+
},
134+
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.",
135+
},
136+
"vnet_id": {
137+
Type: schema.TypeString,
138+
Optional: true,
139+
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.",
140+
},
141+
"l4_protocol": {
142+
Type: schema.TypeString,
143+
ValidateFunc: validation.StringInSlice([]string{"tcp", "udp"}, false),
144+
Optional: true,
145+
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.",
96146
},
97147
},
98148
},
@@ -1030,6 +1080,38 @@ func convertSaasSchemaToStruct(d *schema.ResourceData) *cloudflare.SaasApplicati
10301080
}
10311081
}
10321082

1083+
func convertPublicDestinationStruct(payload map[string]any) cloudflare.AccessDestination {
1084+
dest := cloudflare.AccessDestination{
1085+
Type: cloudflare.AccessDestinationPublic,
1086+
}
1087+
if uri, ok := payload["uri"].(string); ok {
1088+
dest.URI = uri
1089+
}
1090+
return dest
1091+
}
1092+
1093+
func convertPrivateDestinationStruct(payload map[string]any) cloudflare.AccessDestination {
1094+
dest := cloudflare.AccessDestination{
1095+
Type: cloudflare.AccessDestinationPrivate,
1096+
}
1097+
if hostname, ok := payload["hostname"].(string); ok {
1098+
dest.Hostname = hostname
1099+
}
1100+
if ip, ok := payload["cidr"].(string); ok {
1101+
dest.CIDR = ip
1102+
}
1103+
if portRange, ok := payload["port_range"].(string); ok {
1104+
dest.PortRange = portRange
1105+
}
1106+
if l4Protocol, ok := payload["l4_protocol"].(string); ok {
1107+
dest.L4Protocol = l4Protocol
1108+
}
1109+
if vnetID, ok := payload["vnet_id"].(string); ok {
1110+
dest.VnetID = vnetID
1111+
}
1112+
return dest
1113+
}
1114+
10331115
func convertDestinationsToStruct(destinationPayloads []interface{}) ([]cloudflare.AccessDestination, error) {
10341116
destinations := make([]cloudflare.AccessDestination, len(destinationPayloads))
10351117
for i, dp := range destinationPayloads {
@@ -1038,17 +1120,13 @@ func convertDestinationsToStruct(destinationPayloads []interface{}) ([]cloudflar
10381120
if dType, ok := dpMap["type"].(string); ok {
10391121
switch dType {
10401122
case "public":
1041-
destinations[i].Type = cloudflare.AccessDestinationPublic
1123+
destinations[i] = convertPublicDestinationStruct(dpMap)
10421124
case "private":
1043-
destinations[i].Type = cloudflare.AccessDestinationPrivate
1125+
destinations[i] = convertPrivateDestinationStruct(dpMap)
10441126
default:
10451127
return nil, fmt.Errorf("failed to parse destination type: value must be one of public or private")
10461128
}
10471129
}
1048-
1049-
if uri, ok := dpMap["uri"].(string); ok {
1050-
destinations[i].URI = uri
1051-
}
10521130
}
10531131

10541132
return destinations, nil
@@ -1558,10 +1636,27 @@ func convertScimConfigMappingsStructsToSchema(mappingsData []*cloudflare.AccessA
15581636
func convertDestinationsToSchema(destinations []cloudflare.AccessDestination) []interface{} {
15591637
schemas := make([]interface{}, len(destinations))
15601638
for i, dest := range destinations {
1561-
schemas[i] = map[string]interface{}{
1562-
"type": string(dest.Type),
1563-
"uri": dest.URI,
1639+
resultDest := make(map[string]interface{})
1640+
resultDest["type"] = string(dest.Type)
1641+
if dest.URI != "" {
1642+
resultDest["uri"] = dest.URI
1643+
}
1644+
if dest.Hostname != "" {
1645+
resultDest["hostname"] = dest.Hostname
1646+
}
1647+
if dest.CIDR != "" {
1648+
resultDest["cidr"] = dest.CIDR
1649+
}
1650+
if dest.PortRange != "" {
1651+
resultDest["port_range"] = dest.PortRange
1652+
}
1653+
if dest.L4Protocol != "" {
1654+
resultDest["l4_protocol"] = dest.L4Protocol
1655+
}
1656+
if dest.VnetID != "" {
1657+
resultDest["vnet_id"] = dest.VnetID
15641658
}
1659+
schemas[i] = resultDest
15651660
}
15661661
return schemas
15671662
}

0 commit comments

Comments
 (0)