Skip to content

Commit

Permalink
Resource: service (#219)
Browse files Browse the repository at this point in the history
* WIP

* updated project layout

* fix linter

* ignore sec warn

* fix: re-generated docs

* fix users test

* added connector model

* fix convertion graphql.ID to string

* added connector tokens model

* added group model

* added remote-network model

* fix fmt

* added user model

* fix fmt

* WIP

* added connectors pages

* added featching all pages for groups

* added featching all pages for resources

* added featching all pages for users

* updated test results location

* wip: fixing tests

* added generic paginated resource

* added tests

* renamed transport pkg to client

* fixed tests

* fix path to generated test coverage report

* fix read resources

* remove parallel resource tests

* added debug log

* debug error

* debug error

* fix test

* remove logs

* revert changes in ci.yml

* simplify converters

* added tests

* added tests for models

* fix fmt

* added test coverage

* added test coverage

* added test coverage

* fix test

* added test coverage

* run acc test

* revert changes

* added service-account resource

* refactor acc tests

* Fix http_max_retry doc

* regenerated docs

* renamed resource: service-account -> service

* fix acctest

* renamed resource to twingate_service_account

* added test coverage

* added test coverage

* updated doc description

Co-authored-by: Eran Kampf <205185+ekampf@users.noreply.github.com>
  • Loading branch information
vmanilo and ekampf committed Dec 8, 2022
1 parent ce67cde commit a9d4ab9
Show file tree
Hide file tree
Showing 15 changed files with 1,296 additions and 15 deletions.
37 changes: 37 additions & 0 deletions docs/resources/service_account.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "twingate_service_account Resource - terraform-provider-twingate"
subcategory: ""
description: |-
Service Accounts offer a way to provide programmatic, centrally-controlled, and consistent access controls.
---

# twingate_service_account (Resource)

Service Accounts offer a way to provide programmatic, centrally-controlled, and consistent access controls.

## Example Usage

```terraform
provider "twingate" {
api_token = "1234567890abcdef"
network = "mynetwork"
}
resource "twingate_service_account" "github_actions_prod" {
name = "Github Actions PROD"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) The name of the Service Account in Twingate

### Read-Only

- `id` (String) Autogenerated ID of the Service Account


8 changes: 8 additions & 0 deletions examples/resources/twingate_service_account/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
provider "twingate" {
api_token = "1234567890abcdef"
network = "mynetwork"
}

resource "twingate_service_account" "github_actions_prod" {
name = "Github Actions PROD"
}
28 changes: 28 additions & 0 deletions twingate/internal/client/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,31 @@ func (r ResourceNode) ToModel() *model.Resource {
func (q readResourcesByNameQuery) ToModel() []*model.Resource {
return q.Resources.ToModel()
}

func (q createServiceAccountQuery) ToModel() *model.ServiceAccount {
return &model.ServiceAccount{
ID: q.ServiceAccountCreate.Entity.StringID(),
Name: q.ServiceAccountCreate.Entity.StringName(),
}
}

func (q readServiceAccountQuery) ToModel() *model.ServiceAccount {
if q.ServiceAccount == nil {
return nil
}

return q.ServiceAccount.ToModel()
}

func (q gqlServiceAccount) ToModel() *model.ServiceAccount {
return &model.ServiceAccount{
ID: q.StringID(),
Name: q.StringName(),
}
}

func (s *ServiceAccounts) ToModel() []*model.ServiceAccount {
return utils.Map[*ServiceAccountEdge, *model.ServiceAccount](s.Edges, func(edge *ServiceAccountEdge) *model.ServiceAccount {
return edge.Node.ToModel()
})
}
84 changes: 84 additions & 0 deletions twingate/internal/client/pagination_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package client

import (
"context"
"errors"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/twingate/go-graphql-client"
)

func TestPagination(t *testing.T) {
badError := errors.New("bad error")

cases := []struct {
resource *PaginatedResource[int]
nextPage nextPageFunc[int]

expected *PaginatedResource[int]
expectedErr error
}{
{},
{
resource: &PaginatedResource[int]{
PageInfo: PageInfo{
HasNextPage: false,
},
},
expected: &PaginatedResource[int]{
PageInfo: PageInfo{
HasNextPage: false,
},
},
},
{
resource: &PaginatedResource[int]{
PageInfo: PageInfo{
HasNextPage: true,
},
},
nextPage: func(ctx context.Context, variables map[string]interface{}, cursor graphql.String) (*PaginatedResource[int], error) {
return nil, badError
},
expected: &PaginatedResource[int]{
PageInfo: PageInfo{
HasNextPage: true,
},
},
expectedErr: badError,
},
{
resource: &PaginatedResource[int]{
PageInfo: PageInfo{
HasNextPage: true,
},
Edges: []int{1, 2},
},
nextPage: func(ctx context.Context, variables map[string]interface{}, cursor graphql.String) (*PaginatedResource[int], error) {
return &PaginatedResource[int]{
PageInfo: PageInfo{
HasNextPage: false,
},
Edges: []int{3, 4},
}, nil
},
expected: &PaginatedResource[int]{
PageInfo: PageInfo{
HasNextPage: true,
},
Edges: []int{1, 2, 3, 4},
},
},
}

for n, c := range cases {
t.Run(fmt.Sprintf("case_%d", n), func(t *testing.T) {
err := c.resource.fetchPages(context.TODO(), c.nextPage, map[string]interface{}{})

assert.Equal(t, c.expected, c.resource)
assert.Equal(t, c.expectedErr, err)
})
}
}
186 changes: 186 additions & 0 deletions twingate/internal/client/service-account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package client

import (
"context"

"github.com/Twingate/terraform-provider-twingate/twingate/internal/model"
"github.com/twingate/go-graphql-client"
)

const serviceAccountResourceName = "service account"

type gqlServiceAccount struct {
IDName
}

type createServiceAccountQuery struct {
ServiceAccountCreate struct {
Entity IDName
OkError
} `graphql:"serviceAccountCreate(name: $name)"`
}

func (client *Client) CreateServiceAccount(ctx context.Context, serviceAccountName string) (*model.ServiceAccount, error) {
if serviceAccountName == "" {
return nil, NewAPIError(ErrGraphqlNameIsEmpty, "create", serviceAccountResourceName)
}

variables := newVars(gqlField(serviceAccountName, "name"))
response := createServiceAccountQuery{}

err := client.GraphqlClient.NamedMutate(ctx, "createServiceAccount", &response, variables)
if err != nil {
return nil, NewAPIError(err, "create", serviceAccountResourceName)
}

if !response.ServiceAccountCreate.Ok {
message := response.ServiceAccountCreate.Error

return nil, NewAPIError(NewMutationError(message), "create", serviceAccountResourceName)
}

return response.ToModel(), nil
}

type readServiceAccountQuery struct {
ServiceAccount *gqlServiceAccount `graphql:"serviceAccount(id: $id)"`
}

func (client *Client) ReadServiceAccount(ctx context.Context, serviceAccountID string) (*model.ServiceAccount, error) {
if serviceAccountID == "" {
return nil, NewAPIError(ErrGraphqlIDIsEmpty, "read", serviceAccountResourceName)
}

variables := newVars(gqlID(serviceAccountID))
response := readServiceAccountQuery{}

err := client.GraphqlClient.NamedQuery(ctx, "readServiceAccount", &response, variables)
if err != nil {
return nil, NewAPIErrorWithID(err, "read", serviceAccountResourceName, serviceAccountID)
}

if response.ServiceAccount == nil {
return nil, NewAPIErrorWithID(ErrGraphqlResultIsEmpty, "read", serviceAccountResourceName, serviceAccountID)
}

return response.ToModel(), nil
}

type updateServiceAccountQuery struct {
ServiceAccountUpdate struct {
Entity *gqlServiceAccount
OkError
} `graphql:"serviceAccountUpdate(id: $id, name: $name)"`
}

func (client *Client) UpdateServiceAccount(ctx context.Context, serviceAccount *model.ServiceAccount) (*model.ServiceAccount, error) {
if serviceAccount == nil || serviceAccount.ID == "" {
return nil, NewAPIError(ErrGraphqlIDIsEmpty, "update", serviceAccountResourceName)
}

if serviceAccount.Name == "" {
return nil, NewAPIError(ErrGraphqlNameIsEmpty, "update", serviceAccountResourceName)
}

variables := newVars(
gqlID(serviceAccount.ID),
gqlField(serviceAccount.Name, "name"),
)

response := updateServiceAccountQuery{}

err := client.GraphqlClient.NamedMutate(ctx, "updateServiceAccount", &response, variables)
if err != nil {
return nil, NewAPIErrorWithID(err, "update", serviceAccountResourceName, serviceAccount.ID)
}

if !response.ServiceAccountUpdate.Ok {
return nil, NewAPIErrorWithID(NewMutationError(response.ServiceAccountUpdate.Error), "update", serviceAccountResourceName, serviceAccount.ID)
}

if response.ServiceAccountUpdate.Entity == nil {
return nil, NewAPIErrorWithID(ErrGraphqlResultIsEmpty, "update", serviceAccountResourceName, serviceAccount.ID)
}

return response.ServiceAccountUpdate.Entity.ToModel(), nil
}

type deleteServiceAccountQuery struct {
ServiceAccountDelete *OkError `graphql:"serviceAccountDelete(id: $id)"`
}

func (client *Client) DeleteServiceAccount(ctx context.Context, serviceAccountID string) error {
if serviceAccountID == "" {
return NewAPIError(ErrGraphqlIDIsEmpty, "delete", serviceAccountResourceName)
}

variables := newVars(gqlID(serviceAccountID))
response := deleteServiceAccountQuery{}

err := client.GraphqlClient.NamedMutate(ctx, "deleteServiceAccount", &response, variables)
if err != nil {
return NewAPIErrorWithID(err, "delete", serviceAccountResourceName, serviceAccountID)
}

if !response.ServiceAccountDelete.Ok {
return NewAPIErrorWithID(NewMutationError(response.ServiceAccountDelete.Error), "delete", serviceAccountResourceName, serviceAccountID)
}

return nil
}

type ServiceAccountEdge struct {
Node *gqlServiceAccount
}

type ServiceAccounts struct {
PaginatedResource[*ServiceAccountEdge]
}

type readServiceAccountsQuery struct {
ServiceAccounts ServiceAccounts
}

func (client *Client) ReadServiceAccounts(ctx context.Context) ([]*model.ServiceAccount, error) {
response := readServiceAccountsQuery{}

err := client.GraphqlClient.NamedQuery(ctx, "readServiceAccounts", &response, nil)
if err != nil {
return nil, NewAPIErrorWithID(err, "read", serviceAccountResourceName, "All")
}

if len(response.ServiceAccounts.Edges) == 0 {
return nil, NewAPIErrorWithID(ErrGraphqlResultIsEmpty, "read", serviceAccountResourceName, "All")
}

err = response.ServiceAccounts.fetchPages(ctx, client.readServiceAccountsAfter, nil)
if err != nil {
return nil, err
}

return response.ServiceAccounts.ToModel(), nil
}

type readServiceAccountsAfter struct {
ServiceAccounts ServiceAccounts `graphql:"serviceAccounts(after: $serviceAccountsEndCursor)"`
}

func (client *Client) readServiceAccountsAfter(ctx context.Context, variables map[string]interface{}, cursor graphql.String) (*PaginatedResource[*ServiceAccountEdge], error) {
if variables == nil {
variables = make(map[string]interface{})
}

variables["serviceAccountsEndCursor"] = cursor
response := readServiceAccountsAfter{}

err := client.GraphqlClient.NamedQuery(ctx, "readServiceAccounts", &response, variables)
if err != nil {
return nil, NewAPIErrorWithID(err, "read", serviceAccountResourceName, "All")
}

if len(response.ServiceAccounts.Edges) == 0 {
return nil, NewAPIErrorWithID(ErrGraphqlResultIsEmpty, "read", serviceAccountResourceName, "All")
}

return &response.ServiceAccounts.PaginatedResource, nil
}
14 changes: 14 additions & 0 deletions twingate/internal/model/service-account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package model

type ServiceAccount struct {
ID string
Name string
}

func (s ServiceAccount) GetName() string {
return s.Name
}

func (s ServiceAccount) GetID() string {
return s.ID
}
1 change: 1 addition & 0 deletions twingate/internal/provider/resource/all-resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ const (
TwingateConnectorTokens = "twingate_connector_tokens"
TwingateGroup = "twingate_group"
TwingateResource = "twingate_resource"
TwingateServiceAccount = "twingate_service_account"
)

0 comments on commit a9d4ab9

Please sign in to comment.