diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee1bfee8..e72373a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - main + - feature/convert-resource-object paths-ignore: - 'README.md' @@ -15,6 +16,7 @@ on: - 'README.md' branches: - main + - feature/convert-resource-object # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: @@ -118,7 +120,7 @@ jobs: name: Matrix Acceptance Tests needs: build runs-on: ubuntu-latest - if: "!github.event.pull_request.head.repo.fork" +# if: "!github.event.pull_request.head.repo.fork" timeout-minutes: 15 strategy: fail-fast: false @@ -169,7 +171,7 @@ jobs: cleanup: name: Cleanup - if: "!github.event.pull_request.head.repo.fork" +# if: "!github.event.pull_request.head.repo.fork" needs: tests-acceptance runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/go.mod b/go.mod index 46600cba..cabb4dfc 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.4 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/terraform-plugin-docs v0.16.0 - github.com/hashicorp/terraform-plugin-framework v1.4.1 + github.com/hashicorp/terraform-plugin-framework v1.4.2 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 github.com/hashicorp/terraform-plugin-go v0.19.0 github.com/hashicorp/terraform-plugin-testing v1.5.1 @@ -92,7 +92,7 @@ require ( golang.org/x/tools v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect - google.golang.org/grpc v1.57.0 // indirect + google.golang.org/grpc v1.57.1 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 74e47db8..c307c33f 100644 --- a/go.sum +++ b/go.sum @@ -148,8 +148,8 @@ github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQH github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFccGyBZn52KtMNsS12dI= github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA= -github.com/hashicorp/terraform-plugin-framework v1.4.1 h1:ZC29MoB3Nbov6axHdgPbMz7799pT5H8kIrM8YAsaVrs= -github.com/hashicorp/terraform-plugin-framework v1.4.1/go.mod h1:XC0hPcQbBvlbxwmjxuV/8sn8SbZRg4XwGMs22f+kqV0= +github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI= +github.com/hashicorp/terraform-plugin-framework v1.4.2/go.mod h1:GWl3InPFZi2wVQmdVnINPKys09s9mLmTZr95/ngLnbY= github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg= github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU= @@ -399,8 +399,8 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= -google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg= +google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= diff --git a/twingate/internal/provider/resource/helper_test.go b/twingate/internal/provider/resource/helper_test.go index d0a5b1f8..37bc74f1 100644 --- a/twingate/internal/provider/resource/helper_test.go +++ b/twingate/internal/provider/resource/helper_test.go @@ -153,3 +153,37 @@ func stringPtr(s string) *string { func boolPtr(b bool) *bool { return &b } + +func TestIsWildcardAddress(t *testing.T) { + cases := []struct { + address string + expected bool + }{ + { + address: "hello.com", + expected: false, + }, + { + address: "*.hello.com", + expected: true, + }, + { + address: "redis-?-blah.internal", + expected: true, + }, + { + address: "redis-*-blah.internal", + expected: true, + }, + { + address: "10.0.0.0/16", + expected: true, + }, + } + + for n, c := range cases { + t.Run(fmt.Sprintf("case_%d", n), func(t *testing.T) { + assert.Equal(t, c.expected, isWildcardAddress(c.address)) + }) + } +} diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 1d85d942..c18270e2 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "regexp" "strings" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" @@ -33,10 +34,11 @@ import ( ) var ( - ErrPortsWithPolicyAllowAll = errors.New(model.PolicyAllowAll + " policy does not allow specifying ports.") - ErrPortsWithPolicyDenyAll = errors.New(model.PolicyDenyAll + " policy does not allow specifying ports.") - ErrPolicyRestrictedWithoutPorts = errors.New(model.PolicyRestricted + " policy requires specifying ports.") - ErrInvalidAttributeCombination = errors.New("invalid attribute combination") + ErrPortsWithPolicyAllowAll = errors.New(model.PolicyAllowAll + " policy does not allow specifying ports.") + ErrPortsWithPolicyDenyAll = errors.New(model.PolicyDenyAll + " policy does not allow specifying ports.") + ErrPolicyRestrictedWithoutPorts = errors.New(model.PolicyRestricted + " policy requires specifying ports.") + ErrInvalidAttributeCombination = errors.New("invalid attribute combination") + ErrWildcardAddressWithEnabledShortcut = errors.New("Resources with a CIDR range or wildcard can't have the browser shortcut enabled.") ) // Ensure the implementation satisfies the desired interfaces. @@ -150,10 +152,9 @@ func (r *twingateResource) Schema(_ context.Context, _ resource.SchemaRequest, r PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, }, attr.IsBrowserShortcutEnabled: schema.BoolAttribute{ - Optional: true, - Computed: true, - Description: `Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client.`, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + Optional: true, + Computed: true, + Description: `Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client.`, }, attr.ID: schema.StringAttribute{ Computed: true, @@ -395,18 +396,10 @@ func convertResource(plan *resourceModel) (*model.Resource, error) { return nil, ErrInvalidAttributeCombination } - var isVisible, isBrowserShortcutEnabled *bool - if !plan.IsVisible.IsUnknown() { - isVisible = plan.IsVisible.ValueBoolPointer() - } + isBrowserShortcutEnabled := getOptionalBool(plan.IsBrowserShortcutEnabled) - if !plan.IsBrowserShortcutEnabled.IsUnknown() { - isBrowserShortcutEnabled = plan.IsBrowserShortcutEnabled.ValueBoolPointer() - } - - var alias *string - if !plan.Alias.IsUnknown() && !plan.Alias.IsNull() { - alias = plan.Alias.ValueStringPointer() + if isBrowserShortcutEnabled != nil && *isBrowserShortcutEnabled && isWildcardAddress(plan.Address.ValueString()) { + return nil, ErrWildcardAddressWithEnabledShortcut } return &model.Resource{ @@ -417,12 +410,28 @@ func convertResource(plan *resourceModel) (*model.Resource, error) { Groups: groupIDs, ServiceAccounts: serviceAccountIDs, IsAuthoritative: convertAuthoritativeFlag(plan.IsAuthoritative), - Alias: alias, - IsVisible: isVisible, + Alias: getOptionalString(plan.Alias), + IsVisible: getOptionalBool(plan.IsVisible), IsBrowserShortcutEnabled: isBrowserShortcutEnabled, }, nil } +func getOptionalBool(val types.Bool) *bool { + if !val.IsUnknown() { + return val.ValueBoolPointer() + } + + return nil +} + +func getOptionalString(val types.String) *string { + if !val.IsUnknown() && !val.IsNull() { + return val.ValueStringPointer() + } + + return nil +} + func convertIDs(list types.Set) []string { return utils.Map(list.Elements(), func(item tfattr.Value) string { return item.(types.String).ValueString() @@ -1075,3 +1084,9 @@ func (m caseInsensitiveDiffModifier) PlanModifyString(ctx context.Context, req p resp.PlanValue = req.StateValue } } + +var cidrRgxp = regexp.MustCompile(`(\d{1,3}\.){3}\d{1,3}(/\d+)?`) + +func isWildcardAddress(address string) bool { + return strings.ContainsAny(address, "*?") || cidrRgxp.MatchString(address) +} diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 9c94e2df..b6d280b4 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -2454,3 +2454,124 @@ func TestAccTwingateResourceTestCaseInsensitiveAlias(t *testing.T) { }, }) } + +func TestAccTwingateResourceWithBrowserOption(t *testing.T) { + const terraformResourceName = "test40" + theResource := acctests.TerraformResource(terraformResourceName) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + wildcardAddress := "*.acc-test.com" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, false), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, true), + ExpectError: regexp.MustCompile("Resources with a CIDR range or wildcard"), + }, + }, + }) +} + +func TestAccTwingateResourceWithBrowserOptionFailOnUpdate(t *testing.T) { + const terraformResourceName = "test41" + theResource := acctests.TerraformResource(terraformResourceName) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + wildcardAddress := "*.acc-test.com" + simpleAddress := "acc-test.com" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress, true), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress, true), + ExpectError: regexp.MustCompile("Resources with a CIDR range or wildcard"), + }, + }, + }) +} + +func TestAccTwingateResourceWithBrowserOptionRecovered(t *testing.T) { + const terraformResourceName = "test42" + theResource := acctests.TerraformResource(terraformResourceName) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + wildcardAddress := "*.acc-test.com" + simpleAddress := "acc-test.com" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceWithBrowserOption(terraformResourceName, remoteNetworkName, resourceName, simpleAddress, true), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + { + Config: createResourceWithoutBrowserOption(terraformResourceName, remoteNetworkName, resourceName, wildcardAddress), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + ), + }, + }, + }) +} + +func createResourceWithoutBrowserOption(name, networkName, resourceName, address string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[2]s" + } + resource "twingate_resource" "%[1]s" { + name = "%[3]s" + address = "%[4]s" + remote_network_id = twingate_remote_network.%[1]s.id + } + `, name, networkName, resourceName, address) +} + +func createResourceWithBrowserOption(name, networkName, resourceName, address string, browserOption bool) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[2]s" + } + resource "twingate_resource" "%[1]s" { + name = "%[3]s" + address = "%[4]s" + remote_network_id = twingate_remote_network.%[1]s.id + is_browser_shortcut_enabled = %[5]v + } + `, name, networkName, resourceName, address, browserOption) +}