Skip to content

Commit

Permalink
Feature: add filtering support for twingate resources datasource (#443)
Browse files Browse the repository at this point in the history
* added new attributes for resources datasource

* added optional name attributes for resources datasource

* added feature branch

* fix test

* remove feature branch

* enable tests

* remove feature branch

* refactore

* updated docs

* update resources datasource: allow to list all resources

* fix docs

---------

Co-authored-by: Bob Lee <[email protected]>
  • Loading branch information
vmanilo and twingate-blee authored Feb 20, 2024
1 parent 0bffd0b commit cad0a46
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 22 deletions.
14 changes: 12 additions & 2 deletions docs/data-sources/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Resources in Twingate represent servers on the private network that clients can
```terraform
data "twingate_resources" "foo" {
name = "<your resource's name>"
# name_regexp = "<regular expression of resource name>"
# name_contains = "<a string in the resource name>"
# name_exclude = "<your resource's name to exclude>"
# name_prefix = "<prefix of resource name>"
# name_suffix = "<suffix of resource name>"
}
# Resource names are not constrained to be unique within Twingate,
Expand All @@ -24,9 +29,14 @@ data "twingate_resources" "foo" {
<!-- schema generated by tfplugindocs -->
## Schema

### Required
### Optional

- `name` (String) The name of the Resource
- `name` (String) Returns only resources that exactly match this name. If no options are passed it will return all resources. Only one option can be used at a time.
- `name_contains` (String) Match when the value exist in the name of the resource.
- `name_exclude` (String) Match when the value does not exist in the name of the resource.
- `name_prefix` (String) The name of the resource must start with the value.
- `name_regexp` (String) The regular expression match of the name of the resource.
- `name_suffix` (String) The name of the resource must end with the value.

### Read-Only

Expand Down
5 changes: 5 additions & 0 deletions examples/data-sources/twingate_resources/data-source.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
data "twingate_resources" "foo" {
name = "<your resource's name>"
# name_regexp = "<regular expression of resource name>"
# name_contains = "<a string in the resource name>"
# name_exclude = "<your resource's name to exclude>"
# name_prefix = "<prefix of resource name>"
# name_suffix = "<suffix of resource name>"
}

# Resource names are not constrained to be unique within Twingate,
Expand Down
6 changes: 6 additions & 0 deletions twingate/internal/attr/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ const (
RemoteNetworkID = "remote_network_id"
Type = "type"
IsActive = "is_active"

FilterByRegexp = "_regexp"
FilterByContains = "_contains"
FilterByExclude = "_exclude"
FilterByPrefix = "_prefix"
FilterBySuffix = "_suffix"
)
35 changes: 33 additions & 2 deletions twingate/internal/client/query/groups-read.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package query

import (
"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"
)
Expand Down Expand Up @@ -36,7 +37,37 @@ type GroupFilterInput struct {
}

type StringFilterOperationInput struct {
Eq string `json:"eq"`
Eq *string `json:"eq"`
Ne *string `json:"ne"`
StartsWith *string `json:"startsWith"`
EndsWith *string `json:"endsWith"`
Regexp *string `json:"regexp"`
Contains *string `json:"contains"`
}

func NewStringFilterOperationInput(name, filter string) *StringFilterOperationInput {
if filter == "" && name == "" {
return nil
}

var stringFilter StringFilterOperationInput

switch filter {
case attr.FilterByRegexp:
stringFilter.Regexp = &name
case attr.FilterByContains:
stringFilter.Contains = &name
case attr.FilterByExclude:
stringFilter.Ne = &name
case attr.FilterByPrefix:
stringFilter.StartsWith = &name
case attr.FilterBySuffix:
stringFilter.EndsWith = &name
default:
stringFilter.Eq = &name
}

return &stringFilter
}

type GroupTypeFilterOperatorInput struct {
Expand Down Expand Up @@ -66,7 +97,7 @@ func NewGroupFilterInput(input *model.GroupsFilter) *GroupFilterInput {

if input.Name != nil {
filter.Name = &StringFilterOperationInput{
Eq: *input.Name,
Eq: input.Name,
}
}

Expand Down
2 changes: 1 addition & 1 deletion twingate/internal/client/query/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,7 @@ func TestBuildGroupsFilter(t *testing.T) {
filter: &model.GroupsFilter{Name: optionalString("Group")},
expected: &GroupFilterInput{
Name: &StringFilterOperationInput{
Eq: "Group",
Eq: optionalString("Group"),
},
Type: defaultType,
IsActive: defaultActive,
Expand Down
12 changes: 11 additions & 1 deletion twingate/internal/client/query/resources-by-name-read.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package query

type ReadResourcesByName struct {
Resources `graphql:"resources(filter: {name: {eq: $name}}, after: $resourcesEndCursor, first: $pageLimit)"`
Resources `graphql:"resources(filter: $filter, after: $resourcesEndCursor, first: $pageLimit)"`
}

func (q ReadResourcesByName) IsEmpty() bool {
return len(q.Edges) == 0
}

type ResourceFilterInput struct {
Name *StringFilterOperationInput `json:"name"`
}

func NewResourceFilterInput(name, filter string) *ResourceFilterInput {
return &ResourceFilterInput{
Name: NewStringFilterOperationInput(name, filter),
}
}
4 changes: 2 additions & 2 deletions twingate/internal/client/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,11 @@ func (client *Client) UpdateResourceActiveState(ctx context.Context, resource *m
return client.mutate(ctx, &response, variables, opr, attr{id: resource.ID})
}

func (client *Client) ReadResourcesByName(ctx context.Context, name string) ([]*model.Resource, error) {
func (client *Client) ReadResourcesByName(ctx context.Context, name, filter string) ([]*model.Resource, error) {
opr := resourceResource.read()

variables := newVars(
gqlVar(name, "name"),
gqlNullable(query.NewResourceFilterInput(name, filter), "filter"),
cursor(query.CursorResources),
pageLimit(client.pageLimit),
)
Expand Down
13 changes: 13 additions & 0 deletions twingate/internal/provider/datasource/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func addErr(diagnostics *diag.Diagnostics, err error, resource string) {
Expand All @@ -16,3 +17,15 @@ func addErr(diagnostics *diag.Diagnostics, err error, resource string) {
err.Error(),
)
}

func countOptionalAttributes(attributes ...types.String) int {
var count int

for _, attr := range attributes {
if attr.ValueString() != "" {
count++
}
}

return count
}
81 changes: 73 additions & 8 deletions twingate/internal/provider/datasource/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
)

var ErrResourcesDatasourceShouldSetOneOptionalNameAttribute = errors.New("Only one of name, name_regex, name_contains, name_exclude, name_prefix or name_suffix must be set.")

// Ensure the implementation satisfies the desired interfaces.
var _ datasource.DataSource = &resources{}

Expand All @@ -25,9 +27,14 @@ type resources struct {
}

type resourcesModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Resources []resourceModel `tfsdk:"resources"`
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
NameRegexp types.String `tfsdk:"name_regexp"`
NameContains types.String `tfsdk:"name_contains"`
NameExclude types.String `tfsdk:"name_exclude"`
NamePrefix types.String `tfsdk:"name_prefix"`
NameSuffix types.String `tfsdk:"name_suffix"`
Resources []resourceModel `tfsdk:"resources"`
}

func (d *resources) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
Expand Down Expand Up @@ -69,6 +76,7 @@ func protocolSchema() schema.SingleNestedAttribute {
}
}

//nolint:funlen
func (d *resources) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.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).",
Expand All @@ -78,10 +86,29 @@ func (d *resources) Schema(ctx context.Context, req datasource.SchemaRequest, re
Description: computedDatasourceIDDescription,
},
attr.Name: schema.StringAttribute{
Required: true,
Description: "The name of the Resource",
Optional: true,
Description: "Returns only resources that exactly match this name. If no options are passed it will return all resources. Only one option can be used at a time.",
},
attr.Name + attr.FilterByRegexp: schema.StringAttribute{
Optional: true,
Description: "The regular expression match of the name of the resource.",
},
attr.Name + attr.FilterByContains: schema.StringAttribute{
Optional: true,
Description: "Match when the value exist in the name of the resource.",
},
attr.Name + attr.FilterByExclude: schema.StringAttribute{
Optional: true,
Description: "Match when the value does not exist in the name of the resource.",
},
attr.Name + attr.FilterByPrefix: schema.StringAttribute{
Optional: true,
Description: "The name of the resource must start with the value.",
},
attr.Name + attr.FilterBySuffix: schema.StringAttribute{
Optional: true,
Description: "The name of the resource must end with the value.",
},

// computed
attr.Resources: schema.ListNestedAttribute{
Computed: true,
Expand Down Expand Up @@ -123,6 +150,7 @@ func (d *resources) Schema(ctx context.Context, req datasource.SchemaRequest, re
}
}

//nolint:cyclop
func (d *resources) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data resourcesModel

Expand All @@ -133,14 +161,51 @@ func (d *resources) Read(ctx context.Context, req datasource.ReadRequest, resp *
return
}

resources, err := d.client.ReadResourcesByName(ctx, data.Name.ValueString())
var name, filter string

if data.Name.ValueString() != "" {
name = data.Name.ValueString()
}

if data.NameRegexp.ValueString() != "" {
name = data.NameRegexp.ValueString()
filter = attr.FilterByRegexp
}

if data.NameContains.ValueString() != "" {
name = data.NameContains.ValueString()
filter = attr.FilterByContains
}

if data.NameExclude.ValueString() != "" {
name = data.NameExclude.ValueString()
filter = attr.FilterByExclude
}

if data.NamePrefix.ValueString() != "" {
name = data.NamePrefix.ValueString()
filter = attr.FilterByPrefix
}

if data.NameSuffix.ValueString() != "" {
name = data.NameSuffix.ValueString()
filter = attr.FilterBySuffix
}

if countOptionalAttributes(data.Name, data.NameRegexp, data.NameContains, data.NameExclude, data.NamePrefix, data.NameSuffix) > 1 {
addErr(&resp.Diagnostics, ErrResourcesDatasourceShouldSetOneOptionalNameAttribute, TwingateResources)

return
}

resources, err := d.client.ReadResourcesByName(ctx, name, filter)
if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) {
addErr(&resp.Diagnostics, err, TwingateResources)

return
}

data.ID = types.StringValue("query resources by name: " + data.Name.ValueString())
data.ID = types.StringValue("query resources by name: " + name)
data.Resources = convertResourcesToTerraform(resources)

// Save data into Terraform state
Expand Down
Loading

0 comments on commit cad0a46

Please sign in to comment.