From 3eb653b0cfeb7b50540e35f98e8a185d7f493838 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Wed, 30 Aug 2023 12:44:21 +0200 Subject: [PATCH 01/11] fix migration to access API --- .github/workflows/ci.yml | 4 +- .../client/query/resource-access-read.go | 16 + .../client/query/resource-groups-add.go | 9 - .../client/query/resource-groups-read.go | 16 - .../internal/client/query/resource-read.go | 42 +- .../query/resource-service-accounts-read.go | 16 - twingate/internal/client/resource.go | 147 +----- .../internal/provider/resource/resource.go | 11 +- twingate/internal/test/acctests/helper.go | 30 +- .../internal/test/client/resource_test.go | 463 +----------------- 10 files changed, 84 insertions(+), 670 deletions(-) create mode 100644 twingate/internal/client/query/resource-access-read.go delete mode 100644 twingate/internal/client/query/resource-groups-add.go delete mode 100644 twingate/internal/client/query/resource-groups-read.go delete mode 100644 twingate/internal/client/query/resource-service-accounts-read.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee1bfee8..3bf3ef99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,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 +169,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/twingate/internal/client/query/resource-access-read.go b/twingate/internal/client/query/resource-access-read.go new file mode 100644 index 00000000..def19ec4 --- /dev/null +++ b/twingate/internal/client/query/resource-access-read.go @@ -0,0 +1,16 @@ +package query + +import "github.com/hasura/go-graphql-client" + +type ReadResourceAccess struct { + Resource *gqlResourceAccess `graphql:"resource(id: $id)"` +} + +func (q ReadResourceAccess) IsEmpty() bool { + return q.Resource == nil +} + +type gqlResourceAccess struct { + ID graphql.ID + Access Access `graphql:"access(after: $accessEndCursor, first: $pageLimit)"` +} diff --git a/twingate/internal/client/query/resource-groups-add.go b/twingate/internal/client/query/resource-groups-add.go deleted file mode 100644 index 36461923..00000000 --- a/twingate/internal/client/query/resource-groups-add.go +++ /dev/null @@ -1,9 +0,0 @@ -package query - -type AddResourceGroups struct { - OkError `graphql:"resourceUpdate(id: $id, addedGroupIds: $groupIds)"` -} - -func (q AddResourceGroups) IsEmpty() bool { - return false -} diff --git a/twingate/internal/client/query/resource-groups-read.go b/twingate/internal/client/query/resource-groups-read.go deleted file mode 100644 index a7358df0..00000000 --- a/twingate/internal/client/query/resource-groups-read.go +++ /dev/null @@ -1,16 +0,0 @@ -package query - -import "github.com/hasura/go-graphql-client" - -type ReadResourceGroups struct { - Resource *gqlResourceGroups `graphql:"resource(id: $id)"` -} - -func (q ReadResourceGroups) IsEmpty() bool { - return q.Resource == nil -} - -type gqlResourceGroups struct { - ID graphql.ID - Groups Groups `graphql:"groups(after: $groupsEndCursor, first: $pageLimit)"` -} diff --git a/twingate/internal/client/query/resource-read.go b/twingate/internal/client/query/resource-read.go index 8b20bc68..1219f567 100644 --- a/twingate/internal/client/query/resource-read.go +++ b/twingate/internal/client/query/resource-read.go @@ -6,6 +6,13 @@ import ( "github.com/hasura/go-graphql-client" ) +const ( + AccessGroup = "Group" + AccessServiceAccount = "ServiceAccount" +) + +const CursorAccess = "accessEndCursor" + type ReadResource struct { Resource *gqlResource `graphql:"resource(id: $id)"` } @@ -16,8 +23,24 @@ func (q ReadResource) IsEmpty() bool { type gqlResource struct { ResourceNode - Groups Groups `graphql:"groups(after: $groupsEndCursor, first: $pageLimit)"` - ServiceAccounts ServiceAccounts `graphql:"serviceAccounts(after: $servicesEndCursor, first: $pageLimit)"` + Access Access `graphql:"access(after: $accessEndCursor, first: $pageLimit)"` +} + +type Access struct { + PaginatedResource[*AccessEdge] +} + +type AccessEdge struct { + Node Principal +} + +type Principal struct { + Type string `graphql:"__typename"` + Node `graphql:"... on Node"` +} + +type Node struct { + ID graphql.ID `json:"id"` } type ResourceNode struct { @@ -53,12 +76,15 @@ type PortRange struct { func (r gqlResource) ToModel() *model.Resource { resource := r.ResourceNode.ToModel() - resource.Groups = utils.Map[*GroupEdge, string](r.Groups.Edges, func(edge *GroupEdge) string { - return string(edge.Node.ID) - }) - resource.ServiceAccounts = utils.Map[*ServiceAccountEdge, string](r.ServiceAccounts.Edges, func(edge *ServiceAccountEdge) string { - return string(edge.Node.ID) - }) + + for _, access := range r.Access.Edges { + switch access.Node.Type { + case AccessGroup: + resource.Groups = append(resource.Groups, string(access.Node.ID)) + case AccessServiceAccount: + resource.ServiceAccounts = append(resource.ServiceAccounts, string(access.Node.ID)) + } + } return resource } diff --git a/twingate/internal/client/query/resource-service-accounts-read.go b/twingate/internal/client/query/resource-service-accounts-read.go deleted file mode 100644 index c267dc0b..00000000 --- a/twingate/internal/client/query/resource-service-accounts-read.go +++ /dev/null @@ -1,16 +0,0 @@ -package query - -import "github.com/hasura/go-graphql-client" - -type ReadResourceServiceAccounts struct { - Resource *gqlResourceServiceAccounts `graphql:"resource(id: $id)"` -} - -func (q ReadResourceServiceAccounts) IsEmpty() bool { - return q.Resource == nil -} - -type gqlResourceServiceAccounts struct { - ID graphql.ID - ServiceAccounts ServiceAccounts `graphql:"serviceAccounts(after: $servicesEndCursor, first: $pageLimit)"` -} diff --git a/twingate/internal/client/resource.go b/twingate/internal/client/resource.go index d7ff4396..8518c30e 100644 --- a/twingate/internal/client/resource.go +++ b/twingate/internal/client/resource.go @@ -70,9 +70,7 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) gqlNullable(input.IsVisible, "isVisible"), gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), - cursor(query.CursorUsers), - cursor(query.CursorGroups), - cursor(query.CursorServices), + cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -106,9 +104,7 @@ func (client *Client) ReadResource(ctx context.Context, resourceID string) (*mod variables := newVars( gqlID(resourceID), - cursor(query.CursorUsers), - cursor(query.CursorGroups), - cursor(query.CursorServices), + cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -117,46 +113,26 @@ func (client *Client) ReadResource(ctx context.Context, resourceID string) (*mod return nil, err } - if err := response.Resource.Groups.FetchPages(ctx, client.readResourceGroupsAfter, newVars(gqlID(resourceID))); err != nil { - return nil, err //nolint - } - - if err := response.Resource.ServiceAccounts.FetchPages(ctx, client.readResourceServiceAccountsAfter, newVars(gqlID(resourceID))); err != nil { + if err := response.Resource.Access.FetchPages(ctx, client.readResourceAccessAfter, newVars(gqlID(resourceID))); err != nil { return nil, err //nolint } return response.Resource.ToModel(), nil } -func (client *Client) readResourceGroupsAfter(ctx context.Context, variables map[string]interface{}, cursor string) (*query.PaginatedResource[*query.GroupEdge], error) { - opr := resourceResource.read() - - resourceID := string(variables["id"].(graphql.ID)) - variables[query.CursorGroups] = cursor - gqlNullable("", query.CursorUsers)(variables) - pageLimit(client.pageLimit)(variables) - - response := query.ReadResourceGroups{} - if err := client.query(ctx, &response, variables, opr, attr{id: resourceID}); err != nil { - return nil, err - } - - return &response.Resource.Groups.PaginatedResource, nil -} - -func (client *Client) readResourceServiceAccountsAfter(ctx context.Context, variables map[string]interface{}, cursor string) (*query.PaginatedResource[*query.ServiceAccountEdge], error) { +func (client *Client) readResourceAccessAfter(ctx context.Context, variables map[string]interface{}, cursor string) (*query.PaginatedResource[*query.AccessEdge], error) { opr := resourceResource.read() resourceID := string(variables["id"].(graphql.ID)) - variables[query.CursorServices] = cursor + variables[query.CursorAccess] = cursor pageLimit(client.pageLimit)(variables) - response := query.ReadResourceServiceAccounts{} + response := query.ReadResourceAccess{} if err := client.query(ctx, &response, variables, opr, attr{id: resourceID}); err != nil { return nil, err } - return &response.Resource.ServiceAccounts.PaginatedResource, nil + return &response.Resource.Access.PaginatedResource, nil } func (client *Client) ReadResources(ctx context.Context) ([]*model.Resource, error) { @@ -204,9 +180,7 @@ func (client *Client) UpdateResource(ctx context.Context, input *model.Resource) gqlNullable(input.IsVisible, "isVisible"), gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), - cursor(query.CursorUsers), - cursor(query.CursorGroups), - cursor(query.CursorServices), + cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -215,11 +189,7 @@ func (client *Client) UpdateResource(ctx context.Context, input *model.Resource) return nil, err } - if err := response.Entity.Groups.FetchPages(ctx, client.readResourceGroupsAfter, newVars(gqlID(input.ID))); err != nil { - return nil, err //nolint - } - - if err := response.Entity.ServiceAccounts.FetchPages(ctx, client.readResourceServiceAccountsAfter, newVars(gqlID(input.ID))); err != nil { + if err := response.Entity.Access.FetchPages(ctx, client.readResourceAccessAfter, newVars(gqlID(input.ID))); err != nil { return nil, err //nolint } @@ -296,105 +266,6 @@ func (client *Client) readResourcesByNameAfter(ctx context.Context, variables ma return &response.PaginatedResource, nil } -func (client *Client) DeleteResourceServiceAccounts(ctx context.Context, resourceID string, deleteServiceAccountIDs []string) error { - opr := resourceResource.update() - - if len(deleteServiceAccountIDs) == 0 { - return nil - } - - if resourceID == "" { - return opr.apiError(ErrGraphqlIDIsEmpty) - } - - resourcesToDelete := []string{resourceID} - - for _, serviceAccountID := range deleteServiceAccountIDs { - if err := client.UpdateServiceAccountRemoveResources(ctx, serviceAccountID, resourcesToDelete); err != nil { - return err - } - } - - return nil -} - -func (client *Client) AddResourceGroups(ctx context.Context, resource *model.Resource) error { - opr := resourceResource.update() - - if len(resource.Groups) == 0 { - return nil - } - - if resource.ID == "" { - return opr.apiError(ErrGraphqlIDIsEmpty) - } - - variables := newVars( - gqlID(resource.ID), - gqlIDs(resource.Groups, "groupIds"), - ) - - response := query.AddResourceGroups{} - - return client.mutate(ctx, &response, variables, opr, attr{id: resource.ID}) -} - -func (client *Client) DeleteResourceGroups(ctx context.Context, resourceID string, deleteGroupIDs []string) error { - opr := resourceResource.update() - - if len(deleteGroupIDs) == 0 { - return nil - } - - if resourceID == "" { - return opr.apiError(ErrGraphqlIDIsEmpty) - } - - variables := newVars( - gqlID(resourceID), - gqlIDs(deleteGroupIDs, "removedGroupIds"), - cursor(query.CursorGroups), - cursor(query.CursorUsers), - cursor(query.CursorServices), - pageLimit(client.pageLimit), - ) - - response := query.UpdateResourceRemoveGroups{} - - return client.mutate(ctx, &response, variables, opr, attr{id: resourceID}) -} - -func (client *Client) ReadResourceServiceAccounts(ctx context.Context, resourceID string) ([]string, error) { - serviceAccounts, err := client.ReadServiceAccounts(ctx) - if err != nil { - return nil, err - } - - serviceAccountIDs := make([]string, 0, len(serviceAccounts)) - - for _, account := range serviceAccounts { - if utils.Contains(account.Resources, resourceID) { - serviceAccountIDs = append(serviceAccountIDs, account.ID) - } - } - - return serviceAccountIDs, nil -} - -func (client *Client) AddResourceServiceAccountIDs(ctx context.Context, resource *model.Resource) error { - for _, serviceAccountID := range resource.ServiceAccounts { - _, err := client.UpdateServiceAccount(ctx, &model.ServiceAccount{ - ID: serviceAccountID, - Resources: []string{resource.ID}, - }) - if err != nil { - return err - } - } - - return nil -} - func (client *Client) RemoveResourceAccess(ctx context.Context, resourceID string, principalIDs []string) error { opr := resourceResourceAccess.delete() diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index d5706a1e..798ad35b 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -266,7 +266,7 @@ func (r *twingateResource) Create(ctx context.Context, req resource.CreateReques return } - if err = r.client.AddResourceServiceAccountIDs(ctx, resource); err != nil { + if err = r.client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts); err != nil { addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) return @@ -626,15 +626,6 @@ func (r *twingateResource) helper(ctx context.Context, resource *model.Resource, } } - remoteServiceAccounts, err := r.client.ReadResourceServiceAccounts(ctx, resource.ID) - if err != nil { - addErr(diagnostics, err, operationRead, TwingateServiceAccount) - - return - } - - resource.ServiceAccounts = remoteServiceAccounts - if !resource.IsAuthoritative { resource.Groups = setIntersection(getAccessAttribute(reference.Access, attr.GroupIDs), resource.Groups) resource.ServiceAccounts = setIntersection(getAccessAttribute(reference.Access, attr.ServiceAccountIDs), resource.ServiceAccounts) diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index 8c06b1ca..b6be08f5 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -16,7 +16,6 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -446,10 +445,7 @@ func AddResourceGroup(resourceName, groupName string) sdk.TestCheckFunc { return err } - err = providerClient.AddResourceGroups(context.Background(), &model.Resource{ - ID: resourceID, - Groups: []string{groupID}, - }) + err = providerClient.AddResourceAccess(context.Background(), resourceID, []string{groupID}) if err != nil { return fmt.Errorf("resource with ID %s failed to add group with ID %s: %w", resourceID, groupID, err) } @@ -470,7 +466,7 @@ func DeleteResourceGroup(resourceName, groupName string) sdk.TestCheckFunc { return err } - err = providerClient.DeleteResourceGroups(context.Background(), resourceID, []string{groupID}) + err = providerClient.RemoveResourceAccess(context.Background(), resourceID, []string{groupID}) if err != nil { return fmt.Errorf("resource with ID %s failed to delete group with ID %s: %w", resourceID, groupID, err) } @@ -527,10 +523,7 @@ func AddResourceServiceAccount(resourceName, serviceAccountName string) sdk.Test return err } - _, err = providerClient.UpdateServiceAccount(context.Background(), &model.ServiceAccount{ - ID: serviceAccountID, - Resources: []string{resourceID}, - }) + err = providerClient.AddResourceAccess(context.Background(), resourceID, []string{serviceAccountID}) if err != nil { return fmt.Errorf("resource with ID %s failed to add service account with ID %s: %w", resourceID, serviceAccountID, err) } @@ -551,7 +544,7 @@ func DeleteResourceServiceAccount(resourceName, serviceAccountName string) sdk.T return err } - err = providerClient.DeleteResourceServiceAccounts(context.Background(), resourceID, []string{serviceAccountID}) + err = providerClient.RemoveResourceAccess(context.Background(), resourceID, []string{serviceAccountID}) if err != nil { return fmt.Errorf("resource with ID %s failed to delete service account with ID %s: %w", resourceID, serviceAccountID, err) } @@ -572,21 +565,6 @@ func CheckResourceServiceAccountsLen(resourceName string, expectedServiceAccount return fmt.Errorf("resource with ID %s failed to read: %w", resourceID, err) } - serviceAccounts, err := providerClient.ReadServiceAccounts(context.Background()) - if err != nil { - return fmt.Errorf("failed to read service accounts: %w", err) - } - - serviceAccountIDs := make(map[string]bool) - - for _, account := range serviceAccounts { - if utils.Contains(account.Resources, resource.ID) { - serviceAccountIDs[account.ID] = true - } - } - - resource.ServiceAccounts = utils.MapKeys(serviceAccountIDs) - if len(resource.ServiceAccounts) != expectedServiceAccountsLen { return ErrServiceAccountsLenMismatch(expectedServiceAccountsLen, len(resource.ServiceAccounts)) } diff --git a/twingate/internal/test/client/resource_test.go b/twingate/internal/test/client/resource_test.go index f3421fef..f40bfbfd 100644 --- a/twingate/internal/test/client/resource_test.go +++ b/twingate/internal/test/client/resource_test.go @@ -128,7 +128,6 @@ func TestClientResourceReadOk(t *testing.T) { Groups: []string{ "group1", "group2", }, - ServiceAccounts: []string{}, Protocols: &model.Protocols{ UDP: &model.Protocol{ Ports: []*model.PortRange{}, @@ -158,18 +157,20 @@ func TestClientResourceReadOk(t *testing.T) { "remoteNetwork": { "id": "network1" }, - "groups": { + "access": { "pageInfo": { "hasNextPage": false }, "edges": [ { "node": { + "__typename": "Group", "id": "group1" } }, { "node": { + "__typename": "Group", "id": "group2" } } @@ -222,8 +223,7 @@ func TestClientResourceReadAllGroups(t *testing.T) { Groups: []string{ "group1", "group2", "group3", "group4", }, - ServiceAccounts: []string{}, - IsActive: true, + IsActive: true, Protocols: &model.Protocols{ UDP: &model.Protocol{ Ports: []*model.PortRange{}, @@ -253,7 +253,7 @@ func TestClientResourceReadAllGroups(t *testing.T) { "remoteNetwork": { "id": "network1" }, - "groups": { + "access": { "pageInfo": { "endCursor": "cur001", "hasNextPage": true @@ -261,14 +261,14 @@ func TestClientResourceReadAllGroups(t *testing.T) { "edges": [ { "node": { - "id": "group1", - "name": "Group1 name" + "__typename": "Group", + "id": "group1" } }, { "node": { - "id": "group2", - "name": "Group2 name" + "__typename": "Group", + "id": "group2" } } ] @@ -302,21 +302,21 @@ func TestClientResourceReadAllGroups(t *testing.T) { "data": { "resource": { "id": "resource1", - "groups": { + "access": { "pageInfo": { "hasNextPage": false }, "edges": [ { "node": { - "id": "group3", - "name": "Group3 name" + "__typename": "Group", + "id": "group3" } }, { "node": { - "id": "group4", - "name": "Group4 name" + "__typename": "Group", + "id": "group4" } } ] @@ -402,7 +402,7 @@ func TestClientResourceReadGroupsRequestError(t *testing.T) { "data": { "resource": { "id": "resource1", - "groups": { + "access": { "pageInfo": { "endCursor": "cur001", "hasNextPage": true @@ -434,7 +434,7 @@ func TestClientResourceReadGroupsEmptyResponse(t *testing.T) { "data": { "resource": { "id": "resource1", - "groups": { + "access": { "pageInfo": { "endCursor": "cur001", "hasNextPage": true @@ -553,7 +553,7 @@ func TestClientResourceUpdateFetchGroupsError(t *testing.T) { "error" : null, "entity": { "id": "test", - "groups": { + "access": { "pageInfo": { "endCursor": "cur001", "hasNextPage": true @@ -561,6 +561,7 @@ func TestClientResourceUpdateFetchGroupsError(t *testing.T) { "edges": [ { "node": { + "__typename": "Group", "id": "group-1" } } @@ -1310,434 +1311,6 @@ func TestClientResourcesReadByNameAllRequestError(t *testing.T) { }) } -func TestClientDeleteResourceServiceAccountsWithEmptyServiceAccounts(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Service Accounts - With Empty Service Accounts", func(t *testing.T) { - client := newHTTPMockClient() - for _, serviceAccounts := range [][]string{nil, {}} { - err := client.DeleteResourceServiceAccounts(context.Background(), "resource-test", serviceAccounts) - - assert.NoError(t, err) - } - }) -} - -func TestClientDeleteResourceServiceAccountsWithEmptyResourceID(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Service Accounts - With Empty ResourceID", func(t *testing.T) { - client := newHTTPMockClient() - err := client.DeleteResourceServiceAccounts(context.Background(), "", []string{"serviceAccounts"}) - - assert.EqualError(t, err, "failed to update resource: id is empty") - }) -} - -func TestClientDeleteResourceServiceAccountsOk(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Service Accounts - Ok", func(t *testing.T) { - response1 := `{ - "data": { - "serviceAccount": { - "id": "serviceAccount1", - "name": "test" - } - } - }` - - response2 := `{ - "data": { - "serviceAccountUpdate": { - "entity": { - "id": "serviceAccount1", - "name": "tes" - }, - "ok": true, - "error": null - } - } - }` - - response3 := `{ - "data": { - "serviceAccount": { - "id": "serviceAccount2", - "name": "test" - } - } - }` - - response4 := `{ - "data": { - "serviceAccountUpdate": { - "entity": { - "id": "serviceAccount2", - "name": "test" - }, - "ok": true, - "error": null - } - } - }` - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - MultipleResponders( - httpmock.NewStringResponder(http.StatusOK, response1), - httpmock.NewStringResponder(http.StatusOK, response2), - httpmock.NewStringResponder(http.StatusOK, response3), - httpmock.NewStringResponder(http.StatusOK, response4), - ), - ) - - err := client.DeleteResourceServiceAccounts(context.Background(), "resource1", []string{"serviceAccount1", "serviceAccount2"}) - - assert.NoError(t, err) - }) -} - -func TestClientDeleteResourceServiceAccountsWithError(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Service Accounts - With Error", func(t *testing.T) { - client := newHTTPMockClient() - - err := client.DeleteResourceServiceAccounts(context.Background(), "resource1", []string{""}) - - assert.EqualError(t, err, `failed to update service account: id is empty`) - }) -} - -func TestClientResourcesAddResourceGroupsOk(t *testing.T) { - t.Run("Test Twingate Resource : Add Resource Groups - Ok", func(t *testing.T) { - jsonResponse := `{ - "data": { - "resourceUpdate": { - "ok": true, - "error": null - } - } - }` - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewStringResponder(http.StatusOK, jsonResponse)) - - err := client.AddResourceGroups(context.Background(), &model.Resource{ - ID: "resource-id", - Groups: []string{"g-1"}, - }) - - assert.NoError(t, err) - }) -} - -func TestClientResourcesAddResourceGroupsWithEmtpyGroups(t *testing.T) { - t.Run("Test Twingate Resource : Add Resource Groups - With Empty Groups", func(t *testing.T) { - client := newHTTPMockClient() - - err := client.AddResourceGroups(context.Background(), &model.Resource{ - ID: "resource-id", - }) - - assert.NoError(t, err) - }) -} - -func TestClientResourcesAddResourceGroupsWithEmtpyResourceID(t *testing.T) { - t.Run("Test Twingate Resource : Add Resource Groups - With Empty Resource ID", func(t *testing.T) { - client := newHTTPMockClient() - - err := client.AddResourceGroups(context.Background(), &model.Resource{ - Groups: []string{"g-1"}, - }) - - assert.EqualError(t, err, "failed to update resource: id is empty") - }) -} - -func TestClientResourcesAddResourceGroupsRequestError(t *testing.T) { - t.Run("Test Twingate Resource : Add Resource Groups - Request Error", func(t *testing.T) { - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewErrorResponder(errBadRequest)) - - err := client.AddResourceGroups(context.Background(), &model.Resource{ - ID: "resource-id", - Groups: []string{"g-1"}, - }) - - assert.EqualError(t, err, graphqlErr(client, "failed to update resource with id resource-id", errBadRequest)) - }) -} - -func TestClientResourcesAddResourceGroupsResponseError(t *testing.T) { - t.Run("Test Twingate Resource : Add Resource Groups - Response Error", func(t *testing.T) { - jsonResponse := `{ - "data": { - "resourceUpdate": { - "ok": false, - "error": "response error" - } - } - }` - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewStringResponder(http.StatusOK, jsonResponse)) - - err := client.AddResourceGroups(context.Background(), &model.Resource{ - ID: "resource-id", - Groups: []string{"g-1"}, - }) - - assert.EqualError(t, err, `failed to update resource with id resource-id: response error`) - }) -} - -func TestClientResourcesDeleteResourceGroupsOk(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Groups - Ok", func(t *testing.T) { - jsonResponse := `{ - "data": { - "resourceUpdate": { - "ok": true, - "error": null, - "entity": { - "id": "resource-id" - } - } - } - }` - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewStringResponder(http.StatusOK, jsonResponse)) - - err := client.DeleteResourceGroups(context.Background(), "resource-id", []string{"g-1"}) - - assert.NoError(t, err) - }) -} - -func TestClientResourcesDeleteResourceGroupsWithEmtpyGroups(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Groups - With Empty Groups", func(t *testing.T) { - client := newHTTPMockClient() - - err := client.DeleteResourceGroups(context.Background(), "resource-id", nil) - - assert.NoError(t, err) - }) -} - -func TestClientResourcesDeleteResourceGroupsWithEmtpyResourceID(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Groups - With Empty Resource ID", func(t *testing.T) { - client := newHTTPMockClient() - - err := client.DeleteResourceGroups(context.Background(), "", []string{"g-1"}) - - assert.EqualError(t, err, "failed to update resource: id is empty") - }) -} - -func TestClientResourcesDeleteResourceGroupsRequestError(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Groups - Request Error", func(t *testing.T) { - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewErrorResponder(errBadRequest)) - - err := client.DeleteResourceGroups(context.Background(), "resource-id", []string{"g-1"}) - - assert.EqualError(t, err, graphqlErr(client, "failed to update resource with id resource-id", errBadRequest)) - }) -} - -func TestClientResourcesDeleteResourceGroupsResponseError(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Groups - Response Error", func(t *testing.T) { - jsonResponse := `{ - "data": { - "resourceUpdate": { - "ok": false, - "error": "response error" - } - } - }` - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewStringResponder(http.StatusOK, jsonResponse)) - - err := client.DeleteResourceGroups(context.Background(), "resource-id", []string{"g-1"}) - - assert.EqualError(t, err, `failed to update resource with id resource-id: response error`) - }) -} - -func TestClientResourcesDeleteResourceGroupsEmptyResponse(t *testing.T) { - t.Run("Test Twingate Resource : Delete Resource Groups - Empty Response", func(t *testing.T) { - jsonResponse := `{ - "data": { - "resourceUpdate": { - "ok": true, - "error": null, - "entity": null - } - } - }` - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewStringResponder(http.StatusOK, jsonResponse)) - - err := client.DeleteResourceGroups(context.Background(), "resource-id", []string{"g-1"}) - - assert.EqualError(t, err, `failed to update resource with id resource-id: query result is empty`) - }) -} - -func TestClientReadResourceServiceAccountsOk(t *testing.T) { - t.Run("Test Twingate Resource : Read Resource Service Accounts - Ok", func(t *testing.T) { - jsonResponse := `{ - "data": { - "serviceAccounts": { - "pageInfo": { - "endCursor": "cursor-1", - "hasNextPage": false - }, - "edges": [ - { - "node": { - "id": "id-1", - "name": "test-1", - "resources": { - "pageInfo": { - "endCursor": null, - "hasNextPage": false - }, - "edges": [] - }, - "keys": null - } - }, - { - "node": { - "id": "id-2", - "name": "test-2", - "resources": { - "pageInfo": { - "endCursor": "cursor-2", - "hasNextPage": false - }, - "edges": [ - { - "node": { - "id": "resource-2-1", - "isActive": true - } - } - ] - }, - "keys": null - } - } - ] - } - } - }` - - expected := []string{"id-2"} - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewStringResponder(http.StatusOK, jsonResponse)) - - serviceAccounts, err := client.ReadResourceServiceAccounts(context.Background(), "resource-2-1") - - assert.NoError(t, err) - assert.ElementsMatch(t, expected, serviceAccounts) - }) -} - -func TestClientReadResourceServiceAccountsRequestError(t *testing.T) { - t.Run("Test Twingate Resource : Read Resource Service Accounts - Request Error", func(t *testing.T) { - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewErrorResponder(errBadRequest)) - - serviceAccounts, err := client.ReadResourceServiceAccounts(context.Background(), "resource-2-1") - - assert.EqualError(t, err, graphqlErr(client, "failed to read service account with id All", errBadRequest)) - assert.Nil(t, serviceAccounts) - }) -} - -func TestClientAddResourceServiceAccountIDsOk(t *testing.T) { - t.Run("Test Twingate Resource : Add Resource Service Account IDs - Ok", func(t *testing.T) { - response1 := `{ - "data": { - "serviceAccountUpdate": { - "entity": { - "id": "id-1", - "name": "name" - }, - "ok": true, - "error": null - } - } - }` - - response2 := `{ - "data": { - "serviceAccountUpdate": { - "entity": { - "id": "id-2", - "name": "name" - }, - "ok": true, - "error": null - } - } - }` - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - MultipleResponders( - httpmock.NewStringResponder(http.StatusOK, response1), - httpmock.NewStringResponder(http.StatusOK, response2), - ), - ) - - err := client.AddResourceServiceAccountIDs(context.Background(), &model.Resource{ - ID: "resource-1", - ServiceAccounts: []string{"id-1", "id-2"}, - }) - - assert.NoError(t, err) - }) -} - -func TestClientAddResourceServiceAccountIDsRequestError(t *testing.T) { - t.Run("Test Twingate Resource : Add Resource Service Account IDs - Request Error", func(t *testing.T) { - - client := newHTTPMockClient() - defer httpmock.DeactivateAndReset() - httpmock.RegisterResponder("POST", client.GraphqlServerURL, - httpmock.NewErrorResponder(errBadRequest)) - - err := client.AddResourceServiceAccountIDs(context.Background(), &model.Resource{ - ID: "resource-1", - ServiceAccounts: []string{"id-1", "id-2"}, - }) - - assert.EqualError(t, err, graphqlErr(client, "failed to update service account with id id-1", errBadRequest)) - }) -} - func TestClientRemoveResourceAccessOk(t *testing.T) { t.Run("Test Twingate Resource : Remove Resource Access - Ok", func(t *testing.T) { response := `{ From 11e1ee407e611e092691ab75a0d1a0f9c77e3ec8 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Wed, 30 Aug 2023 15:59:33 +0200 Subject: [PATCH 02/11] added security policy to resource main part --- docs/resources/resource.md | 1 + .../internal/client/query/resource-create.go | 2 +- .../internal/client/query/resource-read.go | 7 +++ .../internal/client/query/resource-update.go | 2 +- twingate/internal/client/resource.go | 2 + twingate/internal/client/variables.go | 2 +- twingate/internal/model/resource.go | 1 + .../internal/provider/resource/resource.go | 47 +++++++++++-------- .../test/acctests/resource/resource_test.go | 40 ++++++++++++++++ 9 files changed, 81 insertions(+), 23 deletions(-) diff --git a/docs/resources/resource.md b/docs/resources/resource.md index 6d6a32e4..5b96c8b3 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -70,6 +70,7 @@ resource "twingate_resource" "resource" { - `is_browser_shortcut_enabled` (Boolean) Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client. - `is_visible` (Boolean) Controls whether this Resource will be visible in the main Resource list in the Twingate Client. - `protocols` (Block List) 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. (see [below for nested schema](#nestedblock--protocols)) +- `security_policy_id` (String) The ID of a twingate_security_policy to set as this Resource's Security Policy. ### Read-Only diff --git a/twingate/internal/client/query/resource-create.go b/twingate/internal/client/query/resource-create.go index 17c697b5..7611a8e2 100644 --- a/twingate/internal/client/query/resource-create.go +++ b/twingate/internal/client/query/resource-create.go @@ -1,7 +1,7 @@ package query type CreateResource struct { - ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, groupIds: $groupIds, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias)"` + ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, groupIds: $groupIds, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId)"` } func (q CreateResource) IsEmpty() bool { diff --git a/twingate/internal/client/query/resource-read.go b/twingate/internal/client/query/resource-read.go index 1219f567..7dd78ec4 100644 --- a/twingate/internal/client/query/resource-read.go +++ b/twingate/internal/client/query/resource-read.go @@ -56,6 +56,7 @@ type ResourceNode struct { IsVisible bool IsBrowserShortcutEnabled bool Alias string + SecurityPolicy *gqlSecurityPolicy } type Protocols struct { @@ -90,6 +91,11 @@ func (r gqlResource) ToModel() *model.Resource { } func (r ResourceNode) ToModel() *model.Resource { + var securityPolicyID string + if r.SecurityPolicy != nil { + securityPolicyID = string(r.SecurityPolicy.ID) + } + return &model.Resource{ ID: string(r.ID), Name: r.Name, @@ -100,6 +106,7 @@ func (r ResourceNode) ToModel() *model.Resource { IsVisible: &r.IsVisible, IsBrowserShortcutEnabled: &r.IsBrowserShortcutEnabled, Alias: optionalString(r.Alias), + SecurityPolicyID: optionalString(securityPolicyID), } } diff --git a/twingate/internal/client/query/resource-update.go b/twingate/internal/client/query/resource-update.go index 419b4f57..4bdc0e5e 100644 --- a/twingate/internal/client/query/resource-update.go +++ b/twingate/internal/client/query/resource-update.go @@ -1,7 +1,7 @@ package query type UpdateResource struct { - ResourceEntityResponse `graphql:"resourceUpdate(id: $id, name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias)"` + ResourceEntityResponse `graphql:"resourceUpdate(id: $id, name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId)"` } func (q UpdateResource) IsEmpty() bool { diff --git a/twingate/internal/client/resource.go b/twingate/internal/client/resource.go index 8518c30e..297362c3 100644 --- a/twingate/internal/client/resource.go +++ b/twingate/internal/client/resource.go @@ -70,6 +70,7 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) gqlNullable(input.IsVisible, "isVisible"), gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), + gqlNullableID(input.SecurityPolicyID, "securityPolicyId"), cursor(query.CursorAccess), pageLimit(client.pageLimit), ) @@ -180,6 +181,7 @@ func (client *Client) UpdateResource(ctx context.Context, input *model.Resource) gqlNullable(input.IsVisible, "isVisible"), gqlNullable(input.IsBrowserShortcutEnabled, "isBrowserShortcutEnabled"), gqlNullable(input.Alias, "alias"), + gqlNullableID(input.SecurityPolicyID, "securityPolicyId"), cursor(query.CursorAccess), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/client/variables.go b/twingate/internal/client/variables.go index b9e6a945..89a9d64f 100644 --- a/twingate/internal/client/variables.go +++ b/twingate/internal/client/variables.go @@ -105,7 +105,7 @@ func getValue(val any) any { } } -func gqlNullableID(val interface{}, name string) gqlVarOption { +func gqlNullableID(val interface{}, name string) gqlVarOption { //nolint return func(values map[string]interface{}) map[string]interface{} { var ( gqlValue interface{} diff --git a/twingate/internal/model/resource.go b/twingate/internal/model/resource.go index 89b2be27..8a223350 100644 --- a/twingate/internal/model/resource.go +++ b/twingate/internal/model/resource.go @@ -33,6 +33,7 @@ type Resource struct { IsVisible *bool IsBrowserShortcutEnabled *bool Alias *string + SecurityPolicyID *string } func (r Resource) AccessToTerraform() []interface{} { diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 798ad35b..8e011ba2 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -55,6 +55,7 @@ type resourceModel struct { IsVisible types.Bool `tfsdk:"is_visible"` IsBrowserShortcutEnabled types.Bool `tfsdk:"is_browser_shortcut_enabled"` Alias types.String `tfsdk:"alias"` + SecurityPolicyID types.String `tfsdk:"security_policy_id"` } func (r *twingateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -128,11 +129,14 @@ func (r *twingateResource) Schema(_ context.Context, _ resource.SchemaRequest, r UseStateForUnknownBool(), }, }, - attr.Alias: schema.StringAttribute{ Optional: true, Description: "Set a DNS alias address for the Resource. Must be a DNS-valid name string.", }, + attr.SecurityPolicyID: schema.StringAttribute{ + Optional: true, + Description: "The ID of a twingate_security_policy to set as this Resource's Security Policy.", + }, // computed attr.IsVisible: schema.BoolAttribute{ @@ -147,7 +151,6 @@ func (r *twingateResource) Schema(_ context.Context, _ resource.SchemaRequest, r Description: `Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client.`, PlanModifiers: []planmodifier.Bool{UseStateForUnknownBool()}, }, - attr.ID: schema.StringAttribute{ Computed: true, Description: "Autogenerated ID of the Resource, encoded in base64", @@ -306,20 +309,6 @@ func convertResource(plan *resourceModel) (*model.Resource, error) { return nil, ErrInvalidAttributeCombination } - var isVisible, isBrowserShortcutEnabled *bool - if !plan.IsVisible.IsUnknown() { - isVisible = plan.IsVisible.ValueBoolPointer() - } - - if !plan.IsBrowserShortcutEnabled.IsUnknown() { - isBrowserShortcutEnabled = plan.IsBrowserShortcutEnabled.ValueBoolPointer() - } - - var alias *string - if !plan.Alias.IsUnknown() && !plan.Alias.IsNull() { - alias = plan.Alias.ValueStringPointer() - } - return &model.Resource{ Name: plan.Name.ValueString(), RemoteNetworkID: plan.RemoteNetworkID.ValueString(), @@ -328,12 +317,29 @@ func convertResource(plan *resourceModel) (*model.Resource, error) { Groups: groupIDs, ServiceAccounts: serviceAccountIDs, IsAuthoritative: convertAuthoritativeFlag(plan.IsAuthoritative), - Alias: alias, - IsVisible: isVisible, - IsBrowserShortcutEnabled: isBrowserShortcutEnabled, + Alias: getPlanOptionalStringVal(plan.Alias), + IsVisible: getPlanOptionalBoolVal(plan.IsVisible), + IsBrowserShortcutEnabled: getPlanOptionalBoolVal(plan.IsBrowserShortcutEnabled), + SecurityPolicyID: getPlanOptionalStringVal(plan.SecurityPolicyID), }, nil } +func getPlanOptionalStringVal(planValue types.String) *string { + if !planValue.IsUnknown() && !planValue.IsNull() { + return planValue.ValueStringPointer() + } + + return nil +} + +func getPlanOptionalBoolVal(planValue types.Bool) *bool { + if !planValue.IsUnknown() { + return planValue.ValueBoolPointer() + } + + return nil +} + func convertIDs(list types.Set) []string { return utils.Map(list.Elements(), func(item tfattr.Value) string { return item.(types.String).ValueString() @@ -528,7 +534,8 @@ func isResourceChanged(plan, state *resourceModel) bool { !plan.Protocols.Equal(state.Protocols) || !plan.IsVisible.Equal(state.IsVisible) || !plan.IsBrowserShortcutEnabled.Equal(state.IsBrowserShortcutEnabled) || - !plan.Alias.Equal(state.Alias) + !plan.Alias.Equal(state.Alias) || + !plan.SecurityPolicyID.Equal(state.SecurityPolicyID) } func (r *twingateResource) updateResourceAccess(ctx context.Context, plan, state *resourceModel, input *model.Resource) error { diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 1a9d9363..7c0b7201 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -2365,3 +2365,43 @@ func TestAccTwingateResourcePolicyTransitionAllowAllToDenyAll(t *testing.T) { }, }) } + +func TestAccTwingateResourceCreateWithSecurityPolicy(t *testing.T) { + const terraformResourceName = "test38" + theResource := acctests.TerraformResource(terraformResourceName) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceOnlyWithSecurityPolicy(terraformResourceName, remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttrWith(theResource, attr.SecurityPolicyID, nonEmptyValue), + ), + }, + }, + }) +} + +func createResourceOnlyWithSecurityPolicy(terraformResourceName, networkName, resourceName string) string { + return fmt.Sprintf(` + data "twingate_security_policy" "default" { + name = "Default Policy" + } + + resource "twingate_remote_network" "%s" { + name = "%s" + } + resource "twingate_resource" "%s" { + name = "%s" + address = "acc-test.com" + remote_network_id = twingate_remote_network.%s.id + security_policy_id = data.twingate_security_policy.default.id + } + `, terraformResourceName, networkName, terraformResourceName, resourceName, terraformResourceName) +} From dcc3ce97c04ec015b38bb920a21c743bc86b70c2 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Wed, 30 Aug 2023 21:05:17 +0200 Subject: [PATCH 03/11] wip --- .../internal/client/query/resource-create.go | 2 +- twingate/internal/client/resource.go | 6 +- twingate/internal/model/resource.go | 1 + .../internal/provider/resource/resource.go | 61 ++++++++++++++++--- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/twingate/internal/client/query/resource-create.go b/twingate/internal/client/query/resource-create.go index 7611a8e2..1be824c5 100644 --- a/twingate/internal/client/query/resource-create.go +++ b/twingate/internal/client/query/resource-create.go @@ -1,7 +1,7 @@ package query type CreateResource struct { - ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, groupIds: $groupIds, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId)"` + ResourceEntityResponse `graphql:"resourceCreate(name: $name, address: $address, remoteNetworkId: $remoteNetworkId, protocols: $protocols, isVisible: $isVisible, isBrowserShortcutEnabled: $isBrowserShortcutEnabled, alias: $alias, securityPolicyId: $securityPolicyId)"` } func (q CreateResource) IsEmpty() bool { diff --git a/twingate/internal/client/resource.go b/twingate/internal/client/resource.go index 297362c3..808687b3 100644 --- a/twingate/internal/client/resource.go +++ b/twingate/internal/client/resource.go @@ -63,7 +63,6 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) variables := newVars( gqlID(input.RemoteNetworkID, "remoteNetworkId"), - gqlIDs(input.Groups, "groupIds"), gqlVar(input.Name, "name"), gqlVar(input.Address, "address"), gqlVar(newProtocolsInput(input.Protocols), "protocols"), @@ -82,6 +81,7 @@ func (client *Client) CreateResource(ctx context.Context, input *model.Resource) resource := response.Entity.ToModel() resource.Groups = input.Groups + resource.GroupsSecurityPolicyID = input.GroupsSecurityPolicyID resource.ServiceAccounts = input.ServiceAccounts resource.IsAuthoritative = input.IsAuthoritative @@ -294,7 +294,7 @@ type AccessInput struct { SecurityPolicyID *string `json:"securityPolicyId"` } -func (client *Client) AddResourceAccess(ctx context.Context, resourceID string, principalIDs []string) error { +func (client *Client) AddResourceAccess(ctx context.Context, resourceID string, principalIDs []string, securityPolicyID *string) error { opr := resourceResourceAccess.update() if len(principalIDs) == 0 { @@ -306,7 +306,7 @@ func (client *Client) AddResourceAccess(ctx context.Context, resourceID string, } access := utils.Map(principalIDs, func(id string) AccessInput { - return AccessInput{PrincipalID: id} + return AccessInput{PrincipalID: id, SecurityPolicyID: securityPolicyID} }) variables := newVars( diff --git a/twingate/internal/model/resource.go b/twingate/internal/model/resource.go index 8a223350..bcda0a85 100644 --- a/twingate/internal/model/resource.go +++ b/twingate/internal/model/resource.go @@ -34,6 +34,7 @@ type Resource struct { IsBrowserShortcutEnabled *bool Alias *string SecurityPolicyID *string + GroupsSecurityPolicyID *string } func (r Resource) AccessToTerraform() []interface{} { diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 8e011ba2..f5e78c69 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -175,6 +175,10 @@ func accessBlock() schema.ListNestedBlock { Description: "Restrict access to certain groups or service accounts", NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ + attr.SecurityPolicyID: schema.StringAttribute{ + Optional: true, + Description: "The ID of a twingate_security_policy to use as the access policy for the group IDs in the access block.", + }, attr.GroupIDs: schema.SetAttribute{ Optional: true, ElementType: types.StringType, @@ -269,7 +273,13 @@ func (r *twingateResource) Create(ctx context.Context, req resource.CreateReques return } - if err = r.client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts); err != nil { + if err = r.client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts, nil); err != nil { + addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) + + return + } + + if err = r.client.AddResourceAccess(ctx, resource.ID, resource.Groups, resource.GroupsSecurityPolicyID); err != nil { addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) return @@ -279,6 +289,24 @@ func (r *twingateResource) Create(ctx context.Context, req resource.CreateReques } func getAccessAttribute(list types.List, attribute string) []string { + val := getAccessAttributeVal(list, attribute) + if val == nil { + return nil + } + + return convertIDs(val.(types.Set)) +} + +func getAccessSecurityPolicy(list types.List) string { + val := getAccessAttributeVal(list, attr.SecurityPolicyID) + if val == nil { + return "" + } + + return val.(types.String).ValueString() +} + +func getAccessAttributeVal(list types.List, attribute string) tfattr.Value { if list.IsNull() || list.IsUnknown() || len(list.Elements()) == 0 { return nil } @@ -293,7 +321,7 @@ func getAccessAttribute(list types.List, attribute string) []string { return nil } - return convertIDs(val.(types.Set)) + return val } func convertResource(plan *resourceModel) (*model.Resource, error) { @@ -304,17 +332,23 @@ func convertResource(plan *resourceModel) (*model.Resource, error) { groupIDs := getAccessAttribute(plan.Access, attr.GroupIDs) serviceAccountIDs := getAccessAttribute(plan.Access, attr.ServiceAccountIDs) + groupsSecurityPolicyID := getAccessSecurityPolicy(plan.Access) if !plan.Access.IsNull() && groupIDs == nil && serviceAccountIDs == nil { return nil, ErrInvalidAttributeCombination } + if len(serviceAccountIDs) > 0 && groupsSecurityPolicyID != "" { + return nil, ErrInvalidAttributeCombination + } + return &model.Resource{ Name: plan.Name.ValueString(), RemoteNetworkID: plan.RemoteNetworkID.ValueString(), Address: plan.Address.ValueString(), Protocols: protocols, Groups: groupIDs, + GroupsSecurityPolicyID: optionalString(groupsSecurityPolicyID), ServiceAccounts: serviceAccountIDs, IsAuthoritative: convertAuthoritativeFlag(plan.IsAuthoritative), Alias: getPlanOptionalStringVal(plan.Alias), @@ -324,6 +358,14 @@ func convertResource(plan *resourceModel) (*model.Resource, error) { }, nil } +func optionalString(s string) *string { + if s == "" { + return nil + } + + return &s +} + func getPlanOptionalStringVal(planValue types.String) *string { if !planValue.IsUnknown() && !planValue.IsNull() { return planValue.ValueStringPointer() @@ -539,7 +581,7 @@ func isResourceChanged(plan, state *resourceModel) bool { } func (r *twingateResource) updateResourceAccess(ctx context.Context, plan, state *resourceModel, input *model.Resource) error { - idsToDelete, idsToAdd, err := r.getChangedAccessIDs(ctx, plan, state, input) + idsToDelete, groupsToAdd, serviceAccountsToAdd, err := r.getChangedAccessIDs(ctx, plan, state, input) if err != nil { return fmt.Errorf("failed to update resource access: %w", err) } @@ -548,17 +590,22 @@ func (r *twingateResource) updateResourceAccess(ctx context.Context, plan, state return fmt.Errorf("failed to update resource access: %w", err) } - if err := r.client.AddResourceAccess(ctx, input.ID, idsToAdd); err != nil { + if err := r.client.AddResourceAccess(ctx, input.ID, serviceAccountsToAdd, nil); err != nil { + return fmt.Errorf("failed to update resource access: %w", err) + } + + securityPolicyID := optionalString(getAccessSecurityPolicy(plan.Access)) + if err := r.client.AddResourceAccess(ctx, input.ID, groupsToAdd, securityPolicyID); err != nil { return fmt.Errorf("failed to update resource access: %w", err) } return nil } -func (r *twingateResource) getChangedAccessIDs(ctx context.Context, plan, state *resourceModel, resource *model.Resource) ([]string, []string, error) { +func (r *twingateResource) getChangedAccessIDs(ctx context.Context, plan, state *resourceModel, resource *model.Resource) ([]string, []string, []string, error) { remote, err := r.client.ReadResource(ctx, resource.ID) if err != nil { - return nil, nil, fmt.Errorf("failed to get changedIDs: %w", err) + return nil, nil, nil, fmt.Errorf("failed to get changedIDs: %w", err) } var oldGroups, oldServiceAccounts []string @@ -577,7 +624,7 @@ func (r *twingateResource) getChangedAccessIDs(ctx context.Context, plan, state groupsToAdd := setDifference(resource.Groups, remote.Groups) serviceAccountsToAdd := setDifference(resource.ServiceAccounts, remote.ServiceAccounts) - return append(groupsToDelete, serviceAccountsToDelete...), append(groupsToAdd, serviceAccountsToAdd...), nil + return append(groupsToDelete, serviceAccountsToDelete...), groupsToAdd, serviceAccountsToAdd, nil } func getOldIDsNonAuthoritative(plan, state *resourceModel, attribute string) []string { From 1e837adfab50d7ca246b458c754baa8a8900f74c Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Tue, 5 Sep 2023 21:22:43 +0200 Subject: [PATCH 04/11] wip --- golangci.yml | 1 + .../internal/client/query/resource-read.go | 9 ++- .../internal/provider/resource/resource.go | 14 ++++- twingate/internal/test/acctests/helper.go | 4 +- .../test/acctests/resource/resource_test.go | 55 +++++++++++++++++++ 5 files changed, 78 insertions(+), 5 deletions(-) diff --git a/golangci.yml b/golangci.yml index df04e6c0..2b0f633a 100644 --- a/golangci.yml +++ b/golangci.yml @@ -22,6 +22,7 @@ linters-settings: - github.com/hashicorp/terraform-plugin-framework/datasource.DataSource - github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.Set - github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier.Bool + - github.com/hashicorp/terraform-plugin-framework/attr.Value errcheck: check-type-assertions: false check-blank: false diff --git a/twingate/internal/client/query/resource-read.go b/twingate/internal/client/query/resource-read.go index 7dd78ec4..d3c57b6b 100644 --- a/twingate/internal/client/query/resource-read.go +++ b/twingate/internal/client/query/resource-read.go @@ -31,7 +31,8 @@ type Access struct { } type AccessEdge struct { - Node Principal + Node Principal + SecurityPolicy *gqlSecurityPolicy } type Principal struct { @@ -82,6 +83,12 @@ func (r gqlResource) ToModel() *model.Resource { switch access.Node.Type { case AccessGroup: resource.Groups = append(resource.Groups, string(access.Node.ID)) + + if resource.GroupsSecurityPolicyID == nil && access.SecurityPolicy != nil { + id := string(access.SecurityPolicy.ID) + resource.GroupsSecurityPolicyID = &id + } + case AccessServiceAccount: resource.ServiceAccounts = append(resource.ServiceAccounts, string(access.Node.ID)) } diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index f5e78c69..008d3290 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -714,6 +714,10 @@ func setState(ctx context.Context, state, reference *resourceModel, resource *mo state.Alias = reference.Alias } + if !state.SecurityPolicyID.IsNull() || !reference.SecurityPolicyID.IsUnknown() { + state.SecurityPolicyID = reference.SecurityPolicyID + } + if !state.Protocols.IsNull() { protocols, diags := convertProtocolsToTerraform(ctx, resource.Protocols, reference.Protocols) diagnostics.Append(diags...) @@ -728,7 +732,8 @@ func setState(ctx context.Context, state, reference *resourceModel, resource *mo if !state.Access.IsNull() { access, diags := convertAccessBlockToTerraform(ctx, resource, state.Access.Elements()[0].(types.Object).Attributes()[attr.GroupIDs], - state.Access.Elements()[0].(types.Object).Attributes()[attr.ServiceAccountIDs]) + state.Access.Elements()[0].(types.Object).Attributes()[attr.ServiceAccountIDs], + state.Access.Elements()[0].(types.Object).Attributes()[attr.SecurityPolicyID]) diagnostics.Append(diags...) @@ -740,7 +745,7 @@ func setState(ctx context.Context, state, reference *resourceModel, resource *mo } } -func convertAccessBlockToTerraform(ctx context.Context, resource *model.Resource, stateGroupIDs, stateServiceAccounts tfattr.Value) (types.List, diag.Diagnostics) { +func convertAccessBlockToTerraform(ctx context.Context, resource *model.Resource, stateGroupIDs, stateServiceAccounts, stateSecurityPolicyID tfattr.Value) (types.List, diag.Diagnostics) { var diagnostics, diags diag.Diagnostics groupIDs, serviceAccountIDs := types.SetNull(types.StringType), types.SetNull(types.StringType) @@ -762,6 +767,7 @@ func convertAccessBlockToTerraform(ctx context.Context, resource *model.Resource attributes := map[string]tfattr.Value{ attr.GroupIDs: stateGroupIDs, attr.ServiceAccountIDs: stateServiceAccounts, + attr.SecurityPolicyID: stateSecurityPolicyID, } if !groupIDs.IsNull() { @@ -772,6 +778,10 @@ func convertAccessBlockToTerraform(ctx context.Context, resource *model.Resource attributes[attr.ServiceAccountIDs] = serviceAccountIDs } + if resource.GroupsSecurityPolicyID != nil { + attributes[attr.SecurityPolicyID] = types.StringPointerValue(resource.GroupsSecurityPolicyID) + } + obj, diags := types.ObjectValue(accessAttributeTypes(), attributes) diagnostics.Append(diags...) diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index b6be08f5..7177cad9 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -445,7 +445,7 @@ func AddResourceGroup(resourceName, groupName string) sdk.TestCheckFunc { return err } - err = providerClient.AddResourceAccess(context.Background(), resourceID, []string{groupID}) + err = providerClient.AddResourceAccess(context.Background(), resourceID, []string{groupID}, nil) if err != nil { return fmt.Errorf("resource with ID %s failed to add group with ID %s: %w", resourceID, groupID, err) } @@ -523,7 +523,7 @@ func AddResourceServiceAccount(resourceName, serviceAccountName string) sdk.Test return err } - err = providerClient.AddResourceAccess(context.Background(), resourceID, []string{serviceAccountID}) + err = providerClient.AddResourceAccess(context.Background(), resourceID, []string{serviceAccountID}, nil) if err != nil { return fmt.Errorf("resource with ID %s failed to add service account with ID %s: %w", resourceID, serviceAccountID, err) } diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 7c0b7201..b135775a 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -2405,3 +2405,58 @@ func createResourceOnlyWithSecurityPolicy(terraformResourceName, networkName, re } `, terraformResourceName, networkName, terraformResourceName, resourceName, terraformResourceName) } + +func TestAccTwingateResourceWithAccessServiceAccountsAndSecurityPolicy(t *testing.T) { + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + serviceAccountName := test.RandomName("s39") + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResource39(remoteNetworkName, resourceName, createServiceAccount(resourceName, serviceAccountName)), + ExpectError: regexp.MustCompile(resource.ErrInvalidAttributeCombination.Error()), + }, + }, + }) +} + +func createResource39(networkName, resourceName string, terraformServiceAccount string) string { + return fmt.Sprintf(` + data "twingate_security_policy" "default" { + name = "Default Policy" + } + + resource "twingate_remote_network" "test39" { + name = "%s" + } + + %s + + resource "twingate_resource" "test39" { + name = "%s" + address = "acc-test.com.39" + remote_network_id = twingate_remote_network.test39.id + + protocols { + allow_icmp = true + tcp { + policy = "%s" + ports = ["80", "82-83"] + } + udp { + policy = "%s" + } + } + + access { + service_account_ids = [%s] + security_policy_id = data.twingate_security_policy.default.id + } + + } + `, networkName, terraformServiceAccount, resourceName, model.PolicyRestricted, model.PolicyAllowAll, acctests.TerraformServiceAccount(resourceName)+".id") +} From deb985c6536149d440086b248bbf9c16b5094633 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Mon, 11 Sep 2023 17:57:18 +0200 Subject: [PATCH 05/11] wip --- twingate/internal/provider/resource/resource.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 008d3290..1fb93322 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -816,6 +816,10 @@ func convertAccessBlockToTerraformOnImport(ctx context.Context, resource *model. attr.ServiceAccountIDs: serviceAccountIDs, } + if resource.GroupsSecurityPolicyID != nil { + attributes[attr.SecurityPolicyID] = types.StringPointerValue(resource.GroupsSecurityPolicyID) + } + obj, diags := types.ObjectValue(accessAttributeTypes(), attributes) diagnostics.Append(diags...) From 6226227bbcab7bf0befc290c9edc395b35a42c7c Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 14 Sep 2023 06:31:42 +0200 Subject: [PATCH 06/11] revert resource to prevent breaking changes --- .github/workflows/ci.yml | 4 +- go.mod | 37 +- go.sum | 103 +- main.go | 39 +- twingate/internal/provider/resource/helper.go | 19 + .../internal/provider/resource/resource.go | 1232 +++++------------ twingate/internal/test/acctests/helper.go | 24 +- .../test/acctests/resource/resource_test.go | 6 +- twingate/provider.go | 240 ++-- twingate/v2/provider.go | 206 +++ 10 files changed, 819 insertions(+), 1091 deletions(-) create mode 100644 twingate/v2/provider.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee1bfee8..3bf3ef99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,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 +169,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 30acec16..4bafa6fb 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,11 @@ 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.3.5 + github.com/hashicorp/terraform-plugin-framework v1.4.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 - github.com/hashicorp/terraform-plugin-go v0.18.0 + github.com/hashicorp/terraform-plugin-go v0.19.0 + github.com/hashicorp/terraform-plugin-mux v0.12.0 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 github.com/hashicorp/terraform-plugin-testing v1.5.1 github.com/hasura/go-graphql-client v0.10.0 github.com/iancoleman/strcase v0.3.0 @@ -25,9 +27,9 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/Masterminds/sprig/v3 v3.2.2 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/agext/levenshtein v1.2.2 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/ccojocar/zxcvbn-go v1.0.1 // indirect @@ -46,20 +48,19 @@ require ( github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.4.10 // indirect + github.com/hashicorp/go-plugin v1.5.1 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hc-install v0.5.2 // indirect - github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/hashicorp/hc-install v0.6.0 // indirect + github.com/hashicorp/hcl/v2 v2.18.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.18.1 // indirect + github.com/hashicorp/terraform-exec v0.19.0 // indirect github.com/hashicorp/terraform-json v0.17.1 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.1 // indirect + github.com/hashicorp/terraform-registry-address v0.2.2 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/huandu/xstrings v1.3.2 // indirect - github.com/imdario/mergo v0.3.13 // indirect + github.com/imdario/mergo v0.3.15 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect @@ -79,19 +80,19 @@ require ( github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect - github.com/zclconf/go-cty v1.13.3 // indirect - golang.org/x/crypto v0.12.0 // indirect + github.com/zclconf/go-cty v1.14.0 // indirect + golang.org/x/crypto v0.13.0 // indirect golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/term v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.12.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect + google.golang.org/grpc v1.57.0 // 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 a40e3fc2..263c463c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= @@ -5,26 +6,26 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV4/4= github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -43,9 +44,9 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= +github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -62,6 +63,7 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -105,8 +107,8 @@ github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk= -github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= +github.com/hashicorp/go-plugin v1.5.1 h1:oGm7cWBaYIp3lJpx1RUEfLWophprE2EV/KUeqBYo+6k= +github.com/hashicorp/go-plugin v1.5.1/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -114,32 +116,34 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0= -github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI= -github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= -github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +github.com/hashicorp/hc-install v0.6.0 h1:fDHnU7JNFNSQebVKYhHZ0va1bC6SrPQ8fpebsvNr2w4= +github.com/hashicorp/hc-install v0.6.0/go.mod h1:10I912u3nntx9Umo1VAeYPUUuehk0aRQJYpMwbX5wQA= +github.com/hashicorp/hcl/v2 v2.18.0 h1:wYnG7Lt31t2zYkcquwgKo6MWXzRUDIeIVU5naZwHLl8= +github.com/hashicorp/hcl/v2 v2.18.0/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4= -github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980= +github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81SpgVtZNNtFSM= +github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg= github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA= 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.3.5 h1:FJ6s3CVWVAxlhiF/jhy6hzs4AnPHiflsp9KgzTGl1wo= -github.com/hashicorp/terraform-plugin-framework v1.3.5/go.mod h1:2gGDpWiTI0irr9NSTLFAKlTi6KwGti3AoU19rFqU30o= +github.com/hashicorp/terraform-plugin-framework v1.4.0 h1:WKbtCRtNrjsh10eA7NZvC/Qyr7zp77j+D21aDO5th9c= +github.com/hashicorp/terraform-plugin-framework v1.4.0/go.mod h1:XC0hPcQbBvlbxwmjxuV/8sn8SbZRg4XwGMs22f+kqV0= 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.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE= -github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y= +github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU= +github.com/hashicorp/terraform-plugin-go v0.19.0/go.mod h1:EhRSkEPNoylLQntYsk5KrDHTZJh9HQoumZXbOGOXmec= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0 h1:gY4SG34ANc6ZSeWEKC9hDTChY0ZiN+Myon17fSA0Xgc= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0/go.mod h1:deXEw/iJXtJxNV9d1c/OVJrvL7Zh0a++v7rzokW6wVY= +github.com/hashicorp/terraform-plugin-mux v0.12.0 h1:TJlmeslQ11WlQtIFAfth0vXx+gSNgvMEng2Rn9z3WZY= +github.com/hashicorp/terraform-plugin-mux v0.12.0/go.mod h1:8MR0AgmV+Q03DIjyrAKxXyYlq2EUnYBQP8gxAAA0zeM= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 h1:wcOKYwPI9IorAJEBLzgclh3xVolO7ZorYd6U1vnok14= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0/go.mod h1:qH/34G25Ugdj5FcM95cSoXzUgIbgfhVLXCcEcYaMwq8= github.com/hashicorp/terraform-plugin-testing v1.5.1 h1:T4aQh9JAhmWo4+t1A7x+rnxAJHCDIYW9kXyo4sVO92c= github.com/hashicorp/terraform-plugin-testing v1.5.1/go.mod h1:dg8clO6K59rZ8w9EshBmDp1CxTIPu3yA4iaDpX1h5u0= -github.com/hashicorp/terraform-registry-address v0.2.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM= -github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y= +github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno= +github.com/hashicorp/terraform-registry-address v0.2.2/go.mod h1:LtwNbCihUoUZ3RYriyS2wF/lGPB6gF9ICLRtuDk7hSo= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= @@ -152,12 +156,12 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -222,7 +226,7 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= @@ -253,16 +257,18 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.13.3 h1:m+b9q3YDbg6Bec5rr+KGy1MzEVzY/jC2X+YX4yqKtHI= -github.com/zclconf/go-cty v1.13.3/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc= +github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U= golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -278,7 +284,9 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= @@ -302,7 +310,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -310,26 +317,33 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -347,10 +361,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= -google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +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/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= @@ -365,7 +379,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/gotestsum v1.10.1 h1:TOV5xZVd5HDscBLSrPXpc4/MQm6QQr/YSI9iDC62d7E= diff --git a/main.go b/main.go index e2805d77..561a0aaa 100644 --- a/main.go +++ b/main.go @@ -6,27 +6,52 @@ import ( "log" "github.com/Twingate/terraform-provider-twingate/twingate" + twingateV2 "github.com/Twingate/terraform-provider-twingate/twingate/v2" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-mux/tf5to6server" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" ) var ( version = "dev" ) -const registry = "registry.terraform.io/Twingate/twingate" - func main() { var debug bool flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers") flag.Parse() - err := providerserver.Serve(context.Background(), twingate.New(version), - providerserver.ServeOpts{ - Debug: debug, - Address: registry, - ProtocolVersion: 6, + ctx := context.Background() + upgradedSdkProvider, err := tf5to6server.UpgradeServer(ctx, twingate.Provider(version).GRPCProvider) + if err != nil { + log.Fatal(err) + } + providers := []func() tfprotov6.ProviderServer{ + func() tfprotov6.ProviderServer { + return upgradedSdkProvider }, + providerserver.NewProtocol6(twingateV2.New(version)()), + } + + muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) + + if err != nil { + log.Fatal(err) + } + + var serveOpts []tf6server.ServeOpt + if debug { + serveOpts = append(serveOpts, tf6server.WithManagedDebug()) + } + + err = tf6server.Serve( + "registry.terraform.io/Twingate/twingate", + muxServer.ProviderServer, + serveOpts..., ) if err != nil { diff --git a/twingate/internal/provider/resource/helper.go b/twingate/internal/provider/resource/helper.go index 3a18a2e1..feb14fe5 100644 --- a/twingate/internal/provider/resource/helper.go +++ b/twingate/internal/provider/resource/helper.go @@ -5,8 +5,27 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" tfDiag "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +func ErrAttributeSet(err error, attribute string) diag.Diagnostics { + return diag.FromErr(fmt.Errorf("error setting %s: %w ", attribute, err)) +} + +func convertIDs(data interface{}) []string { + return utils.Map[interface{}, string]( + data.(*schema.Set).List(), + func(elem interface{}) string { + return elem.(string) + }, + ) +} + +func castToStrings(a, b interface{}) (string, string) { + return a.(string), b.(string) +} + // setIntersection - for given two sets A and B, // A ∩ B (read as A intersection B) is the set of common elements that belong to set A and B. // If A = {1, 2, 3, 4} and B = {3, 4, 5, 7}, then the intersection of A and B is given by A ∩ B = {3, 4}. diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index 798ad35b..7555031d 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -4,608 +4,272 @@ import ( "context" "errors" "fmt" + "log" "reflect" + "strings" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - tfattr "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) 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") ) -// Ensure the implementation satisfies the desired interfaces. -var _ resource.Resource = &twingateResource{} - -func NewResourceResource() resource.Resource { - return &twingateResource{} -} - -type twingateResource struct { - client *client.Client -} - -type resourceModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Address types.String `tfsdk:"address"` - RemoteNetworkID types.String `tfsdk:"remote_network_id"` - IsAuthoritative types.Bool `tfsdk:"is_authoritative"` - Protocols types.List `tfsdk:"protocols"` - Access types.List `tfsdk:"access"` - IsVisible types.Bool `tfsdk:"is_visible"` - IsBrowserShortcutEnabled types.Bool `tfsdk:"is_browser_shortcut_enabled"` - Alias types.String `tfsdk:"alias"` -} - -func (r *twingateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = TwingateResource -} - -func (r *twingateResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { - if req.ProviderData == nil { - return +func Resource() *schema.Resource { //nolint:funlen + portsSchema := &schema.Resource{ + Schema: map[string]*schema.Schema{ + attr.Policy: { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(model.Policies, false), + Description: fmt.Sprintf("Whether to allow or deny all ports, or restrict protocol access within certain port ranges: Can be `%s` (only listed ports are allowed), `%s`, or `%s`", model.PolicyRestricted, model.PolicyAllowAll, model.PolicyDenyAll), + }, + attr.Ports: { + Type: schema.TypeList, + Optional: true, + Description: "List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + DiffSuppressFunc: portsNotChanged, + }, + }, } - r.client = req.ProviderData.(*client.Client) -} - -func (r *twingateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root(attr.ID), req, resp) - - res, err := r.client.ReadResource(ctx, req.ID) - if err != nil { - resp.Diagnostics.AddError("failed to import state", err.Error()) - - return + protocolsSchema := &schema.Resource{ + Schema: map[string]*schema.Schema{ + attr.AllowIcmp: { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether to allow ICMP (ping) traffic", + }, + attr.TCP: { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: portsSchema, + }, + attr.UDP: { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: portsSchema, + }, + }, } - if res.Protocols != nil { - protocols, diags := convertProtocolsToTerraformOnImport(ctx, res.Protocols) - resp.Diagnostics.Append(diags...) - - if resp.Diagnostics.HasError() { - return - } - - resp.State.SetAttribute(ctx, path.Root(attr.Protocols), protocols) + accessSchema := &schema.Resource{ + Schema: map[string]*schema.Schema{ + attr.GroupIDs: { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + MinItems: 1, + Optional: true, + AtLeastOneOf: []string{attr.Path(attr.Access, attr.ServiceAccountIDs)}, + Description: "List of Group IDs that will have permission to access the Resource.", + }, + attr.ServiceAccountIDs: { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + MinItems: 1, + Optional: true, + AtLeastOneOf: []string{attr.Path(attr.Access, attr.GroupIDs)}, + Description: "List of Service Account IDs that will have permission to access the Resource.", + }, + }, } - if len(res.Groups) > 0 || len(res.ServiceAccounts) > 0 { - access, diags := convertAccessBlockToTerraformOnImport(ctx, res) - - resp.Diagnostics.Append(diags...) + return &schema.Resource{ + Description: "Resources in Twingate represent servers on the private network that clients can connect to. Resources can be defined by IP, CIDR range, FQDN, or DNS zone. For more information, see the Twingate [documentation](https://docs.twingate.com/docs/resources-and-access-nodes).", + CreateContext: resourceCreate, + UpdateContext: resourceUpdate, + ReadContext: resourceRead, + DeleteContext: resourceDelete, - if resp.Diagnostics.HasError() { - return - } - - resp.State.SetAttribute(ctx, path.Root(attr.Access), access) - } -} - -func (r *twingateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Resources in Twingate represent servers on the private network that clients can connect to. Resources can be defined by IP, CIDR range, FQDN, or DNS zone. For more information, see the Twingate [documentation](https://docs.twingate.com/docs/resources-and-access-nodes).", - Attributes: map[string]schema.Attribute{ - attr.Name: schema.StringAttribute{ + Schema: map[string]*schema.Schema{ + // required + attr.Name: { + Type: schema.TypeString, Required: true, Description: "The name of the Resource", }, - attr.Address: schema.StringAttribute{ + attr.Address: { + Type: schema.TypeString, Required: true, Description: "The Resource's IP/CIDR or FQDN/DNS zone", }, - attr.RemoteNetworkID: schema.StringAttribute{ + attr.RemoteNetworkID: { + Type: schema.TypeString, Required: true, Description: "Remote Network ID where the Resource lives", }, // optional - attr.IsAuthoritative: schema.BoolAttribute{ + attr.IsAuthoritative: { + Type: schema.TypeBool, Optional: true, Computed: true, Description: "Determines whether assignments in the access block will override any existing assignments. Default is `true`. If set to `false`, assignments made outside of Terraform will be ignored.", - PlanModifiers: []planmodifier.Bool{ - UseStateForUnknownBool(), - }, }, - - attr.Alias: schema.StringAttribute{ + attr.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.", + Elem: protocolsSchema, + DiffSuppressOnRefresh: true, + DiffSuppressFunc: protocolsNotChanged, + }, + attr.Access: { + Type: schema.TypeList, Optional: true, - Description: "Set a DNS alias address for the Resource. Must be a DNS-valid name string.", + MaxItems: 1, + Description: "Restrict access to certain groups or service accounts", + Elem: accessSchema, }, - // computed - attr.IsVisible: schema.BoolAttribute{ - Optional: true, - Computed: true, - Description: "Controls whether this Resource will be visible in the main Resource list in the Twingate Client.", - PlanModifiers: []planmodifier.Bool{UseStateForUnknownBool()}, - }, - 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{UseStateForUnknownBool()}, - }, - - attr.ID: schema.StringAttribute{ + attr.IsVisible: { + Type: schema.TypeBool, + Optional: true, Computed: true, - Description: "Autogenerated ID of the Resource, encoded in base64", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, + Description: "Controls whether this Resource will be visible in the main Resource list in the Twingate Client.", }, - }, - - Blocks: map[string]schema.Block{ - attr.Access: accessBlock(), - attr.Protocols: protocolsBlock(), - }, - } -} - -func accessBlock() schema.ListNestedBlock { - return schema.ListNestedBlock{ - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - }, - Description: "Restrict access to certain groups or service accounts", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - attr.GroupIDs: schema.SetAttribute{ - Optional: true, - ElementType: types.StringType, - Description: "List of Group IDs that will have permission to access the Resource.", - Validators: []validator.Set{ - setvalidator.SizeAtLeast(1), - }, - }, - attr.ServiceAccountIDs: schema.SetAttribute{ - Optional: true, - ElementType: types.StringType, - Description: "List of Service Account IDs that will have permission to access the Resource.", - Validators: []validator.Set{ - setvalidator.SizeAtLeast(1), - }, - }, + attr.IsBrowserShortcutEnabled: { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: `Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client.`, }, - }, - } -} - -func protocolsBlock() schema.ListNestedBlock { - return schema.ListNestedBlock{ - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - attr.AllowIcmp: schema.BoolAttribute{ - Computed: true, - Optional: true, - Description: "Whether to allow ICMP (ping) traffic", - }, + attr.Alias: { + Type: schema.TypeString, + Optional: true, + Description: "Set a DNS alias address for the Resource. Must be a DNS-valid name string.", + DiffSuppressFunc: aliasDiff, }, - Blocks: map[string]schema.Block{ - attr.TCP: protocolSchema(), - attr.UDP: protocolSchema(), + attr.ID: { + Type: schema.TypeString, + Computed: true, + Description: "Autogenerated ID of the Resource, encoded in base64", }, }, - 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.", - } -} - -func protocolSchema() schema.ListNestedBlock { - return schema.ListNestedBlock{ - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - attr.Policy: schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(model.Policies...), - }, - Description: fmt.Sprintf("Whether to allow or deny all ports, or restrict protocol access within certain port ranges: Can be `%s` (only listed ports are allowed), `%s`, or `%s`", model.PolicyRestricted, model.PolicyAllowAll, model.PolicyDenyAll), - }, - attr.Ports: schema.SetAttribute{ - Optional: true, - Computed: true, - ElementType: types.StringType, - Description: "List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port", - PlanModifiers: []planmodifier.Set{ - PortsDiff(), - }, - }, - }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, }, } } -func (r *twingateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan resourceModel - - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - - if resp.Diagnostics.HasError() { - return - } +func resourceCreate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*client.Client) - input, err := convertResource(&plan) + resource, err := convertResource(resourceData) if err != nil { - addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) - - return + return diag.FromErr(err) } - resource, err := r.client.CreateResource(ctx, input) + resource, err = client.CreateResource(ctx, resource) if err != nil { - addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) - - return - } - - if err = r.client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts); err != nil { - addErr(&resp.Diagnostics, err, operationCreate, TwingateResource) - - return - } - - r.helper(ctx, resource, &plan, &plan, &resp.State, &resp.Diagnostics, err, operationCreate) -} - -func getAccessAttribute(list types.List, attribute string) []string { - if list.IsNull() || list.IsUnknown() || len(list.Elements()) == 0 { - return nil + return diag.FromErr(err) } - obj := list.Elements()[0].(types.Object) - if obj.IsNull() || obj.IsUnknown() { - return nil + if err = client.AddResourceAccess(ctx, resource.ID, resource.ServiceAccounts); err != nil { + return diag.FromErr(err) } - val := obj.Attributes()[attribute] - if val == nil || val.IsNull() || val.IsUnknown() { - return nil - } + log.Printf("[INFO] Created resource %s", resource.Name) - return convertIDs(val.(types.Set)) + return resourceResourceReadHelper(ctx, client, resourceData, resource, nil) } -func convertResource(plan *resourceModel) (*model.Resource, error) { - protocols, err := convertProtocols(&plan.Protocols) - if err != nil { - return nil, err - } - - groupIDs := getAccessAttribute(plan.Access, attr.GroupIDs) - serviceAccountIDs := getAccessAttribute(plan.Access, attr.ServiceAccountIDs) - - if !plan.Access.IsNull() && groupIDs == nil && serviceAccountIDs == nil { - return nil, ErrInvalidAttributeCombination - } - - var isVisible, isBrowserShortcutEnabled *bool - if !plan.IsVisible.IsUnknown() { - isVisible = plan.IsVisible.ValueBoolPointer() - } - - if !plan.IsBrowserShortcutEnabled.IsUnknown() { - isBrowserShortcutEnabled = plan.IsBrowserShortcutEnabled.ValueBoolPointer() - } - - var alias *string - if !plan.Alias.IsUnknown() && !plan.Alias.IsNull() { - alias = plan.Alias.ValueStringPointer() - } +func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*client.Client) - return &model.Resource{ - Name: plan.Name.ValueString(), - RemoteNetworkID: plan.RemoteNetworkID.ValueString(), - Address: plan.Address.ValueString(), - Protocols: protocols, - Groups: groupIDs, - ServiceAccounts: serviceAccountIDs, - IsAuthoritative: convertAuthoritativeFlag(plan.IsAuthoritative), - Alias: alias, - IsVisible: isVisible, - IsBrowserShortcutEnabled: isBrowserShortcutEnabled, - }, nil -} - -func convertIDs(list types.Set) []string { - return utils.Map(list.Elements(), func(item tfattr.Value) string { - return item.(types.String).ValueString() - }) -} - -func convertProtocols(protocols *types.List) (*model.Protocols, error) { - if protocols == nil || protocols.IsNull() || len(protocols.Elements()) == 0 { - return model.DefaultProtocols(), nil - } - - udp, err := convertProtocol(protocols.Elements()[0].(types.Object).Attributes()[attr.UDP]) + resource, err := convertResource(resourceData) if err != nil { - return nil, err - } - - tcp, err := convertProtocol(protocols.Elements()[0].(types.Object).Attributes()[attr.TCP]) - if err != nil { - return nil, err - } - - return &model.Protocols{ - AllowIcmp: protocols.Elements()[0].(types.Object).Attributes()[attr.AllowIcmp].(types.Bool).ValueBool(), - UDP: udp, - TCP: tcp, - }, nil -} - -func convertProtocol(listVal tfattr.Value) (*model.Protocol, error) { - obj := convertProtocolObj(listVal) - if obj.IsNull() { - return nil, nil //nolint:nilnil - } - - ports, err := decodePorts(obj) - if err != nil { - return nil, err - } - - policy := obj.Attributes()[attr.Policy].(types.String).ValueString() - if err := isValidPolicy(policy, ports); err != nil { - return nil, err - } - - if policy == model.PolicyDenyAll { - policy = model.PolicyRestricted - } - - return model.NewProtocol(policy, ports), nil -} - -func convertProtocolObj(listVal tfattr.Value) types.Object { - if listVal == nil || listVal.IsNull() { - return types.ObjectNull(nil) - } - - list := listVal.(types.List) - - if list.IsNull() || len(list.Elements()) == 0 { - return types.ObjectNull(nil) - } - - protocol := list.Elements()[0] - - if protocol == nil || protocol.IsNull() { - return types.ObjectNull(nil) - } - - obj, ok := protocol.(types.Object) - if !ok || obj.IsNull() { - return types.ObjectNull(nil) - } - - return obj -} - -func isValidPolicy(policy string, ports []*model.PortRange) error { - switch policy { - case model.PolicyAllowAll: - if len(ports) > 0 { - return ErrPortsWithPolicyAllowAll - } - - case model.PolicyDenyAll: - if len(ports) > 0 { - return ErrPortsWithPolicyDenyAll - } - - case model.PolicyRestricted: - if len(ports) == 0 { - return ErrPolicyRestrictedWithoutPorts - } + return diag.FromErr(err) } - return nil -} + resource.ID = resourceData.Id() -func decodePorts(obj types.Object) ([]*model.PortRange, error) { - portsVal := obj.Attributes()[attr.Ports] - if portsVal == nil || portsVal.IsNull() { - return nil, nil - } - - portsList, ok := portsVal.(types.Set) - if !ok { - return nil, nil - } - - return convertPorts(portsList) -} - -func convertPorts(list types.Set) ([]*model.PortRange, error) { - items := list.Elements() - - var ports = make([]*model.PortRange, 0, len(items)) - - for _, port := range items { - portRange, err := model.NewPortRange(port.(types.String).ValueString()) + if resourceData.HasChange(attr.Access) { + idsToDelete, idsToAdd, err := getChangedAccessIDs(ctx, resourceData, resource, client) if err != nil { - return nil, err //nolint:wrapcheck + return diag.FromErr(err) } - ports = append(ports, portRange) - } - - return ports, nil -} - -func (r *twingateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state resourceModel - - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - - if resp.Diagnostics.HasError() { - return - } - - resource, err := r.client.ReadResource(ctx, state.ID.ValueString()) - if resource != nil { - resource.IsAuthoritative = convertAuthoritativeFlag(state.IsAuthoritative) - } - - r.helper(ctx, resource, &state, &state, &resp.State, &resp.Diagnostics, err, operationRead) -} - -func (r *twingateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan, state resourceModel - - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - - if resp.Diagnostics.HasError() { - return - } - - input, err := convertResource(&plan) - if err != nil { - addErr(&resp.Diagnostics, err, operationUpdate, TwingateResource) - - return - } - - input.ID = state.ID.ValueString() - - if !plan.Access.Equal(state.Access) { - if err := r.updateResourceAccess(ctx, &plan, &state, input); err != nil { - addErr(&resp.Diagnostics, err, operationUpdate, TwingateResource) + if err := client.RemoveResourceAccess(ctx, resource.ID, idsToDelete); err != nil { + return diag.FromErr(err) + } - return + if err = client.AddResourceAccess(ctx, resource.ID, idsToAdd); err != nil { + return diag.FromErr(err) } } - var resource *model.Resource - - if isResourceChanged(&plan, &state) { - resource, err = r.client.UpdateResource(ctx, input) + if resourceData.HasChanges( + attr.RemoteNetworkID, + attr.Name, + attr.Address, + attr.Protocols, + attr.IsVisible, + attr.IsBrowserShortcutEnabled, + attr.Alias, + ) { + resource, err = client.UpdateResource(ctx, resource) } else { - resource, err = r.client.ReadResource(ctx, input.ID) + resource, err = client.ReadResource(ctx, resource.ID) } if resource != nil { - resource.IsAuthoritative = input.IsAuthoritative + resource.IsAuthoritative = convertAuthoritativeFlagLegacy(resourceData) } - r.helper(ctx, resource, &state, &plan, &resp.State, &resp.Diagnostics, err, operationUpdate) -} + log.Printf("[INFO] Updated resource %s", resource.Name) -func isResourceChanged(plan, state *resourceModel) bool { - return !plan.RemoteNetworkID.Equal(state.RemoteNetworkID) || - !plan.Name.Equal(state.Name) || - !plan.Address.Equal(state.Address) || - !plan.Protocols.Equal(state.Protocols) || - !plan.IsVisible.Equal(state.IsVisible) || - !plan.IsBrowserShortcutEnabled.Equal(state.IsBrowserShortcutEnabled) || - !plan.Alias.Equal(state.Alias) + return resourceResourceReadHelper(ctx, client, resourceData, resource, err) } -func (r *twingateResource) updateResourceAccess(ctx context.Context, plan, state *resourceModel, input *model.Resource) error { - idsToDelete, idsToAdd, err := r.getChangedAccessIDs(ctx, plan, state, input) - if err != nil { - return fmt.Errorf("failed to update resource access: %w", err) - } +func resourceRead(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*client.Client) - if err := r.client.RemoveResourceAccess(ctx, input.ID, idsToDelete); err != nil { - return fmt.Errorf("failed to update resource access: %w", err) - } - - if err := r.client.AddResourceAccess(ctx, input.ID, idsToAdd); err != nil { - return fmt.Errorf("failed to update resource access: %w", err) + resource, err := client.ReadResource(ctx, resourceData.Id()) + if resource != nil { + resource.IsAuthoritative = convertAuthoritativeFlagLegacy(resourceData) } - return nil + return resourceResourceReadHelper(ctx, client, resourceData, resource, err) } -func (r *twingateResource) getChangedAccessIDs(ctx context.Context, plan, state *resourceModel, resource *model.Resource) ([]string, []string, error) { - remote, err := r.client.ReadResource(ctx, resource.ID) - if err != nil { - return nil, nil, fmt.Errorf("failed to get changedIDs: %w", err) - } +func resourceDelete(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + c := meta.(*client.Client) + resourceID := resourceData.Id() - var oldGroups, oldServiceAccounts []string - if resource.IsAuthoritative { - oldGroups, oldServiceAccounts = remote.Groups, remote.ServiceAccounts - } else { - oldGroups = getOldIDsNonAuthoritative(plan, state, attr.GroupIDs) - oldServiceAccounts = getOldIDsNonAuthoritative(plan, state, attr.ServiceAccountIDs) + err := c.DeleteResource(ctx, resourceID) + if err != nil { + return diag.FromErr(err) } - // ids to delete - groupsToDelete := setDifference(oldGroups, resource.Groups) - serviceAccountsToDelete := setDifference(oldServiceAccounts, resource.ServiceAccounts) - - // ids to add - groupsToAdd := setDifference(resource.Groups, remote.Groups) - serviceAccountsToAdd := setDifference(resource.ServiceAccounts, remote.ServiceAccounts) - - return append(groupsToDelete, serviceAccountsToDelete...), append(groupsToAdd, serviceAccountsToAdd...), nil -} - -func getOldIDsNonAuthoritative(plan, state *resourceModel, attribute string) []string { - if !plan.Access.Equal(state.Access) { - return getAccessAttribute(state.Access, attribute) - } + log.Printf("[INFO] Deleted resource id %s", resourceData.Id()) return nil } -func (r *twingateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state resourceModel - - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - - if resp.Diagnostics.HasError() { - return - } - - err := r.client.DeleteResource(ctx, state.ID.ValueString()) - addErr(&resp.Diagnostics, err, operationDelete, TwingateResource) -} - -func (r *twingateResource) helper(ctx context.Context, resource *model.Resource, state, reference *resourceModel, respState *tfsdk.State, diagnostics *diag.Diagnostics, err error, operation string) { +func resourceResourceReadHelper(ctx context.Context, resourceClient *client.Client, resourceData *schema.ResourceData, resource *model.Resource, err error) diag.Diagnostics { if err != nil { if errors.Is(err, client.ErrGraphqlResultIsEmpty) { // clear state - respState.RemoveResource(ctx) + resourceData.SetId("") - return + return nil } - addErr(diagnostics, err, operation, TwingateResource) - - return + return diag.FromErr(err) } if resource.Protocols == nil { @@ -614,494 +278,346 @@ func (r *twingateResource) helper(ctx context.Context, resource *model.Resource, if !resource.IsActive { // fix set active state for the resource on `terraform apply` - err = r.client.UpdateResourceActiveState(ctx, &model.Resource{ + err = resourceClient.UpdateResourceActiveState(ctx, &model.Resource{ ID: resource.ID, IsActive: true, }) if err != nil { - addErr(diagnostics, err, operationUpdate, TwingateResource) - - return + return diag.FromErr(err) } } if !resource.IsAuthoritative { - resource.Groups = setIntersection(getAccessAttribute(reference.Access, attr.GroupIDs), resource.Groups) - resource.ServiceAccounts = setIntersection(getAccessAttribute(reference.Access, attr.ServiceAccountIDs), resource.ServiceAccounts) + groups, serviceAccounts := convertAccess(resourceData) + resource.ServiceAccounts = setIntersection(serviceAccounts, resource.ServiceAccounts) + resource.Groups = setIntersection(groups, resource.Groups) } - setState(ctx, state, reference, resource, diagnostics) - - if diagnostics.HasError() { - return - } + resourceData.SetId(resource.ID) - // Set refreshed state - diagnostics.Append(respState.Set(ctx, state)...) + return readDiagnostics(resourceData, resource) } -func setState(ctx context.Context, state, reference *resourceModel, resource *model.Resource, diagnostics *diag.Diagnostics) { //nolint:cyclop - state.ID = types.StringValue(resource.ID) - state.Name = types.StringValue(resource.Name) - state.RemoteNetworkID = types.StringValue(resource.RemoteNetworkID) - state.Address = types.StringValue(resource.Address) - state.IsAuthoritative = types.BoolValue(resource.IsAuthoritative) - - if !state.IsVisible.IsNull() || !reference.IsVisible.IsUnknown() { - state.IsVisible = types.BoolPointerValue(resource.IsVisible) +func readDiagnostics(resourceData *schema.ResourceData, resource *model.Resource) diag.Diagnostics { //nolint:cyclop + if err := resourceData.Set(attr.Name, resource.Name); err != nil { + return ErrAttributeSet(err, attr.Name) } - if !state.IsBrowserShortcutEnabled.IsNull() || !reference.IsBrowserShortcutEnabled.IsUnknown() { - state.IsBrowserShortcutEnabled = types.BoolPointerValue(resource.IsBrowserShortcutEnabled) + if err := resourceData.Set(attr.RemoteNetworkID, resource.RemoteNetworkID); err != nil { + return ErrAttributeSet(err, attr.RemoteNetworkID) } - if !state.Alias.IsNull() || !reference.Alias.IsUnknown() { - state.Alias = reference.Alias + if err := resourceData.Set(attr.Address, resource.Address); err != nil { + return ErrAttributeSet(err, attr.Address) } - if !state.Protocols.IsNull() { - protocols, diags := convertProtocolsToTerraform(ctx, resource.Protocols, reference.Protocols) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return - } - - state.Protocols = protocols + if err := resourceData.Set(attr.IsAuthoritative, resource.IsAuthoritative); err != nil { + return ErrAttributeSet(err, attr.IsAuthoritative) } - if !state.Access.IsNull() { - access, diags := convertAccessBlockToTerraform(ctx, resource, - state.Access.Elements()[0].(types.Object).Attributes()[attr.GroupIDs], - state.Access.Elements()[0].(types.Object).Attributes()[attr.ServiceAccountIDs]) - - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return - } - - state.Access = access + if err := resourceData.Set(attr.Access, resource.AccessToTerraform()); err != nil { + return ErrAttributeSet(err, attr.Access) } -} - -func convertAccessBlockToTerraform(ctx context.Context, resource *model.Resource, stateGroupIDs, stateServiceAccounts tfattr.Value) (types.List, diag.Diagnostics) { - var diagnostics, diags diag.Diagnostics - groupIDs, serviceAccountIDs := types.SetNull(types.StringType), types.SetNull(types.StringType) + protocols, _ := convertProtocols(resourceData) - if len(resource.Groups) > 0 { - groupIDs, diags = makeSet(resource.Groups) - diagnostics.Append(diags...) + if portRangeEqual(protocols.TCP.Ports, resource.Protocols.TCP.Ports) { + resource.Protocols.TCP.Ports = protocols.TCP.Ports } - if len(resource.ServiceAccounts) > 0 { - serviceAccountIDs, diags = makeSet(resource.ServiceAccounts) - diagnostics.Append(diags...) + if portRangeEqual(protocols.UDP.Ports, resource.Protocols.UDP.Ports) { + resource.Protocols.UDP.Ports = protocols.UDP.Ports } - if diagnostics.HasError() { - return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + if err := resourceData.Set(attr.Protocols, resource.Protocols.ToTerraform()); err != nil { + return ErrAttributeSet(err, attr.Protocols) } - attributes := map[string]tfattr.Value{ - attr.GroupIDs: stateGroupIDs, - attr.ServiceAccountIDs: stateServiceAccounts, + if resource.IsVisible != nil { + if err := resourceData.Set(attr.IsVisible, *resource.IsVisible); err != nil { + return ErrAttributeSet(err, attr.IsVisible) + } } - if !groupIDs.IsNull() { - attributes[attr.GroupIDs] = groupIDs + if resource.IsBrowserShortcutEnabled != nil { + if err := resourceData.Set(attr.IsBrowserShortcutEnabled, *resource.IsBrowserShortcutEnabled); err != nil { + return ErrAttributeSet(err, attr.IsBrowserShortcutEnabled) + } } - if !serviceAccountIDs.IsNull() { - attributes[attr.ServiceAccountIDs] = serviceAccountIDs + var alias interface{} + if resource.Alias != nil { + alias = *resource.Alias } - obj, diags := types.ObjectValue(accessAttributeTypes(), attributes) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + if err := resourceData.Set(attr.Alias, alias); err != nil { + return ErrAttributeSet(err, attr.Alias) } - return makeObjectsList(ctx, obj) + return nil } -func convertAccessBlockToTerraformOnImport(ctx context.Context, resource *model.Resource) (types.List, diag.Diagnostics) { - var diagnostics, diags diag.Diagnostics - - groupIDs, serviceAccountIDs := types.SetNull(types.StringType), types.SetNull(types.StringType) +func aliasDiff(key, _, _ string, resourceData *schema.ResourceData) bool { + oldVal, newVal := castToStrings(resourceData.GetChange(key)) - if len(resource.Groups) > 0 { - groupIDs, diags = makeSet(resource.Groups) - diagnostics.Append(diags...) - } - - if len(resource.ServiceAccounts) > 0 { - serviceAccountIDs, diags = makeSet(resource.ServiceAccounts) - diagnostics.Append(diags...) - } + return oldVal == newVal +} - if diagnostics.HasError() { - return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics - } +func equalPorts(a, b interface{}) bool { + oldPorts, newPorts := a.([]interface{}), b.([]interface{}) - attributes := map[string]tfattr.Value{ - attr.GroupIDs: groupIDs, - attr.ServiceAccountIDs: serviceAccountIDs, + oldPortsRange, err := convertPorts(oldPorts) + if err != nil { + return false } - obj, diags := types.ObjectValue(accessAttributeTypes(), attributes) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return makeObjectsListNull(ctx, accessAttributeTypes()), diagnostics + newPortsRange, err := convertPorts(newPorts) + if err != nil { + return false } - return makeObjectsList(ctx, obj) + return portRangeEqual(oldPortsRange, newPortsRange) } -func accessAttributeTypes() map[string]tfattr.Type { - return map[string]tfattr.Type{ - attr.GroupIDs: types.SetType{ - ElemType: types.StringType, - }, - attr.ServiceAccountIDs: types.SetType{ - ElemType: types.StringType, - }, - } -} +func portRangeEqual(portsA, portsB []*model.PortRange) bool { + mapA := convertPortsRangeToMap(portsA) + mapB := convertPortsRangeToMap(portsB) -func makeObjectsListNull(ctx context.Context, attributeTypes map[string]tfattr.Type) types.List { - return types.ListNull(types.ObjectNull(attributeTypes).Type(ctx)) + return reflect.DeepEqual(mapA, mapB) } -func makeObjectsList(ctx context.Context, objects ...types.Object) (types.List, diag.Diagnostics) { - obj := objects[0] - - items := utils.Map(objects, func(item types.Object) tfattr.Value { - return tfattr.Value(item) - }) - - return types.ListValue(obj.Type(ctx), items) -} +func convertPortsRangeToMap(portsRange []*model.PortRange) map[int]struct{} { + out := make(map[int]struct{}) -func makeSet(list []string) (types.Set, diag.Diagnostics) { - return types.SetValue(types.StringType, stringsToTerraformValue(list)) -} + for _, port := range portsRange { + if port.Start == port.End { + out[port.Start] = struct{}{} -func stringsToTerraformValue(list []string) []tfattr.Value { - if len(list) == 0 { - return nil - } + continue + } - out := make([]tfattr.Value, 0, len(list)) - for _, item := range list { - out = append(out, types.StringValue(item)) + for i := port.Start; i <= port.End; i++ { + out[i] = struct{}{} + } } return out } -func convertProtocolsToTerraform(ctx context.Context, protocols *model.Protocols, reference types.List) (types.List, diag.Diagnostics) { - if protocols == nil { - return defaultProtocolsModelToTerraform(ctx) - } - - var diagnostics diag.Diagnostics - - tcp, diags := convertProtocolModelToTerraform(protocols.TCP, reference.Elements()[0].(types.Object).Attributes()[attr.TCP]) - diagnostics.Append(diags...) - - udp, diags := convertProtocolModelToTerraform(protocols.UDP, reference.Elements()[0].(types.Object).Attributes()[attr.UDP]) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return types.ListNull(types.ObjectNull(protocolsAttributeTypes()).Type(ctx)), diagnostics - } - - attributes := map[string]tfattr.Value{ - attr.AllowIcmp: types.BoolValue(protocols.AllowIcmp), - attr.TCP: types.ListValueMust(tcp.Type(ctx), []tfattr.Value{tcp}), - attr.UDP: types.ListValueMust(udp.Type(ctx), []tfattr.Value{udp}), - } - - obj := types.ObjectValueMust(protocolsAttributeTypes(), attributes) - list := []tfattr.Value{obj} - - return types.ListValue(obj.Type(ctx), list) -} - -func convertProtocolsToTerraformOnImport(ctx context.Context, protocols *model.Protocols) (types.List, diag.Diagnostics) { - if protocols == nil { - return defaultProtocolsModelToTerraform(ctx) +func portsNotChanged(attribute, oldValue, newValue string, data *schema.ResourceData) bool { + keys := []string{ + attr.Path(attr.Protocols, attr.TCP, attr.Ports), + attr.Path(attr.Protocols, attr.UDP, attr.Ports), } - var diagnostics diag.Diagnostics - - tcp, diags := convertProtocolModelToTerraformOnImport(protocols.TCP) - diagnostics.Append(diags...) - - udp, diags := convertProtocolModelToTerraformOnImport(protocols.UDP) - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return types.ListNull(types.ObjectNull(protocolsAttributeTypes()).Type(ctx)), diagnostics + if strings.HasSuffix(attribute, "#") && newValue == "0" { + return newValue == oldValue } - attributes := map[string]tfattr.Value{ - attr.AllowIcmp: types.BoolValue(protocols.AllowIcmp), - attr.TCP: types.ListValueMust(tcp.Type(ctx), []tfattr.Value{tcp}), - attr.UDP: types.ListValueMust(udp.Type(ctx), []tfattr.Value{udp}), + for _, key := range keys { + if strings.HasPrefix(attribute, key) { + return equalPorts(data.GetChange(key)) + } } - obj := types.ObjectValueMust(protocolsAttributeTypes(), attributes) - list := []tfattr.Value{obj} - - return types.ListValue(obj.Type(ctx), list) + return false } -func protocolsAttributeTypes() map[string]tfattr.Type { - return map[string]tfattr.Type{ - attr.AllowIcmp: types.BoolType, - attr.TCP: types.ListType{ - ElemType: types.ObjectType{ - AttrTypes: protocolAttributeTypes(), - }, - }, - attr.UDP: types.ListType{ - ElemType: types.ObjectType{ - AttrTypes: protocolAttributeTypes(), - }, - }, +// protocolsNotChanged - suppress protocols change when uses default value. +func protocolsNotChanged(attribute, oldValue, newValue string, data *schema.ResourceData) bool { + switch attribute { + case attr.Len(attr.Protocols): + return newValue == "0" + case attr.Len(attr.Protocols, attr.TCP), attr.Len(attr.Protocols, attr.UDP): + return newValue == "0" + case attr.Path(attr.Protocols, attr.TCP, attr.Policy), attr.Path(attr.Protocols, attr.UDP, attr.Policy): + return oldValue == model.PolicyAllowAll && newValue == "" } -} -func protocolAttributeTypes() map[string]tfattr.Type { - return map[string]tfattr.Type{ - attr.Policy: types.StringType, - attr.Ports: types.SetType{ - ElemType: types.StringType, - }, - } + return false } -func defaultProtocolsModelToTerraform(ctx context.Context) (types.List, diag.Diagnostics) { - attributeTypes := protocolsAttributeTypes() - - var diagnostics diag.Diagnostics - - defaultPorts, diags := defaultProtocolModelToTerraform() - diagnostics.Append(diags...) - - if diagnostics.HasError() { - return makeObjectsListNull(ctx, attributeTypes), diagnostics +func getChangedAccessIDs(ctx context.Context, resourceData *schema.ResourceData, resource *model.Resource, client *client.Client) ([]string, []string, error) { + remote, err := client.ReadResource(ctx, resource.ID) + if err != nil { + return nil, nil, fmt.Errorf("failed to get changedIDs: %w", err) } - tcp, diags := makeObjectsList(ctx, defaultPorts) - diagnostics.Append(diags...) - - udp, diags := makeObjectsList(ctx, defaultPorts) - diagnostics.Append(diags...) - - attributes := map[string]tfattr.Value{ - attr.AllowIcmp: types.BoolValue(true), - attr.TCP: tcp, - attr.UDP: udp, + var oldGroups, oldServiceAccounts []string + if resource.IsAuthoritative { + oldGroups, oldServiceAccounts = remote.Groups, remote.ServiceAccounts + } else { + oldGroups = getOldIDsNonAuthoritative(resourceData, attr.GroupIDs) + oldServiceAccounts = getOldIDsNonAuthoritative(resourceData, attr.ServiceAccountIDs) } - obj, diags := types.ObjectValue(attributeTypes, attributes) - diagnostics.Append(diags...) + // ids to delete + groupsToDelete := setDifference(oldGroups, resource.Groups) + serviceAccountsToDelete := setDifference(oldServiceAccounts, resource.ServiceAccounts) - if diagnostics.HasError() { - return makeObjectsListNull(ctx, attributeTypes), diagnostics - } + // ids to add + groupsToAdd := setDifference(resource.Groups, remote.Groups) + serviceAccountsToAdd := setDifference(resource.ServiceAccounts, remote.ServiceAccounts) - return makeObjectsList(ctx, obj) + return append(groupsToDelete, serviceAccountsToDelete...), append(groupsToAdd, serviceAccountsToAdd...), nil } -func defaultProtocolModelToTerraform() (basetypes.ObjectValue, diag.Diagnostics) { - attributes := map[string]tfattr.Value{ - attr.Policy: types.StringValue(model.PolicyAllowAll), - attr.Ports: types.ListNull(types.StringType), +func getOldIDsNonAuthoritative(resourceData *schema.ResourceData, attribute string) []string { + if resourceData.HasChange(attr.Path(attr.Access, attribute)) { + old, _ := resourceData.GetChange(attr.Path(attr.Access, attribute)) + + return convertIDs(old) } - return types.ObjectValue(protocolAttributeTypes(), attributes) + return nil } -func convertProtocolModelToTerraformOnImport(protocol *model.Protocol) (types.Object, diag.Diagnostics) { - if protocol == nil { - return types.ObjectNull(protocolAttributeTypes()), nil +func convertResource(data *schema.ResourceData) (*model.Resource, error) { + protocols, err := convertProtocols(data) + if err != nil { + return nil, err } - var statePorts = types.Set{} - - ports := convertPortsToTerraform(protocol.Ports) - if equalPorts(ports, statePorts) && !statePorts.IsNull() { - ports = statePorts + groups, serviceAccounts := convertAccess(data) + res := &model.Resource{ + Name: data.Get(attr.Name).(string), + RemoteNetworkID: data.Get(attr.RemoteNetworkID).(string), + Address: data.Get(attr.Address).(string), + Protocols: protocols, + Groups: groups, + ServiceAccounts: serviceAccounts, + IsAuthoritative: convertAuthoritativeFlagLegacy(data), + Alias: getOptionalString(data, attr.Alias), } - policy := protocol.Policy - if policy == model.PolicyRestricted && len(ports.Elements()) == 0 { - policy = model.PolicyDenyAll + isVisible, ok := data.GetOkExists(attr.IsVisible) //nolint + if val := isVisible.(bool); ok { + res.IsVisible = &val } - attributes := map[string]tfattr.Value{ - attr.Policy: types.StringValue(policy), - attr.Ports: ports, + isBrowserShortcutEnabled, ok := data.GetOkExists(attr.IsBrowserShortcutEnabled) //nolint:staticcheck + if val := isBrowserShortcutEnabled.(bool); ok { + res.IsBrowserShortcutEnabled = &val } - return types.ObjectValue(protocolAttributeTypes(), attributes) + return res, nil } -func convertProtocolModelToTerraform(protocol *model.Protocol, reference tfattr.Value) (types.Object, diag.Diagnostics) { - if protocol == nil { - return types.ObjectNull(protocolAttributeTypes()), nil - } - - var statePorts = types.Set{} - - statePortsVal := reference.(types.List).Elements()[0].(types.Object).Attributes()[attr.Ports] - - if statePortsVal != nil && !statePortsVal.IsUnknown() { - statePortsSet, ok := statePortsVal.(types.Set) - if ok { - statePorts = statePortsSet - } - } - - ports := convertPortsToTerraform(protocol.Ports) - if equalPorts(ports, statePorts) && !statePorts.IsNull() { - ports = statePorts - } +func getOptionalString(data *schema.ResourceData, attr string) *string { + var result *string - policy := protocol.Policy - if policy == model.PolicyRestricted && len(ports.Elements()) == 0 { - policy = model.PolicyDenyAll - } + cfg := data.GetRawConfig() + val := cfg.GetAttr(attr) - attributes := map[string]tfattr.Value{ - attr.Policy: types.StringValue(policy), - attr.Ports: ports, + if !val.IsNull() { + str := val.AsString() + result = &str } - return types.ObjectValue(protocolAttributeTypes(), attributes) + return result } -func convertPortsToTerraform(ports []*model.PortRange) types.Set { - elements := make([]tfattr.Value, 0, len(ports)) - for _, port := range ports { - elements = append(elements, types.StringValue(port.String())) +func convertAccess(data *schema.ResourceData) ([]string, []string) { + rawList := data.Get(attr.Access).([]interface{}) + if len(rawList) == 0 || rawList[0] == nil { + return nil, nil } - return types.SetValueMust(types.StringType, elements) -} + rawMap := rawList[0].(map[string]interface{}) -func PortsDiff() planmodifier.Set { - return portsDiff{} + return convertIDs(rawMap[attr.GroupIDs]), convertIDs(rawMap[attr.ServiceAccountIDs]) } -type portsDiff struct{} +func convertAuthoritativeFlagLegacy(data *schema.ResourceData) bool { + flag, hasFlag := data.GetOkExists(attr.IsAuthoritative) //nolint:staticcheck -// Description returns a human-readable description of the plan modifier. -func (m portsDiff) Description(_ context.Context) string { - return "Handles ports difference." -} + if hasFlag { + return flag.(bool) + } -// MarkdownDescription returns a markdown description of the plan modifier. -func (m portsDiff) MarkdownDescription(_ context.Context) string { - return "Handles ports difference." + // default value + return true } -// PlanModifySet implements the plan modification logic. -func (m portsDiff) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { - return +func convertProtocols(data *schema.ResourceData) (*model.Protocols, error) { + rawList := data.Get(attr.Protocols).([]interface{}) + if len(rawList) == 0 { + return model.DefaultProtocols(), nil } - if equalPorts(req.StateValue, req.PlanValue) { - resp.PlanValue = req.StateValue - } -} + rawMap := rawList[0].(map[string]interface{}) -func equalPorts(one, another types.Set) bool { - oldPortsRange, err := convertPorts(one) + udp, err := convertProtocol(rawMap[attr.UDP].([]interface{})) if err != nil { - return false + return nil, err } - newPortsRange, err := convertPorts(another) + tcp, err := convertProtocol(rawMap[attr.TCP].([]interface{})) if err != nil { - return false + return nil, err } - oldPortsMap := convertPortsRangeToMap(oldPortsRange) - newPortsMap := convertPortsRangeToMap(newPortsRange) - - return reflect.DeepEqual(oldPortsMap, newPortsMap) + return &model.Protocols{ + UDP: udp, + TCP: tcp, + AllowIcmp: rawMap[attr.AllowIcmp].(bool), + }, nil } -func convertPortsRangeToMap(portsRange []*model.PortRange) map[int]struct{} { - out := make(map[int]struct{}) +func convertProtocol(rawList []interface{}) (*model.Protocol, error) { + if len(rawList) == 0 { + return nil, nil //nolint:nilnil + } - for _, port := range portsRange { - if port.Start == port.End { - out[port.Start] = struct{}{} + rawMap := rawList[0].(map[string]interface{}) + policy := rawMap[attr.Policy].(string) - continue - } + ports, err := convertPorts(rawMap[attr.Ports].([]interface{})) + if err != nil { + return nil, err + } - for i := port.Start; i <= port.End; i++ { - out[i] = struct{}{} + switch policy { + case model.PolicyAllowAll: + if len(ports) > 0 { + return nil, ErrPortsWithPolicyAllowAll } - } - return out -} + case model.PolicyDenyAll: + if len(ports) > 0 { + return nil, ErrPortsWithPolicyDenyAll + } -// UseStateForUnknownBool returns a plan modifier that copies a known prior state -// value into the planned value. Use this when it is known that an unconfigured -// value will remain the same after a resource update. -// -// To prevent Terraform errors, the framework automatically sets unconfigured -// and Computed attributes to an unknown value "(known after apply)" on update. -// Using this plan modifier will instead display the prior state value in the -// plan, unless a prior plan modifier adjusts the value. -func UseStateForUnknownBool() planmodifier.Bool { - return useStateForUnknownBoolModifier{} -} + case model.PolicyRestricted: + if len(ports) == 0 { + return nil, ErrPolicyRestrictedWithoutPorts + } + } -// useStateForUnknownModifier implements the plan modifier. -type useStateForUnknownBoolModifier struct{} + if policy == model.PolicyDenyAll { + policy = model.PolicyRestricted + } -// Description returns a human-readable description of the plan modifier. -func (m useStateForUnknownBoolModifier) Description(_ context.Context) string { - return "Once set, the value of this attribute in state will not change." + return model.NewProtocol(policy, ports), nil } -// MarkdownDescription returns a markdown description of the plan modifier. -func (m useStateForUnknownBoolModifier) MarkdownDescription(_ context.Context) string { - return "Once set, the value of this attribute in state will not change." -} +func convertPorts(rawList []interface{}) ([]*model.PortRange, error) { + var ports = make([]*model.PortRange, 0, len(rawList)) -// PlanModifyBool implements the plan modification logic. -func (m useStateForUnknownBoolModifier) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { - return - } + for _, port := range rawList { + var str string + if port != nil { + str = port.(string) + } - // Do nothing if there is a known planned value. - if !req.PlanValue.IsUnknown() { - return - } + portRange, err := model.NewPortRange(str) + if err != nil { + return nil, err //nolint:wrapcheck + } - // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. - if req.ConfigValue.IsUnknown() { - return + ports = append(ports, portRange) } - resp.PlanValue = req.StateValue + return ports, nil } diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index b6be08f5..a2144c45 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -16,8 +16,11 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" + twingateV2 "github.com/Twingate/terraform-provider-twingate/twingate/v2" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-mux/tf5to6server" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -54,7 +57,26 @@ var providerClient = func() *client.Client { //nolint }() var ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ //nolint - "twingate": providerserver.NewProtocol6WithError(twingate.New("test")()), + "twingate": func() (tfprotov6.ProviderServer, error) { + upgradedSdkProvider, err := tf5to6server.UpgradeServer(context.Background(), twingate.Provider("test").GRPCProvider) + if err != nil { + log.Fatal(err) + } + + providers := []func() tfprotov6.ProviderServer{ + func() tfprotov6.ProviderServer { + return upgradedSdkProvider + }, + providerserver.NewProtocol6(twingateV2.New("test")()), + } + + provider, err := tf6muxserver.NewMuxServer(context.Background(), providers...) + if err != nil { + return nil, fmt.Errorf("failed to run mux server: %w", err) + } + + return provider, nil + }, } func SetPageLimit(limit int) { diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 1a9d9363..56241a1e 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -605,8 +605,8 @@ func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) { Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"82", "83", "80"`), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), - sdk.TestCheckResourceAttr(theResource, firstTCPPort, "80"), - sdk.TestCheckResourceAttr(theResource, firstUDPPort, "80"), + sdk.TestCheckResourceAttr(theResource, firstTCPPort, "82"), + sdk.TestCheckResourceAttr(theResource, firstUDPPort, "82"), ), }, // no changes @@ -624,7 +624,7 @@ func TestAccTwingateResourcePortReorderingNoChanges(t *testing.T) { }, // new changes applied { - Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"82-83", "70"`), + Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"70", "82-83"`), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "70"), diff --git a/twingate/provider.go b/twingate/provider.go index 79f23386..08e51024 100644 --- a/twingate/provider.go +++ b/twingate/provider.go @@ -9,14 +9,9 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" - twingateDatasource "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/datasource" - twingateResource "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-framework/provider/schema" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) const ( @@ -32,138 +27,92 @@ const ( EnvHTTPMaxRetry = "TWINGATE_HTTP_MAX_RETRY" ) -var _ provider.Provider = &Twingate{} - -type Twingate struct { - version string -} - -type twingateProviderModel struct { - APIToken types.String `tfsdk:"api_token"` - Network types.String `tfsdk:"network"` - URL types.String `tfsdk:"url"` - HTTPTimeout types.Int64 `tfsdk:"http_timeout"` - HTTPMaxRetry types.Int64 `tfsdk:"http_max_retry"` -} - -func New(version string) func() provider.Provider { - return func() provider.Provider { - return &Twingate{ - version: version, - } - } -} - -func (t Twingate) Metadata(ctx context.Context, request provider.MetadataRequest, response *provider.MetadataResponse) { - response.TypeName = "twingate" - response.Version = t.version -} - -func (t Twingate) Schema(ctx context.Context, request provider.SchemaRequest, response *provider.SchemaResponse) { - response.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - attr.APIToken: schema.StringAttribute{ - Optional: true, - Sensitive: true, - Description: fmt.Sprintf("The access key for API operations. You can retrieve this\n"+ - "from the Twingate Admin Console ([documentation](https://docs.twingate.com/docs/api-overview)).\n"+ - "Alternatively, this can be specified using the %s environment variable.", EnvAPIToken), - }, - attr.Network: schema.StringAttribute{ - Optional: true, - Description: fmt.Sprintf("Your Twingate network ID for API operations.\n"+ - "You can find it in the Admin Console URL, for example:\n"+ - "`autoco.twingate.com`, where `autoco` is your network ID\n"+ - "Alternatively, this can be specified using the %s environment variable.", EnvNetwork), - }, - attr.URL: schema.StringAttribute{ - Optional: true, - Description: fmt.Sprintf("The default is '%s'\n"+ - "This is optional and shouldn't be changed under normal circumstances.", DefaultURL), - }, - attr.HTTPTimeout: schema.Int64Attribute{ - Optional: true, - Description: fmt.Sprintf("Specifies a time limit in seconds for the http requests made. The default value is %s seconds.\n"+ - "Alternatively, this can be specified using the %s environment variable", DefaultHTTPTimeout, EnvHTTPTimeout), - }, - attr.HTTPMaxRetry: schema.Int64Attribute{ - Optional: true, - Description: fmt.Sprintf("Specifies a retry limit for the http requests made. The default value is %s.\n"+ - "Alternatively, this can be specified using the %s environment variable", DefaultHTTPMaxRetry, EnvHTTPMaxRetry), - }, +func Provider(version string) *schema.Provider { + provider := &schema.Provider{ + Schema: providerOptions(), + ResourcesMap: map[string]*schema.Resource{ + resource.TwingateResource: resource.Resource(), }, + DataSourcesMap: map[string]*schema.Resource{}, } -} + provider.ConfigureContextFunc = configure(version, provider) -func (t Twingate) Configure(ctx context.Context, request provider.ConfigureRequest, response *provider.ConfigureResponse) { - var config twingateProviderModel - - response.Diagnostics.Append(request.Config.Get(ctx, &config)...) - - if response.Diagnostics.HasError() { - return - } - - // Default values to environment variables, but override - // with Terraform configuration value if set. - - apiToken := os.Getenv(EnvAPIToken) - network := os.Getenv(EnvNetwork) - url := withDefault(os.Getenv(EnvURL), DefaultURL) - httpTimeout := mustGetInt(withDefault(os.Getenv(EnvHTTPTimeout), DefaultHTTPTimeout)) - httpMaxRetry := mustGetInt(withDefault(os.Getenv(EnvHTTPMaxRetry), DefaultHTTPMaxRetry)) - - apiToken = overrideStrWithConfig(config.APIToken, apiToken) - network = overrideStrWithConfig(config.Network, network) - url = overrideStrWithConfig(config.URL, url) - httpTimeout = overrideIntWithConfig(config.HTTPTimeout, httpTimeout) - httpMaxRetry = overrideIntWithConfig(config.HTTPMaxRetry, httpMaxRetry) - - if network == "" { - response.Diagnostics.AddAttributeError( - path.Root(attr.Network), - fmt.Sprintf("Missing Twingate %s", attr.Network), - fmt.Sprintf("The provider cannot create the Twingate API client as there is a missing or empty value for the Twingate %s. "+ - "Set the %s value in the configuration or use the %s environment variable. "+ - "If either is already set, ensure the value is not empty.", attr.Network, attr.Network, EnvNetwork), - ) - - return - } - - client := client.NewClient(url, - apiToken, - network, - time.Duration(httpTimeout)*time.Second, - httpMaxRetry, - t.version) - - response.DataSourceData = client - response.ResourceData = client + return provider } -func mustGetInt(str string) int { - if val, err := strconv.Atoi(str); err == nil { - return val +func providerOptions() map[string]*schema.Schema { + return map[string]*schema.Schema{ + attr.APIToken: { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: fmt.Sprintf("The access key for API operations. You can retrieve this\n"+ + "from the Twingate Admin Console ([documentation](https://docs.twingate.com/docs/api-overview)).\n"+ + "Alternatively, this can be specified using the %s environment variable.", EnvAPIToken), + }, + attr.Network: { + Type: schema.TypeString, + Optional: true, + Sensitive: false, + Description: fmt.Sprintf("Your Twingate network ID for API operations.\n"+ + "You can find it in the Admin Console URL, for example:\n"+ + "`autoco.twingate.com`, where `autoco` is your network ID\n"+ + "Alternatively, this can be specified using the %s environment variable.", EnvNetwork), + }, + attr.URL: { + Type: schema.TypeString, + Optional: true, + Sensitive: false, + Description: fmt.Sprintf("The default is '%s'\n"+ + "This is optional and shouldn't be changed under normal circumstances.", DefaultURL), + }, + attr.HTTPTimeout: { + Type: schema.TypeInt, + Optional: true, + Description: fmt.Sprintf("Specifies a time limit in seconds for the http requests made. The default value is %s seconds.\n"+ + "Alternatively, this can be specified using the %s environment variable", DefaultHTTPTimeout, EnvHTTPTimeout), + }, + attr.HTTPMaxRetry: { + Type: schema.TypeInt, + Optional: true, + Description: fmt.Sprintf("Specifies a retry limit for the http requests made. The default value is %s.\n"+ + "Alternatively, this can be specified using the %s environment variable", DefaultHTTPMaxRetry, EnvHTTPMaxRetry), + }, } - - return 0 } -func overrideStrWithConfig(cfg types.String, defaultValue string) string { - if !cfg.IsNull() { - return cfg.ValueString() - } - - return defaultValue -} +func configure(version string, _ *schema.Provider) func(context.Context, *schema.ResourceData) (interface{}, diag.Diagnostics) { + return func(ctx context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) { + apiToken := os.Getenv(EnvAPIToken) + network := os.Getenv(EnvNetwork) + url := withDefault(os.Getenv(EnvURL), DefaultURL) + httpTimeout := mustGetInt(withDefault(os.Getenv(EnvHTTPTimeout), DefaultHTTPTimeout)) + httpMaxRetry := mustGetInt(withDefault(os.Getenv(EnvHTTPMaxRetry), DefaultHTTPMaxRetry)) + + apiToken = withDefault(data.Get(attr.APIToken).(string), apiToken) + network = withDefault(data.Get(attr.Network).(string), network) + url = withDefault(data.Get(attr.URL).(string), url) + httpTimeout = withDefault(data.Get(attr.HTTPTimeout).(int), httpTimeout) + httpMaxRetry = withDefault(data.Get(attr.HTTPMaxRetry).(int), httpMaxRetry) + + if network != "" { + return client.NewClient(url, + apiToken, + network, + time.Duration(httpTimeout)*time.Second, + httpMaxRetry, + version), + nil + } -func overrideIntWithConfig(cfg types.Int64, defaultValue int) int { - if !cfg.IsNull() { - return int(cfg.ValueInt64()) + return nil, diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Unable to create Twingate client", + Detail: "Unable to create anonymous Twingate client, network has to be provided", + }, + } } - - return defaultValue } func withDefault[T comparable](val, defaultVal T) T { @@ -175,33 +124,10 @@ func withDefault[T comparable](val, defaultVal T) T { return val } -func (t Twingate) DataSources(ctx context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - twingateDatasource.NewConnectorDatasource, - twingateDatasource.NewConnectorsDatasource, - twingateDatasource.NewGroupDatasource, - twingateDatasource.NewGroupsDatasource, - twingateDatasource.NewRemoteNetworkDatasource, - twingateDatasource.NewRemoteNetworksDatasource, - twingateDatasource.NewServiceAccountsDatasource, - twingateDatasource.NewUserDatasource, - twingateDatasource.NewUsersDatasource, - twingateDatasource.NewSecurityPolicyDatasource, - twingateDatasource.NewSecurityPoliciesDatasource, - twingateDatasource.NewResourceDatasource, - twingateDatasource.NewResourcesDatasource, +func mustGetInt(str string) int { + if val, err := strconv.Atoi(str); err == nil { + return val } -} -func (t Twingate) Resources(ctx context.Context) []func() resource.Resource { - return []func() resource.Resource{ - twingateResource.NewConnectorTokensResource, - twingateResource.NewConnectorResource, - twingateResource.NewGroupResource, - twingateResource.NewRemoteNetworkResource, - twingateResource.NewServiceAccountResource, - twingateResource.NewServiceKeyResource, - twingateResource.NewUserResource, - twingateResource.NewResourceResource, - } + return 0 } diff --git a/twingate/v2/provider.go b/twingate/v2/provider.go new file mode 100644 index 00000000..09509f2d --- /dev/null +++ b/twingate/v2/provider.go @@ -0,0 +1,206 @@ +package v2 + +import ( + "context" + "fmt" + "os" + "strconv" + "time" + + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" + twingateDatasource "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/datasource" + twingateResource "github.com/Twingate/terraform-provider-twingate/twingate/internal/provider/resource" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +const ( + DefaultHTTPTimeout = "10" + DefaultHTTPMaxRetry = "10" + DefaultURL = "twingate.com" + + // EnvAPIToken env var for Token. + EnvAPIToken = "TWINGATE_API_TOKEN" // #nosec G101 + EnvNetwork = "TWINGATE_NETWORK" + EnvURL = "TWINGATE_URL" + EnvHTTPTimeout = "TWINGATE_HTTP_TIMEOUT" + EnvHTTPMaxRetry = "TWINGATE_HTTP_MAX_RETRY" +) + +var _ provider.Provider = &Twingate{} + +type Twingate struct { + version string +} + +type twingateProviderModel struct { + APIToken types.String `tfsdk:"api_token"` + Network types.String `tfsdk:"network"` + URL types.String `tfsdk:"url"` + HTTPTimeout types.Int64 `tfsdk:"http_timeout"` + HTTPMaxRetry types.Int64 `tfsdk:"http_max_retry"` +} + +func New(version string) func() provider.Provider { + return func() provider.Provider { + return &Twingate{ + version: version, + } + } +} + +func (t Twingate) Metadata(ctx context.Context, request provider.MetadataRequest, response *provider.MetadataResponse) { + response.TypeName = "twingate" + response.Version = t.version +} + +func (t Twingate) Schema(ctx context.Context, request provider.SchemaRequest, response *provider.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + attr.APIToken: schema.StringAttribute{ + Optional: true, + Sensitive: true, + Description: fmt.Sprintf("The access key for API operations. You can retrieve this\n"+ + "from the Twingate Admin Console ([documentation](https://docs.twingate.com/docs/api-overview)).\n"+ + "Alternatively, this can be specified using the %s environment variable.", EnvAPIToken), + }, + attr.Network: schema.StringAttribute{ + Optional: true, + Description: fmt.Sprintf("Your Twingate network ID for API operations.\n"+ + "You can find it in the Admin Console URL, for example:\n"+ + "`autoco.twingate.com`, where `autoco` is your network ID\n"+ + "Alternatively, this can be specified using the %s environment variable.", EnvNetwork), + }, + attr.URL: schema.StringAttribute{ + Optional: true, + Description: fmt.Sprintf("The default is '%s'\n"+ + "This is optional and shouldn't be changed under normal circumstances.", DefaultURL), + }, + attr.HTTPTimeout: schema.Int64Attribute{ + Optional: true, + Description: fmt.Sprintf("Specifies a time limit in seconds for the http requests made. The default value is %s seconds.\n"+ + "Alternatively, this can be specified using the %s environment variable", DefaultHTTPTimeout, EnvHTTPTimeout), + }, + attr.HTTPMaxRetry: schema.Int64Attribute{ + Optional: true, + Description: fmt.Sprintf("Specifies a retry limit for the http requests made. The default value is %s.\n"+ + "Alternatively, this can be specified using the %s environment variable", DefaultHTTPMaxRetry, EnvHTTPMaxRetry), + }, + }, + } +} + +func (t Twingate) Configure(ctx context.Context, request provider.ConfigureRequest, response *provider.ConfigureResponse) { + var config twingateProviderModel + + response.Diagnostics.Append(request.Config.Get(ctx, &config)...) + + if response.Diagnostics.HasError() { + return + } + + // Default values to environment variables, but override + // with Terraform configuration value if set. + + apiToken := os.Getenv(EnvAPIToken) + network := os.Getenv(EnvNetwork) + url := withDefault(os.Getenv(EnvURL), DefaultURL) + httpTimeout := mustGetInt(withDefault(os.Getenv(EnvHTTPTimeout), DefaultHTTPTimeout)) + httpMaxRetry := mustGetInt(withDefault(os.Getenv(EnvHTTPMaxRetry), DefaultHTTPMaxRetry)) + + apiToken = overrideStrWithConfig(config.APIToken, apiToken) + network = overrideStrWithConfig(config.Network, network) + url = overrideStrWithConfig(config.URL, url) + httpTimeout = overrideIntWithConfig(config.HTTPTimeout, httpTimeout) + httpMaxRetry = overrideIntWithConfig(config.HTTPMaxRetry, httpMaxRetry) + + if network == "" { + response.Diagnostics.AddAttributeError( + path.Root(attr.Network), + fmt.Sprintf("Missing Twingate %s", attr.Network), + fmt.Sprintf("The provider cannot create the Twingate API client as there is a missing or empty value for the Twingate %s. "+ + "Set the %s value in the configuration or use the %s environment variable. "+ + "If either is already set, ensure the value is not empty.", attr.Network, attr.Network, EnvNetwork), + ) + + return + } + + client := client.NewClient(url, + apiToken, + network, + time.Duration(httpTimeout)*time.Second, + httpMaxRetry, + t.version) + + response.DataSourceData = client + response.ResourceData = client +} + +func mustGetInt(str string) int { + if val, err := strconv.Atoi(str); err == nil { + return val + } + + return 0 +} + +func overrideStrWithConfig(cfg types.String, defaultValue string) string { + if !cfg.IsNull() { + return cfg.ValueString() + } + + return defaultValue +} + +func overrideIntWithConfig(cfg types.Int64, defaultValue int) int { + if !cfg.IsNull() { + return int(cfg.ValueInt64()) + } + + return defaultValue +} + +func withDefault[T comparable](val, defaultVal T) T { + var zeroValue T + if val == zeroValue { + return defaultVal + } + + return val +} + +func (t Twingate) DataSources(ctx context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + twingateDatasource.NewConnectorDatasource, + twingateDatasource.NewConnectorsDatasource, + twingateDatasource.NewGroupDatasource, + twingateDatasource.NewGroupsDatasource, + twingateDatasource.NewRemoteNetworkDatasource, + twingateDatasource.NewRemoteNetworksDatasource, + twingateDatasource.NewServiceAccountsDatasource, + twingateDatasource.NewUserDatasource, + twingateDatasource.NewUsersDatasource, + twingateDatasource.NewSecurityPolicyDatasource, + twingateDatasource.NewSecurityPoliciesDatasource, + twingateDatasource.NewResourceDatasource, + twingateDatasource.NewResourcesDatasource, + } +} + +func (t Twingate) Resources(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + twingateResource.NewConnectorTokensResource, + twingateResource.NewConnectorResource, + twingateResource.NewGroupResource, + twingateResource.NewRemoteNetworkResource, + twingateResource.NewServiceAccountResource, + twingateResource.NewServiceKeyResource, + twingateResource.NewUserResource, + } +} From 4111e1d297d0be231806137cf078096a8b1e049d Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 14 Sep 2023 06:33:22 +0200 Subject: [PATCH 07/11] doc update --- docs/resources/resource.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/resources/resource.md b/docs/resources/resource.md index 6d6a32e4..7c5a03e1 100644 --- a/docs/resources/resource.md +++ b/docs/resources/resource.md @@ -64,12 +64,12 @@ resource "twingate_resource" "resource" { ### Optional -- `access` (Block List) Restrict access to certain groups or service accounts (see [below for nested schema](#nestedblock--access)) +- `access` (Block List, Max: 1) Restrict access to certain groups or service accounts (see [below for nested schema](#nestedblock--access)) - `alias` (String) Set a DNS alias address for the Resource. Must be a DNS-valid name string. - `is_authoritative` (Boolean) Determines whether assignments in the access block will override any existing assignments. Default is `true`. If set to `false`, assignments made outside of Terraform will be ignored. - `is_browser_shortcut_enabled` (Boolean) Controls whether an "Open in Browser" shortcut will be shown for this Resource in the Twingate Client. - `is_visible` (Boolean) Controls whether this Resource will be visible in the main Resource list in the Twingate Client. -- `protocols` (Block List) 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. (see [below for nested schema](#nestedblock--protocols)) +- `protocols` (Block List, Max: 1) 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. (see [below for nested schema](#nestedblock--protocols)) ### Read-Only @@ -87,11 +87,14 @@ Optional: ### Nested Schema for `protocols` +Required: + +- `tcp` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--protocols--tcp)) +- `udp` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--protocols--udp)) + Optional: - `allow_icmp` (Boolean) Whether to allow ICMP (ping) traffic -- `tcp` (Block List) (see [below for nested schema](#nestedblock--protocols--tcp)) -- `udp` (Block List) (see [below for nested schema](#nestedblock--protocols--udp)) ### Nested Schema for `protocols.tcp` @@ -102,7 +105,7 @@ Required: Optional: -- `ports` (Set of String) List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port +- `ports` (List of String) List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port @@ -114,7 +117,7 @@ Required: Optional: -- `ports` (Set of String) List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port +- `ports` (List of String) List of port ranges between 1 and 65535 inclusive, in the format `100-200` for a range, or `8080` for a single port ## Import From 832bd18b213fa4ec982f0008c7baab77503f0ab9 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 14 Sep 2023 06:41:41 +0200 Subject: [PATCH 08/11] revert unit tests --- .../provider/resource/converter_test.go | 78 ++++++++----------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/twingate/internal/provider/resource/converter_test.go b/twingate/internal/provider/resource/converter_test.go index 188b0145..1bc533de 100644 --- a/twingate/internal/provider/resource/converter_test.go +++ b/twingate/internal/provider/resource/converter_test.go @@ -1,39 +1,43 @@ package resource import ( - "context" "errors" "fmt" "testing" "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "github.com/Twingate/terraform-provider-twingate/twingate/internal/model" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/utils" - tfattr "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stretchr/testify/assert" ) func TestConvertProtocol(t *testing.T) { cases := []struct { - input types.List + input []interface{} expected *model.Protocol expectedErr error }{ {}, { - input: makeObjectsListMust(types.ObjectValueMust(protocolAttributeTypes(), map[string]tfattr.Value{ - attr.Policy: types.StringValue(model.PolicyAllowAll), - attr.Ports: makeTestSet("-"), - })), + input: []interface{}{ + map[string]interface{}{ + attr.Policy: model.PolicyAllowAll, + attr.Ports: []interface{}{ + "-", + }, + }, + }, expectedErr: errors.New("failed to parse protocols port range"), }, { - input: makeObjectsListMust(types.ObjectValueMust(protocolAttributeTypes(), map[string]tfattr.Value{ - attr.Policy: types.StringValue(model.PolicyRestricted), - attr.Ports: makeTestSet("80-88"), - })), + input: []interface{}{ + map[string]interface{}{ + attr.Policy: model.PolicyRestricted, + attr.Ports: []interface{}{ + "80-88", + }, + }, + }, expected: &model.Protocol{ Policy: model.PolicyRestricted, Ports: []*model.PortRange{ @@ -58,17 +62,6 @@ func TestConvertProtocol(t *testing.T) { } -func makeObjectsListMust(objects ...types.Object) types.List { - obj := objects[0] - - items := utils.Map(objects, func(item types.Object) tfattr.Value { - return tfattr.Value(item) - }) - - return types.ListValueMust(obj.Type(context.Background()), items) - -} - func TestConvertPortsRangeToMap(t *testing.T) { cases := []struct { portsRange []*model.PortRange @@ -132,49 +125,40 @@ func TestConvertPortsRangeToMap(t *testing.T) { } } -func makeTestSet(values ...string) types.Set { - elements := make([]tfattr.Value, 0, len(values)) - for _, val := range values { - elements = append(elements, types.StringValue(val)) - } - - return types.SetValueMust(types.StringType, elements) -} - func TestEqualPorts(t *testing.T) { cases := []struct { - inputA types.Set - inputB types.Set + inputA []interface{} + inputB []interface{} expected bool }{ { - inputA: makeTestSet(""), - inputB: makeTestSet(""), + inputA: []interface{}{""}, + inputB: []interface{}{""}, expected: false, }, { - inputA: makeTestSet("80"), - inputB: makeTestSet(""), + inputA: []interface{}{"80"}, + inputB: []interface{}{""}, expected: false, }, { - inputA: makeTestSet("80"), - inputB: makeTestSet("90"), + inputA: []interface{}{"80"}, + inputB: []interface{}{"90"}, expected: false, }, { - inputA: makeTestSet("80"), - inputB: makeTestSet("80"), + inputA: []interface{}{"80"}, + inputB: []interface{}{"80"}, expected: true, }, { - inputA: makeTestSet("80-81"), - inputB: makeTestSet("80", "81"), + inputA: []interface{}{"80-81"}, + inputB: []interface{}{"80", "81"}, expected: true, }, { - inputA: makeTestSet("80-81", "70"), - inputB: makeTestSet("70", "80", "81"), + inputA: []interface{}{"80-81", "70"}, + inputB: []interface{}{"70", "80", "81"}, expected: true, }, } From 9bb78f0a56688893e66aef2ff00165324c5b3076 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 14 Sep 2023 06:48:03 +0200 Subject: [PATCH 09/11] revert resource acctests --- .../test/acctests/resource/resource_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index 56241a1e..74c2fd4d 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -486,7 +486,7 @@ func TestAccTwingateResourcePortReorderingCreatesNoChanges(t *testing.T) { CheckDestroy: acctests.CheckTwingateResourceDestroy, Steps: []sdk.TestStep{ { - Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"82-83", "80"`), + Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"80", "82-83"`), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "80"), @@ -505,7 +505,7 @@ func TestAccTwingateResourcePortReorderingCreatesNoChanges(t *testing.T) { }, // new changes applied { - Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"82-83", "70"`), + Config: createResourceWithPortRange(remoteNetworkName, resourceName, `"70", "82-83"`), Check: acctests.ComposeTestCheckFunc( acctests.CheckTwingateResourceExists(theResource), sdk.TestCheckResourceAttr(theResource, firstTCPPort, "70"), @@ -1147,7 +1147,7 @@ func TestAccTwingateResourceAccessWithEmptyGroups(t *testing.T) { Steps: []sdk.TestStep{ { Config: createResource18(remoteNetworkName, resourceName), - ExpectError: regexp.MustCompile("Error: Invalid Attribute Value"), + ExpectError: regexp.MustCompile("Error: Not enough list items"), }, }, }) @@ -1194,7 +1194,7 @@ func TestAccTwingateResourceAccessWithEmptyServiceAccounts(t *testing.T) { Steps: []sdk.TestStep{ { Config: createResource19(remoteNetworkName, resourceName), - ExpectError: regexp.MustCompile("Error: Invalid Attribute Value"), + ExpectError: regexp.MustCompile("Error: Not enough list items"), }, }, }) @@ -1241,7 +1241,7 @@ func TestAccTwingateResourceAccessWithEmptyBlock(t *testing.T) { Steps: []sdk.TestStep{ { Config: createResource20(remoteNetworkName, resourceName), - ExpectError: regexp.MustCompile("invalid attribute combination"), + ExpectError: regexp.MustCompile("Missing required argument"), }, }, }) @@ -1839,10 +1839,10 @@ func TestAccTwingateResourceCreateWithAlias(t *testing.T) { ), }, { - // alias attr commented out, means it has nil state + // alias attr commented out, means state keeps the same value without changes Config: createResource29WithoutAlias(terraformResourceName, remoteNetworkName, resourceName), Check: acctests.ComposeTestCheckFunc( - sdk.TestCheckNoResourceAttr(theResource, attr.Alias), + sdk.TestCheckResourceAttr(theResource, attr.Alias, aliasName), ), }, { From ecd4064fdf758b1fde7794764a45e6049cf2e7f8 Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Thu, 14 Sep 2023 06:54:48 +0200 Subject: [PATCH 10/11] revert ci changes --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bf3ef99..ee1bfee8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,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 +169,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 From 58a79d5dec0ce66c4910a65d8e3cbec1e433bb6e Mon Sep 17 00:00:00 2001 From: Volodymyr Manilo Date: Tue, 19 Sep 2023 20:54:33 +0200 Subject: [PATCH 11/11] added security policy to resource --- .../internal/provider/resource/resource.go | 61 +++++++++++-------- .../test/acctests/resource/resource_test.go | 49 +++++++++++++++ 2 files changed, 83 insertions(+), 27 deletions(-) diff --git a/twingate/internal/provider/resource/resource.go b/twingate/internal/provider/resource/resource.go index c7bb64b6..d90b4e7c 100644 --- a/twingate/internal/provider/resource/resource.go +++ b/twingate/internal/provider/resource/resource.go @@ -205,6 +205,7 @@ func resourceCreate(ctx context.Context, resourceData *schema.ResourceData, meta return diag.FromErr(err) } } + log.Printf("[INFO] Created resource %s", resource.Name) return resourceResourceReadHelper(ctx, client, resourceData, resource, nil) @@ -221,27 +222,8 @@ func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta resource.ID = resourceData.Id() if resourceData.HasChange(attr.Access) { - idsToDelete, groupsToAdd, serviceAccountsToAdd, err := getChangedAccessIDs(ctx, resourceData, resource, client) - if err != nil { - return diag.FromErr(err) - } - - if err := client.RemoveResourceAccess(ctx, resource.ID, idsToDelete); err != nil { - return diag.FromErr(err) - } - - if resource.GroupsSecurityPolicyID == nil { - if err = client.AddResourceAccess(ctx, resource.ID, append(serviceAccountsToAdd, groupsToAdd...), nil); err != nil { - return diag.FromErr(err) - } - } else { - if err = client.AddResourceAccess(ctx, resource.ID, serviceAccountsToAdd, nil); err != nil { - return diag.FromErr(err) - } - - if err = client.AddResourceAccess(ctx, resource.ID, groupsToAdd, resource.GroupsSecurityPolicyID); err != nil { - return diag.FromErr(err) - } + if diagErr := updateResourceAccess(ctx, resource, resourceData, meta); diagErr.HasError() { + return diagErr } } @@ -269,6 +251,35 @@ func resourceUpdate(ctx context.Context, resourceData *schema.ResourceData, meta return resourceResourceReadHelper(ctx, client, resourceData, resource, err) } +func updateResourceAccess(ctx context.Context, resource *model.Resource, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*client.Client) + + idsToDelete, groupsToAdd, serviceAccountsToAdd, err := getChangedAccessIDs(ctx, resourceData, resource, client) + if err != nil { + return diag.FromErr(err) + } + + if err := client.RemoveResourceAccess(ctx, resource.ID, idsToDelete); err != nil { + return diag.FromErr(err) + } + + if resource.GroupsSecurityPolicyID == nil { + if err = client.AddResourceAccess(ctx, resource.ID, append(serviceAccountsToAdd, groupsToAdd...), nil); err != nil { + return diag.FromErr(err) + } + } else { + if err = client.AddResourceAccess(ctx, resource.ID, serviceAccountsToAdd, nil); err != nil { + return diag.FromErr(err) + } + + if err = client.AddResourceAccess(ctx, resource.ID, groupsToAdd, resource.GroupsSecurityPolicyID); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + func resourceRead(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*client.Client) @@ -381,12 +392,7 @@ func readDiagnostics(resourceData *schema.ResourceData, resource *model.Resource } } - var alias interface{} - if resource.Alias != nil { - alias = *resource.Alias - } - - if err := resourceData.Set(attr.Alias, alias); err != nil { + if err := resourceData.Set(attr.Alias, resource.Alias); err != nil { return ErrAttributeSet(err, attr.Alias) } @@ -570,6 +576,7 @@ func convertAccess(data *schema.ResourceData) ([]string, []string, *string) { rawMap := rawList[0].(map[string]interface{}) var securityPolicyID *string + rawVal := rawMap[attr.SecurityPolicyID] if rawVal != nil { if val, ok := rawVal.(string); ok { diff --git a/twingate/internal/test/acctests/resource/resource_test.go b/twingate/internal/test/acctests/resource/resource_test.go index aeb8c190..e43ff50a 100644 --- a/twingate/internal/test/acctests/resource/resource_test.go +++ b/twingate/internal/test/acctests/resource/resource_test.go @@ -2460,3 +2460,52 @@ func createResource39(networkName, resourceName string, terraformServiceAccount } `, networkName, terraformServiceAccount, resourceName, model.PolicyRestricted, model.PolicyAllowAll, acctests.TerraformServiceAccount(resourceName)+".id") } + +func TestAccTwingateResourceAccessGroupsWithSecurityPolicy(t *testing.T) { + const terraformResourceName = "test40" + theResource := acctests.TerraformResource(terraformResourceName) + remoteNetworkName := test.RandomName() + resourceName := test.RandomResourceName() + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: createResourceAccessGroupsWithSecurityPolicy(terraformResourceName, remoteNetworkName, resourceName), + Check: acctests.ComposeTestCheckFunc( + acctests.CheckTwingateResourceExists(theResource), + sdk.TestCheckResourceAttrWith(theResource, attr.Path(attr.Access, attr.SecurityPolicyID), nonEmptyValue), + ), + }, + }, + }) +} + +func createResourceAccessGroupsWithSecurityPolicy(terraformResourceName, networkName, resourceName string) string { + return fmt.Sprintf(` + data "twingate_security_policy" "default" { + name = "Default Policy" + } + + resource "twingate_remote_network" "%s" { + name = "%s" + } + + resource "twingate_group" "g40" { + name = "resource-g40" + } + + + resource "twingate_resource" "%s" { + name = "%s" + address = "acc-test.com" + remote_network_id = twingate_remote_network.%s.id + access { + group_ids = [twingate_group.g40.id] + security_policy_id = data.twingate_security_policy.default.id + } + } + `, terraformResourceName, networkName, terraformResourceName, resourceName, terraformResourceName) +}