diff --git a/examples/resources/twingate_resource/resource.tf b/examples/resources/twingate_resource/resource.tf index e8cafe01..f9af43a2 100644 --- a/examples/resources/twingate_resource/resource.tf +++ b/examples/resources/twingate_resource/resource.tf @@ -26,4 +26,5 @@ resource "twingate_resource" "resource" { policy = "ALLOW_ALL" } } -} \ No newline at end of file +} + diff --git a/twingate/gql_response.go b/twingate/gql_response.go index 2d860a29..1becb49d 100644 --- a/twingate/gql_response.go +++ b/twingate/gql_response.go @@ -44,6 +44,10 @@ type ProtocolsInput struct { } func (pi *ProtocolsInput) flattenProtocols() []interface{} { + if pi == nil { + return nil + } + protocols := make(map[string]interface{}) protocols["allow_icmp"] = pi.AllowIcmp diff --git a/twingate/resource_resource.go b/twingate/resource_resource.go index ff89aea6..c0122f68 100644 --- a/twingate/resource_resource.go +++ b/twingate/resource_resource.go @@ -38,6 +38,21 @@ func protocolDiff(k, oldValue, newValue string, d *schema.ResourceData) bool { return false } +func protocolsDiff(key, oldValue, newValue string, resourceData *schema.ResourceData) bool { + switch key { + case "protocols.#", "protocols.0.tcp.#", "protocols.0.udp.#": + return oldValue == "1" && newValue == "0" + + case "protocols.0.tcp.0.policy", "protocols.0.udp.0.policy": + oldPolicy, newPolicy := castToStrings(resourceData.GetChange(key)) + + return oldPolicy == newPolicy + + default: + return false + } +} + func equalPorts(a, b interface{}) bool { oldPorts, newPorts := convertPortsToSlice(a.([]interface{})), convertPortsToSlice(b.([]interface{})) @@ -139,10 +154,12 @@ func resourceResource() *schema.Resource { //nolint:funlen Description: "List of Group IDs that have permission to access the Resource, cannot be generated by Terraform and must be retrieved from the Twingate Admin Console or API", }, "protocols": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Restrict access to certain protocols and ports. By default or when this argument is not defined, there is no restriction, and all protocols and ports are allowed.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Restrict access to certain protocols and ports. By default or when this argument is not defined, there is no restriction, and all protocols and ports are allowed.", + DiffSuppressOnRefresh: true, + DiffSuppressFunc: protocolsDiff, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "allow_icmp": { @@ -330,6 +347,10 @@ func resourceResourceRead(ctx context.Context, resourceData *schema.ResourceData return diag.FromErr(err) } + if resource.Protocols == nil { + resource.Protocols = newEmptyProtocols() + } + if !resource.IsActive { // fix set active state for the resource on `terraform apply` err = client.updateResourceActiveState(ctx, &Resource{ @@ -364,11 +385,9 @@ func resourceResourceReadDiagnostics(resourceData *schema.ResourceData, resource return diag.FromErr(fmt.Errorf("error setting group_ids: %w ", err)) } - if len(resourceData.Get("protocols").([]interface{})) > 0 { - protocols := resource.Protocols.flattenProtocols() - if err := resourceData.Set("protocols", protocols); err != nil { - return diag.FromErr(fmt.Errorf("error setting protocols: %w ", err)) - } + protocols := resource.Protocols.flattenProtocols() + if err := resourceData.Set("protocols", protocols); err != nil { + return diag.FromErr(fmt.Errorf("error setting protocols: %w ", err)) } return diags diff --git a/twingate/resource_resource_test.go b/twingate/resource_resource_test.go index 54ee0cf5..de7865e0 100644 --- a/twingate/resource_resource_test.go +++ b/twingate/resource_resource_test.go @@ -59,7 +59,6 @@ func TestAccTwingateResource_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckTwingateResourceExists("twingate_resource.test"), resource.TestCheckNoResourceAttr("twingate_resource.test", "group_ids.#"), - resource.TestCheckNoResourceAttr("twingate_resource.test", "protocols.0.tcp.0.ports.0"), ), }, { @@ -661,3 +660,56 @@ func deleteTwingateResource(resourceName, resourceType string) resource.TestChec return nil } } + +func TestAccTwingateResource_import(t *testing.T) { + remoteNetworkName := acctest.RandomWithPrefix(testPrefixName) + groupName := acctest.RandomWithPrefix(testPrefixName + "-group") + groupName2 := acctest.RandomWithPrefix(testPrefixName + "-group") + resourceName := acctest.RandomWithPrefix(testPrefixName + "-resource") + + const terraformResourceName = "twingate_resource.test" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testTwingateResource_withProtocolsAndGroups(remoteNetworkName, groupName, groupName2, resourceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTwingateResourceExists(terraformResourceName), + ), + }, + { + ImportState: true, + ResourceName: terraformResourceName, + ImportStateCheck: func(data []*terraform.InstanceState) error { + if len(data) != 1 { + return fmt.Errorf("expected 1 resource, got %d", len(data)) + } + + attributes := []struct { + name string + expected string + }{ + {name: "address", expected: "updated-acc-test.com"}, + {name: "protocols.0.tcp.0.policy", expected: policyRestricted}, + {name: "protocols.0.tcp.0.ports.#", expected: "2"}, + {name: "protocols.0.tcp.0.ports.0", expected: "80"}, + {name: "protocols.0.udp.0.policy", expected: policyAllowAll}, + {name: "protocols.0.udp.0.ports.#", expected: "0"}, + } + + res := data[0] + for _, attr := range attributes { + if res.Attributes[attr.name] != attr.expected { + return fmt.Errorf("attribute %s doesn't match, expected: %s, got: %s", attr.name, attr.expected, res.Attributes[attr.name]) + } + } + + return nil + }, + }, + }, + }) +}