diff --git a/docs/data-sources/service_accounts.md b/docs/data-sources/service_accounts.md index dcbb3996..ffcc1394 100644 --- a/docs/data-sources/service_accounts.md +++ b/docs/data-sources/service_accounts.md @@ -15,6 +15,11 @@ Service Accounts offer a way to provide programmatic, centrally-controlled, and ```terraform data "twingate_service_accounts" "foo" { name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" } ``` @@ -23,7 +28,12 @@ data "twingate_service_accounts" "foo" { ### Optional -- `name` (String) Filter results by the name of the Service Account. +- `name` (String) Returns only service accounts that exactly match this name. If no options are passed it will return all service accounts. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the service account. +- `name_exclude` (String) Match when the value does not exist in the name of the service account. +- `name_prefix` (String) The name of the service account must start with the value. +- `name_regexp` (String) The regular expression match of the name of the service account. +- `name_suffix` (String) The name of the service account must end with the value. ### Read-Only diff --git a/examples/data-sources/twingate_service_accounts/data-source.tf b/examples/data-sources/twingate_service_accounts/data-source.tf index 17924121..93be3d44 100644 --- a/examples/data-sources/twingate_service_accounts/data-source.tf +++ b/examples/data-sources/twingate_service_accounts/data-source.tf @@ -1,3 +1,8 @@ data "twingate_service_accounts" "foo" { name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" } diff --git a/twingate/internal/client/query/service-accounts-read.go b/twingate/internal/client/query/service-accounts-read.go index 9e861ab5..1f0f0cc3 100644 --- a/twingate/internal/client/query/service-accounts-read.go +++ b/twingate/internal/client/query/service-accounts-read.go @@ -99,21 +99,15 @@ func IsGqlKeyActive(item *GqlKeyIDEdge) bool { } type ServiceAccountFilterInput struct { - Name StringFilter `json:"name"` + Name *StringFilterOperationInput `json:"name"` } -type StringFilter struct { - Eq string `json:"eq"` -} - -func NewServiceAccountFilterInput(name string) *ServiceAccountFilterInput { +func NewServiceAccountFilterInput(name, filter string) *ServiceAccountFilterInput { if name == "" { return nil } return &ServiceAccountFilterInput{ - Name: StringFilter{ - Eq: name, - }, + Name: NewStringFilterOperationInput(name, filter), } } diff --git a/twingate/internal/client/service-account.go b/twingate/internal/client/service-account.go index f8ef61e7..7f85163d 100644 --- a/twingate/internal/client/service-account.go +++ b/twingate/internal/client/service-account.go @@ -119,13 +119,17 @@ func (client *Client) readServiceAccountsAfter(ctx context.Context, variables ma func (client *Client) ReadServiceAccounts(ctx context.Context, input ...string) ([]*model.ServiceAccount, error) { opr := resourceServiceAccount.read() - var name string + var name, filter string if len(input) > 0 { name = input[0] } + if len(input) > 1 { + filter = input[1] + } + variables := newVars( - gqlNullable(query.NewServiceAccountFilterInput(name), "filter"), + gqlNullable(query.NewServiceAccountFilterInput(name, filter), "filter"), cursor(query.CursorServices), cursor(query.CursorResources), cursor(query.CursorServiceKeys), diff --git a/twingate/internal/provider/datasource/service-accounts.go b/twingate/internal/provider/datasource/service-accounts.go index cc9b2342..0528fdef 100644 --- a/twingate/internal/provider/datasource/service-accounts.go +++ b/twingate/internal/provider/datasource/service-accounts.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrServiceAccountsDatasourceShouldSetOneOptionalNameAttribute = 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 = &serviceAccounts{} @@ -26,6 +28,11 @@ type serviceAccounts struct { type serviceAccountsModel struct { 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"` ServiceAccounts []serviceAccountModel `tfsdk:"service_accounts"` } @@ -68,7 +75,27 @@ func (d *serviceAccounts) Schema(ctx context.Context, req datasource.SchemaReque }, attr.Name: schema.StringAttribute{ Optional: true, - Description: "Filter results by the name of the Service Account.", + Description: "Returns only service accounts that exactly match this name. If no options are passed it will return all service accounts. 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 service account.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the service account.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the service account.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the service account must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the service account must end with the value.", }, attr.ServiceAccounts: schema.ListNestedAttribute{ Computed: true, @@ -100,6 +127,7 @@ func (d *serviceAccounts) Schema(ctx context.Context, req datasource.SchemaReque } } +//nolint:cyclop func (d *serviceAccounts) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data serviceAccountsModel @@ -110,7 +138,44 @@ func (d *serviceAccounts) Read(ctx context.Context, req datasource.ReadRequest, return } - accounts, err := d.client.ReadServiceAccounts(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, ErrServiceAccountsDatasourceShouldSetOneOptionalNameAttribute, TwingateResources) + + return + } + + accounts, err := d.client.ReadServiceAccounts(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateServiceAccounts) diff --git a/twingate/internal/test/acctests/datasource/service-accounts_test.go b/twingate/internal/test/acctests/datasource/service-accounts_test.go index 1c3460a4..807fc49e 100644 --- a/twingate/internal/test/acctests/datasource/service-accounts_test.go +++ b/twingate/internal/test/acctests/datasource/service-accounts_test.go @@ -2,6 +2,7 @@ package datasource import ( "fmt" + "regexp" "strings" "testing" @@ -16,6 +17,7 @@ import ( var ( serviceAccountsLen = attr.Len(attr.ServiceAccounts) keyIDsLen = attr.Len(attr.ServiceAccounts, attr.KeyIDs) + serviceAccountName = attr.Path(attr.ServiceAccounts, attr.Name) ) func TestAccDatasourceTwingateServicesFilterByName(t *testing.T) { @@ -247,3 +249,202 @@ func duplicate(val string, n int) []any { return result } + +func TestAccDatasourceTwingateServicesWithMultipleFilters(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceServicesWithMultipleFilters(test.RandomName()), + ExpectError: regexp.MustCompile("Only one of name.*"), + }, + }, + }) +} + +func testDatasourceServicesWithMultipleFilters(name string) string { + return fmt.Sprintf(` + data "twingate_service_accounts" "with-multiple-filters" { + name_regexp = "%[1]s" + name_contains = "%[1]s" + } + `, name) +} + +func TestAccDatasourceTwingateServicesFilterByPrefix(t *testing.T) { + t.Parallel() + + const ( + terraformResourceName = "dts_service" + theDatasource = "data.twingate_service_accounts.out" + ) + + prefix := test.Prefix("orange") + name := acctest.RandomWithPrefix(prefix) + config := []terraformServiceConfig{ + { + serviceName: name, + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + { + serviceName: test.Prefix("lemon"), + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: terraformConfig( + createServices(config), + datasourceServicesWithFilter(config, prefix, attr.FilterByPrefix), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, serviceAccountsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, serviceAccountName, name), + ), + }, + }, + }) +} + +func datasourceServicesWithFilter(configs []terraformServiceConfig, name, filter string) string { + var dependsOn string + ids := getTerraformServiceKeys(configs) + + if ids != "" { + dependsOn = fmt.Sprintf("depends_on = [%s]", ids) + } + + return fmt.Sprintf(` + data "twingate_service_accounts" "out" { + name%s = "%s" + + %s + } + `, filter, name, dependsOn) +} + +func TestAccDatasourceTwingateServicesFilterBySuffix(t *testing.T) { + t.Parallel() + + const ( + terraformResourceName = "dts_service" + theDatasource = "data.twingate_service_accounts.out" + ) + + name := test.Prefix("orange") + config := []terraformServiceConfig{ + { + serviceName: name, + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + { + serviceName: test.Prefix("lemon"), + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: terraformConfig( + createServices(config), + datasourceServicesWithFilter(config, "orange", attr.FilterBySuffix), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, serviceAccountsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, serviceAccountName, name), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateServicesFilterByContains(t *testing.T) { + t.Parallel() + + const ( + terraformResourceName = "dts_service" + theDatasource = "data.twingate_service_accounts.out" + ) + + name := test.Prefix("orange") + config := []terraformServiceConfig{ + { + serviceName: name, + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + { + serviceName: test.Prefix("lemon"), + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: terraformConfig( + createServices(config), + datasourceServicesWithFilter(config, "rang", attr.FilterByContains), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, serviceAccountsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, serviceAccountName, name), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateServicesFilterByRegexp(t *testing.T) { + t.Parallel() + + const ( + terraformResourceName = "dts_service" + theDatasource = "data.twingate_service_accounts.out" + ) + + name := test.Prefix("orange") + config := []terraformServiceConfig{ + { + serviceName: name, + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + { + serviceName: test.Prefix("lemon"), + terraformResourceName: test.TerraformRandName(terraformResourceName), + }, + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: terraformConfig( + createServices(config), + datasourceServicesWithFilter(config, ".*ora.*", attr.FilterByRegexp), + ), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, serviceAccountsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, serviceAccountName, name), + ), + }, + }, + }) +}