diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6931298..404873f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ on: branches: - main + # Ensures only 1 action runs per PR and previous is canceled on new trigger concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/docs/data-sources/connectors.md b/docs/data-sources/connectors.md index 75887715..1b3f55a5 100644 --- a/docs/data-sources/connectors.md +++ b/docs/data-sources/connectors.md @@ -13,7 +13,14 @@ Connectors provide connectivity to Remote Networks. For more information, see Tw ## Example Usage ```terraform -data "twingate_connectors" "all" {} +data "twingate_connectors" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} ``` @@ -21,10 +28,16 @@ data "twingate_connectors" "all" {} ### Optional -- `connectors` (Attributes List) List of Connectors (see [below for nested schema](#nestedatt--connectors)) +- `name` (String) Returns only connectors that exactly match this name. If no options are passed it will return all connectors. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the connector. +- `name_exclude` (String) Match when the value does not exist in the name of the connector. +- `name_prefix` (String) The name of the connector must start with the value. +- `name_regexp` (String) The regular expression match of the name of the connector. +- `name_suffix` (String) The name of the connector must end with the value. ### Read-Only +- `connectors` (Attributes List) List of Connectors (see [below for nested schema](#nestedatt--connectors)) - `id` (String) The ID of this resource. diff --git a/docs/data-sources/remote_networks.md b/docs/data-sources/remote_networks.md index eb79a4a9..85d5c540 100644 --- a/docs/data-sources/remote_networks.md +++ b/docs/data-sources/remote_networks.md @@ -13,12 +13,28 @@ A Remote Network represents a single private network in Twingate that can have o ## Example Usage ```terraform -data "twingate_remote_networks" "all" {} +data "twingate_remote_networks" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} ``` ## Schema +### Optional + +- `name` (String) Returns only remote networks that exactly match this name. If no options are passed it will return all remote networks. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the remote network. +- `name_exclude` (String) Match when the value does not exist in the name of the remote network. +- `name_prefix` (String) The name of the remote network must start with the value. +- `name_regexp` (String) The regular expression match of the name of the remote network. +- `name_suffix` (String) The name of the remote network must end with the value. + ### Read-Only - `id` (String) The ID of this resource. @@ -27,8 +43,11 @@ data "twingate_remote_networks" "all" {} ### Nested Schema for `remote_networks` +Optional: + +- `name` (String) The name of the Remote Network. + Read-Only: - `id` (String) The ID of the Remote Network. - `location` (String) The location of the Remote Network. Must be one of the following: AWS, AZURE, GOOGLE_CLOUD, ON_PREMISE, OTHER. -- `name` (String) The name of the Remote Network diff --git a/docs/data-sources/security_policies.md b/docs/data-sources/security_policies.md index df4c8dd4..bb06628e 100644 --- a/docs/data-sources/security_policies.md +++ b/docs/data-sources/security_policies.md @@ -13,7 +13,14 @@ Security Policies are defined in the Twingate Admin Console and determine user a ## Example Usage ```terraform -data "twingate_security_policies" "all" {} +data "twingate_security_policies" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} ``` @@ -21,11 +28,17 @@ data "twingate_security_policies" "all" {} ### Optional -- `security_policies` (Attributes List) (see [below for nested schema](#nestedatt--security_policies)) +- `name` (String) Returns only security policies that exactly match this name. If no options are passed it will return all security policies. Only one option can be used at a time. +- `name_contains` (String) Match when the value exist in the name of the security policy. +- `name_exclude` (String) Match when the value does not exist in the name of the security policy. +- `name_prefix` (String) The name of the security policy must start with the value. +- `name_regexp` (String) The regular expression match of the name of the security policy. +- `name_suffix` (String) The name of the security policy must end with the value. ### Read-Only - `id` (String) The ID of this resource. +- `security_policies` (Attributes List) (see [below for nested schema](#nestedatt--security_policies)) ### Nested Schema for `security_policies` 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_connectors/data-source.tf b/examples/data-sources/twingate_connectors/data-source.tf index 5b9e6a33..ea592c4d 100644 --- a/examples/data-sources/twingate_connectors/data-source.tf +++ b/examples/data-sources/twingate_connectors/data-source.tf @@ -1 +1,8 @@ -data "twingate_connectors" "all" {} +data "twingate_connectors" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} diff --git a/examples/data-sources/twingate_remote_networks/data-source.tf b/examples/data-sources/twingate_remote_networks/data-source.tf index eb49731b..0bcd82f8 100644 --- a/examples/data-sources/twingate_remote_networks/data-source.tf +++ b/examples/data-sources/twingate_remote_networks/data-source.tf @@ -1 +1,8 @@ -data "twingate_remote_networks" "all" {} \ No newline at end of file +data "twingate_remote_networks" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} \ No newline at end of file diff --git a/examples/data-sources/twingate_security_policies/data-source.tf b/examples/data-sources/twingate_security_policies/data-source.tf index e6f30b17..24e3a604 100644 --- a/examples/data-sources/twingate_security_policies/data-source.tf +++ b/examples/data-sources/twingate_security_policies/data-source.tf @@ -1 +1,8 @@ -data "twingate_security_policies" "all" {} +data "twingate_security_policies" "all" { + name = "" + # name_regexp = "" + # name_contains = "" + # name_exclude = "" + # name_prefix = "" + # name_suffix = "" +} 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/connector.go b/twingate/internal/client/connector.go index 3c37c2e9..efc0d7d6 100644 --- a/twingate/internal/client/connector.go +++ b/twingate/internal/client/connector.go @@ -76,10 +76,11 @@ func (client *Client) ReadConnector(ctx context.Context, connectorID string) (*m return response.ToModel(), nil } -func (client *Client) ReadConnectors(ctx context.Context) ([]*model.Connector, error) { +func (client *Client) ReadConnectors(ctx context.Context, name, filter string) ([]*model.Connector, error) { opr := resourceConnector.read() variables := newVars( + gqlNullable(query.NewConnectorFilterInput(name, filter), "filter"), cursor(query.CursorConnectors), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/client/query/connectors-read.go b/twingate/internal/client/query/connectors-read.go index fdeb5fb1..815e841c 100644 --- a/twingate/internal/client/query/connectors-read.go +++ b/twingate/internal/client/query/connectors-read.go @@ -8,7 +8,7 @@ import ( const CursorConnectors = "connectorsEndCursor" type ReadConnectors struct { - Connectors `graphql:"connectors(after: $connectorsEndCursor, first: $pageLimit)"` + Connectors `graphql:"connectors(filter: $filter, after: $connectorsEndCursor, first: $pageLimit)"` } type Connectors struct { @@ -36,3 +36,13 @@ func (c Connectors) ToModel() []*model.Connector { return edge.Node.ToModel() }) } + +type ConnectorFilterInput struct { + Name *StringFilterOperationInput `json:"name"` +} + +func NewConnectorFilterInput(name, filter string) *ConnectorFilterInput { + return &ConnectorFilterInput{ + Name: NewStringFilterOperationInput(name, filter), + } +} diff --git a/twingate/internal/client/query/remote-networks-read.go b/twingate/internal/client/query/remote-networks-read.go index 7f014c5d..cf6bdc2c 100644 --- a/twingate/internal/client/query/remote-networks-read.go +++ b/twingate/internal/client/query/remote-networks-read.go @@ -8,7 +8,7 @@ import ( const CursorRemoteNetworks = "remoteNetworksEndCursor" type ReadRemoteNetworks struct { - RemoteNetworks `graphql:"remoteNetworks(after: $remoteNetworksEndCursor, first: $pageLimit)"` + RemoteNetworks `graphql:"remoteNetworks(filter: $filter, after: $remoteNetworksEndCursor, first: $pageLimit)"` } func (q ReadRemoteNetworks) IsEmpty() bool { @@ -28,3 +28,13 @@ func (r RemoteNetworks) ToModel() []*model.RemoteNetwork { return edge.Node.ToModel() }) } + +type RemoteNetworkFilterInput struct { + Name *StringFilterOperationInput `json:"name"` +} + +func NewRemoteNetworkFilterInput(name, filter string) *RemoteNetworkFilterInput { + return &RemoteNetworkFilterInput{ + Name: NewStringFilterOperationInput(name, filter), + } +} diff --git a/twingate/internal/client/query/security-policies-read.go b/twingate/internal/client/query/security-policies-read.go index b3319b62..70e14a3c 100644 --- a/twingate/internal/client/query/security-policies-read.go +++ b/twingate/internal/client/query/security-policies-read.go @@ -8,7 +8,7 @@ import ( const CursorPolicies = "policiesEndCursor" type ReadSecurityPolicies struct { - SecurityPolicies `graphql:"securityPolicies(after: $policiesEndCursor, first: $pageLimit)"` + SecurityPolicies `graphql:"securityPolicies(filter: $filter, after: $policiesEndCursor, first: $pageLimit)"` } func (q ReadSecurityPolicies) IsEmpty() bool { @@ -29,3 +29,13 @@ func (q ReadSecurityPolicies) ToModel() []*model.SecurityPolicy { return edge.Node.ToModel() }) } + +type SecurityPolicyFilterField struct { + Name *StringFilterOperationInput `json:"name"` +} + +func NewSecurityPolicyFilterField(name, filter string) *SecurityPolicyFilterField { + return &SecurityPolicyFilterField{ + Name: NewStringFilterOperationInput(name, filter), + } +} 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/remote-network.go b/twingate/internal/client/remote-network.go index 32a59cfe..0b4e51d2 100644 --- a/twingate/internal/client/remote-network.go +++ b/twingate/internal/client/remote-network.go @@ -30,10 +30,11 @@ func (client *Client) CreateRemoteNetwork(ctx context.Context, req *model.Remote return response.ToModel(), nil } -func (client *Client) ReadRemoteNetworks(ctx context.Context) ([]*model.RemoteNetwork, error) { +func (client *Client) ReadRemoteNetworks(ctx context.Context, name, filter string) ([]*model.RemoteNetwork, error) { opr := resourceRemoteNetwork.read() variables := newVars( + gqlNullable(query.NewRemoteNetworkFilterInput(name, filter), "filter"), cursor(query.CursorRemoteNetworks), pageLimit(client.pageLimit), ) diff --git a/twingate/internal/client/security-policy.go b/twingate/internal/client/security-policy.go index e51c1f1f..405e4bd4 100644 --- a/twingate/internal/client/security-policy.go +++ b/twingate/internal/client/security-policy.go @@ -30,10 +30,11 @@ func (client *Client) ReadSecurityPolicy(ctx context.Context, securityPolicyID, return response.ToModel(), nil } -func (client *Client) ReadSecurityPolicies(ctx context.Context) ([]*model.SecurityPolicy, error) { +func (client *Client) ReadSecurityPolicies(ctx context.Context, name, filter string) ([]*model.SecurityPolicy, error) { opr := resourceSecurityPolicy.read() variables := newVars( + gqlNullable(query.NewSecurityPolicyFilterField(name, filter), "filter"), cursor(query.CursorPolicies), pageLimit(client.pageLimit), ) 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/connectors.go b/twingate/internal/provider/datasource/connectors.go index a00b986a..1d3e23a6 100644 --- a/twingate/internal/provider/datasource/connectors.go +++ b/twingate/internal/provider/datasource/connectors.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrConnectorsDatasourceShouldSetOneOptionalNameAttribute = 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 = &connectors{} @@ -24,8 +26,14 @@ type connectors struct { } type connectorsModel struct { - ID types.String `tfsdk:"id"` - Connectors []connectorModel `tfsdk:"connectors"` + 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"` + Connectors []connectorModel `tfsdk:"connectors"` } func (d *connectors) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -59,10 +67,34 @@ func (d *connectors) Schema(ctx context.Context, req datasource.SchemaRequest, r Description: computedDatasourceIDDescription, }, + attr.Name: schema.StringAttribute{ + Optional: true, + Description: "Returns only connectors that exactly match this name. If no options are passed it will return all connectors. 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 connector.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the connector.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the connector.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the connector must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the connector must end with the value.", + }, + // computed attr.Connectors: schema.ListNestedAttribute{ Computed: true, - Optional: true, Description: "List of Connectors", NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ @@ -89,18 +121,63 @@ func (d *connectors) Schema(ctx context.Context, req datasource.SchemaRequest, r } } +//nolint:cyclop func (d *connectors) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - connectors, err := d.client.ReadConnectors(ctx) + var data connectorsModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + 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, ErrConnectorsDatasourceShouldSetOneOptionalNameAttribute, TwingateResources) + + return + } + + connectors, err := d.client.ReadConnectors(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateConnectors) return } - data := connectorsModel{ - ID: types.StringValue("all-connectors"), - Connectors: convertConnectorsToTerraform(connectors), - } + data.ID = types.StringValue("all-connectors") + data.Connectors = convertConnectorsToTerraform(connectors) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/twingate/internal/provider/datasource/remote-networks.go b/twingate/internal/provider/datasource/remote-networks.go index d4c906f9..4efa3343 100644 --- a/twingate/internal/provider/datasource/remote-networks.go +++ b/twingate/internal/provider/datasource/remote-networks.go @@ -14,6 +14,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrRemoteNetworksDatasourceShouldSetOneOptionalNameAttribute = 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 = &remoteNetworks{} @@ -27,6 +29,12 @@ type remoteNetworks struct { type remoteNetworksModel 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"` RemoteNetworks []remoteNetworkModel `tfsdk:"remote_networks"` } @@ -61,6 +69,31 @@ func (d *remoteNetworks) Schema(ctx context.Context, req datasource.SchemaReques Description: computedDatasourceIDDescription, }, + attr.Name: schema.StringAttribute{ + Optional: true, + Description: "Returns only remote networks that exactly match this name. If no options are passed it will return all remote networks. 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 remote network.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the remote network.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the remote network.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the remote network must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the remote network must end with the value.", + }, + attr.RemoteNetworks: schema.ListNestedAttribute{ Computed: true, Description: "List of Remote Networks", @@ -71,8 +104,8 @@ func (d *remoteNetworks) Schema(ctx context.Context, req datasource.SchemaReques Description: "The ID of the Remote Network.", }, attr.Name: schema.StringAttribute{ - Computed: true, - Description: "The name of the Remote Network", + Optional: true, + Description: "The name of the Remote Network.", }, attr.Location: schema.StringAttribute{ Computed: true, @@ -85,6 +118,7 @@ func (d *remoteNetworks) Schema(ctx context.Context, req datasource.SchemaReques } } +//nolint:cyclop func (d *remoteNetworks) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data remoteNetworksModel @@ -95,7 +129,44 @@ func (d *remoteNetworks) Read(ctx context.Context, req datasource.ReadRequest, r return } - networks, err := d.client.ReadRemoteNetworks(ctx) + 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, ErrRemoteNetworksDatasourceShouldSetOneOptionalNameAttribute, TwingateRemoteNetworks) + + return + } + + networks, err := d.client.ReadRemoteNetworks(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateRemoteNetworks) diff --git a/twingate/internal/provider/datasource/security-policies.go b/twingate/internal/provider/datasource/security-policies.go index c814f672..61ebff60 100644 --- a/twingate/internal/provider/datasource/security-policies.go +++ b/twingate/internal/provider/datasource/security-policies.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +var ErrSecurityPoliciesDatasourceShouldSetOneOptionalNameAttribute = 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 = &securityPolicies{} @@ -25,6 +27,12 @@ type securityPolicies struct { type securityPoliciesModel 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"` SecurityPolicies []securityPolicyModel `tfsdk:"security_policies"` } @@ -58,9 +66,32 @@ func (d *securityPolicies) Schema(ctx context.Context, req datasource.SchemaRequ Computed: true, Description: computedDatasourceIDDescription, }, + attr.Name: schema.StringAttribute{ + Optional: true, + Description: "Returns only security policies that exactly match this name. If no options are passed it will return all security policies. 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 security policy.", + }, + attr.Name + attr.FilterByContains: schema.StringAttribute{ + Optional: true, + Description: "Match when the value exist in the name of the security policy.", + }, + attr.Name + attr.FilterByExclude: schema.StringAttribute{ + Optional: true, + Description: "Match when the value does not exist in the name of the security policy.", + }, + attr.Name + attr.FilterByPrefix: schema.StringAttribute{ + Optional: true, + Description: "The name of the security policy must start with the value.", + }, + attr.Name + attr.FilterBySuffix: schema.StringAttribute{ + Optional: true, + Description: "The name of the security policy must end with the value.", + }, attr.SecurityPolicies: schema.ListNestedAttribute{ Computed: true, - Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ attr.ID: schema.StringAttribute{ @@ -78,18 +109,63 @@ func (d *securityPolicies) Schema(ctx context.Context, req datasource.SchemaRequ } } +//nolint:cyclop func (d *securityPolicies) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - policies, err := d.client.ReadSecurityPolicies(ctx) + var data securityPoliciesModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + 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, ErrSecurityPoliciesDatasourceShouldSetOneOptionalNameAttribute, TwingateSecurityPolicies) + + return + } + + policies, err := d.client.ReadSecurityPolicies(ctx, name, filter) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { addErr(&resp.Diagnostics, err, TwingateSecurityPolicy) return } - data := securityPoliciesModel{ - ID: types.StringValue("security-policies-all"), - SecurityPolicies: convertSecurityPoliciesToTerraform(policies), - } + data.ID = types.StringValue("security-policies-all") + data.SecurityPolicies = convertSecurityPoliciesToTerraform(policies) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 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/connectors_test.go b/twingate/internal/test/acctests/datasource/connectors_test.go index 81e511fe..fa17ea70 100644 --- a/twingate/internal/test/acctests/datasource/connectors_test.go +++ b/twingate/internal/test/acctests/datasource/connectors_test.go @@ -13,29 +13,32 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" ) +var ( + connectorsLen = attr.Len(attr.Connectors) + connectorNamePath = attr.Path(attr.Connectors, attr.Name) +) + func TestAccDatasourceTwingateConnectors_basic(t *testing.T) { - t.Run("Test Twingate Datasource : Acc Connectors Basic", func(t *testing.T) { - acctests.SetPageLimit(1) - - networkName1 := test.RandomName() - networkName2 := test.RandomName() - connectorName := test.RandomConnectorName() - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctests.ProviderFactories, - PreCheck: func() { acctests.PreCheck(t) }, - CheckDestroy: acctests.CheckTwingateConnectorDestroy, - Steps: []resource.TestStep{ - { - Config: testDatasourceTwingateConnectors(networkName1, connectorName, networkName2, connectorName, connectorName), - Check: acctests.ComposeTestCheckFunc( - testCheckOutputLength("my_connectors", 2), - testCheckOutputAttr("my_connectors", 0, attr.Name, connectorName), - testCheckOutputAttr("my_connectors", 0, attr.StatusUpdatesEnabled, true), - ), - }, + acctests.SetPageLimit(1) + + networkName1 := test.RandomName() + networkName2 := test.RandomName() + connectorName := test.RandomConnectorName() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateConnectorDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectors(networkName1, connectorName, networkName2, connectorName, connectorName), + Check: acctests.ComposeTestCheckFunc( + testCheckOutputLength("my_connectors", 2), + testCheckOutputAttr("my_connectors", 0, attr.Name, connectorName), + testCheckOutputAttr("my_connectors", 0, attr.StatusUpdatesEnabled, true), + ), }, - }) + }, }) } @@ -66,21 +69,19 @@ func testDatasourceTwingateConnectors(networkName1, connectorName1, networkName2 } func TestAccDatasourceTwingateConnectors_emptyResult(t *testing.T) { - t.Run("Test Twingate Datasource : Acc Connectors - empty result", func(t *testing.T) { - prefix := acctest.RandString(10) - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acctests.ProviderFactories, - PreCheck: func() { acctests.PreCheck(t) }, - Steps: []resource.TestStep{ - { - Config: testTwingateConnectorsDoesNotExists(prefix), - Check: resource.ComposeTestCheckFunc( - testCheckOutputLength("my_connectors_dcs2", 0), - ), - }, + prefix := acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testTwingateConnectorsDoesNotExists(prefix), + Check: resource.ComposeTestCheckFunc( + testCheckOutputLength("my_connectors_dcs2", 0), + ), }, - }) + }, }) } @@ -193,3 +194,140 @@ func testCheckOutputNestedLen(name string, index int, attr string, length int) r return nil } } + +func TestAccDatasourceTwingateConnectorsFilterByName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + connectorName := test.RandomConnectorName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, "", connectorName), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} + +func testDatasourceTwingateConnectorsFilter(resourceName, networkName, connectorName, filter, name string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s_network" { + name = "%[2]s" + } + resource "twingate_connector" "%[1]s_connector" { + remote_network_id = twingate_remote_network.%[1]s_network.id + name = "%[3]s" + } + + data "twingate_connectors" "%[1]s" { + name%[4]s = "%[5]s" + depends_on = [twingate_connector.%[1]s_connector] + } + `, resourceName, networkName, connectorName, filter, name) +} + +func TestAccDatasourceTwingateConnectorsFilterByPrefix(t *testing.T) { + t.Parallel() + + prefix := test.Prefix() + resourceName := test.RandomResourceName() + connectorName := test.RandomConnectorName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, attr.FilterByPrefix, prefix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateConnectorsFilterBySuffix(t *testing.T) { + t.Parallel() + + connectorName := test.RandomConnectorName() + prefix := test.Prefix() + suffix := connectorName[len(prefix):] + resourceName := test.RandomResourceName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, attr.FilterBySuffix, suffix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateConnectorsFilterByContains(t *testing.T) { + t.Parallel() + + connectorName := test.RandomConnectorName() + contains := connectorName[len(connectorName)/2 : len(connectorName)/2+5] + resourceName := test.RandomResourceName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, attr.FilterByContains, contains), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateConnectorsFilterByRegexp(t *testing.T) { + t.Parallel() + + connectorName := test.RandomConnectorName() + contains := connectorName[len(connectorName)/2 : len(connectorName)/2+5] + resourceName := test.RandomResourceName() + theDatasource := "data.twingate_connectors." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateConnectorsFilter(resourceName, test.RandomName(), connectorName, attr.FilterByRegexp, fmt.Sprintf(".*%s.*", contains)), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, connectorsLen, "1"), + resource.TestCheckResourceAttr(theDatasource, connectorNamePath, connectorName), + ), + }, + }, + }) +} diff --git a/twingate/internal/test/acctests/datasource/remote-networks_test.go b/twingate/internal/test/acctests/datasource/remote-networks_test.go index b1eedfff..5ce7dfb7 100644 --- a/twingate/internal/test/acctests/datasource/remote-networks_test.go +++ b/twingate/internal/test/acctests/datasource/remote-networks_test.go @@ -2,6 +2,7 @@ package datasource import ( "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" "testing" "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" @@ -10,6 +11,11 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +var ( + remoteNetworksLen = attr.Len(attr.RemoteNetworks) + remoteNetworkNamePath = attr.Path(attr.RemoteNetworks, attr.Name) +) + func TestAccDatasourceTwingateRemoteNetworks_read(t *testing.T) { t.Run("Test Twingate Datasource : Acc Remote Networks Read", func(t *testing.T) { acctests.SetPageLimit(1) @@ -49,7 +55,140 @@ func testDatasourceTwingateRemoteNetworks2(networkName1, networkName2, prefix st } output "test_networks" { - value = [for n in [for net in data.twingate_remote_networks.all : net if can(net.*.name)][0] : n if length(regexall("%s.*", n.name)) > 0] + value = [for n in [for net in data.twingate_remote_networks.all : net if can(net.*.name)][6] : n if length(regexall("%s.*", n.name)) > 0] } `, networkName1, networkName2, prefix) } + +func TestAccDatasourceTwingateRemoteNetworksFilterByName(t *testing.T) { + t.Parallel() + + resourceName := test.RandomResourceName() + networkName := test.RandomName() + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, networkName, ""), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} + +func testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, name, filter string) string { + return fmt.Sprintf(` + resource "twingate_remote_network" "%[1]s" { + name = "%[2]s" + } + + data "twingate_remote_networks" "%[1]s" { + name%[3]s = "%[4]s" + + depends_on = [twingate_remote_network.%[1]s] + } + `, resourceName, networkName, filter, name) +} + +func TestAccDatasourceTwingateRemoteNetworksFilterByPrefix(t *testing.T) { + t.Parallel() + + prefix := acctest.RandString(5) + resourceName := test.RandomResourceName() + networkName := prefix + "_" + test.RandomName() + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, prefix, attr.FilterByPrefix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateRemoteNetworksFilterBySuffix(t *testing.T) { + t.Parallel() + + suffix := acctest.RandString(5) + resourceName := test.RandomResourceName() + networkName := test.RandomName() + "_" + suffix + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, suffix, attr.FilterBySuffix), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateRemoteNetworksFilterByContains(t *testing.T) { + t.Parallel() + + randString := acctest.RandString(5) + resourceName := test.RandomResourceName() + networkName := test.RandomName() + "_" + randString + "_" + acctest.RandString(5) + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, randString, attr.FilterByContains), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateRemoteNetworksFilterByRegexp(t *testing.T) { + t.Parallel() + + randString := acctest.RandString(5) + resourceName := test.RandomResourceName() + networkName := test.RandomName() + "_" + randString + "_" + acctest.RandString(5) + theDatasource := "data.twingate_remote_networks." + resourceName + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testDatasourceTwingateRemoteNetworksFilter(resourceName, networkName, ".*_"+randString+"_.*", attr.FilterByRegexp), + Check: acctests.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(theDatasource, remoteNetworksLen, "1"), + resource.TestCheckResourceAttr(theDatasource, remoteNetworkNamePath, networkName), + ), + }, + }, + }) +} diff --git a/twingate/internal/test/acctests/datasource/security-policies_test.go b/twingate/internal/test/acctests/datasource/security-policies_test.go index 8f4e1a0e..60029e8f 100644 --- a/twingate/internal/test/acctests/datasource/security-policies_test.go +++ b/twingate/internal/test/acctests/datasource/security-policies_test.go @@ -9,6 +9,8 @@ import ( sdk "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) +var securityPolicyNamePath = attr.Path(attr.SecurityPolicies, attr.Name) + func TestAccDatasourceTwingateSecurityPoliciesBasic(t *testing.T) { t.Run("Test Twingate Datasource : Acc Security Policies - basic", func(t *testing.T) { acctests.SetPageLimit(1) @@ -38,3 +40,95 @@ func testDatasourceTwingateSecurityPolicies() string { data "twingate_security_policies" "all" {} ` } + +func testDatasourceTwingateSecurityPoliciesFilter(filter, name string) string { + return fmt.Sprintf(` + data "twingate_security_policies" "filtered" { + name%[1]s = "%[2]s" + } + `, filter, name) +} + +func TestAccDatasourceTwingateSecurityPoliciesFilterByPrefix(t *testing.T) { + t.Parallel() + + theDatasource := "data.twingate_security_policies.filtered" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByPrefix, "Def"), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"), + sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateSecurityPoliciesFilterBySuffix(t *testing.T) { + t.Parallel() + + theDatasource := "data.twingate_security_policies.filtered" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterBySuffix, "ault Policy"), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"), + sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateSecurityPoliciesFilterByContains(t *testing.T) { + t.Parallel() + + theDatasource := "data.twingate_security_policies.filtered" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByContains, "ault"), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"), + sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"), + ), + }, + }, + }) +} + +func TestAccDatasourceTwingateSecurityPoliciesFilterByRegexp(t *testing.T) { + t.Parallel() + + theDatasource := "data.twingate_security_policies.filtered" + + sdk.Test(t, sdk.TestCase{ + ProtoV6ProviderFactories: acctests.ProviderFactories, + PreCheck: func() { acctests.PreCheck(t) }, + CheckDestroy: acctests.CheckTwingateResourceDestroy, + Steps: []sdk.TestStep{ + { + Config: testDatasourceTwingateSecurityPoliciesFilter(attr.FilterByRegexp, ".*ault .*"), + Check: acctests.ComposeTestCheckFunc( + sdk.TestCheckResourceAttr(theDatasource, attr.Len(attr.SecurityPolicies), "1"), + sdk.TestCheckResourceAttr(theDatasource, securityPolicyNamePath, "Default Policy"), + ), + }, + }, + }) +} 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), + ), + }, + }, + }) +} diff --git a/twingate/internal/test/acctests/datasource/users_test.go b/twingate/internal/test/acctests/datasource/users_test.go index 5fa53c5a..3d919756 100644 --- a/twingate/internal/test/acctests/datasource/users_test.go +++ b/twingate/internal/test/acctests/datasource/users_test.go @@ -1,6 +1,7 @@ package datasource import ( + "errors" "fmt" "testing" @@ -13,6 +14,12 @@ import ( func TestAccDatasourceTwingateUsers_basic(t *testing.T) { t.Run("Test Twingate Datasource : Acc Users Basic", func(t *testing.T) { acctests.SetPageLimit(1) + + users, err := acctests.GetTestUsers() + if err != nil && !errors.Is(err, acctests.ErrResourceNotFound) { + t.Skip("can't run test:", err) + } + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acctests.ProviderFactories, PreCheck: func() { acctests.PreCheck(t) }, @@ -20,7 +27,7 @@ func TestAccDatasourceTwingateUsers_basic(t *testing.T) { { Config: testDatasourceTwingateUsers(), Check: acctests.ComposeTestCheckFunc( - testCheckResourceAttrNotEqual("data.twingate_users.all", attr.Len(attr.Users), "0"), + resource.TestCheckResourceAttr("data.twingate_users.all", attr.Len(attr.Users), fmt.Sprintf("%d", len(users))), ), }, }, diff --git a/twingate/internal/test/acctests/helper.go b/twingate/internal/test/acctests/helper.go index 7519b40c..2eade002 100644 --- a/twingate/internal/test/acctests/helper.go +++ b/twingate/internal/test/acctests/helper.go @@ -504,7 +504,7 @@ func ListSecurityPolicies() ([]*model.SecurityPolicy, error) { return nil, ErrClientNotInited } - securityPolicies, err := providerClient.ReadSecurityPolicies(context.Background()) + securityPolicies, err := providerClient.ReadSecurityPolicies(context.Background(), "", "") if err != nil { return nil, fmt.Errorf("failed to fetch all security policies: %w", err) } diff --git a/twingate/internal/test/client/connector_test.go b/twingate/internal/test/client/connector_test.go index 6136ed8b..565db6af 100644 --- a/twingate/internal/test/client/connector_test.go +++ b/twingate/internal/test/client/connector_test.go @@ -510,7 +510,7 @@ func TestClientConnectorReadEmptyError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, emptyResponse)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Empty(t, connectors) assert.EqualError(t, err, "failed to read connector with id All: query result is empty") @@ -603,7 +603,7 @@ func TestClientConnectorReadAllOk(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.NoError(t, err) assert.Equal(t, expected, connectors) }) @@ -769,7 +769,7 @@ func TestClientReadConnectorsWithRemoteNetworkOk(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.NoError(t, err) assert.Equal(t, expected, connectors) @@ -789,7 +789,7 @@ func TestClientReadConnectorsWithRemoteNetworkError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewStringResponder(200, jsonResponse)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Nil(t, connectors) assert.EqualError(t, err, "failed to read connector with id All: query result is empty") @@ -803,7 +803,7 @@ func TestClientReadConnectorsWithRemoteNetworkRequestError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Nil(t, connectors) assert.EqualError(t, err, graphqlErr(client, "failed to read connector with id All", errBadRequest)) @@ -883,7 +883,7 @@ func TestClientReadConnectorsAllPagesOk(t *testing.T) { }), ) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.NoError(t, err) assert.Equal(t, expected, connectors) }) @@ -933,7 +933,7 @@ func TestClientReadConnectorsAllPagesEmptyResultOnFetching(t *testing.T) { ), ) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Nil(t, connectors) assert.EqualError(t, err, `failed to read connector with id All: query result is empty`) }) @@ -972,7 +972,7 @@ func TestClientReadConnectorsAllPagesRequestErrorOnFetching(t *testing.T) { ), ) - connectors, err := client.ReadConnectors(context.Background()) + connectors, err := client.ReadConnectors(context.Background(), "", "") assert.Nil(t, connectors) assert.EqualError(t, err, graphqlErr(client, "failed to read connector with id All", errBadRequest)) }) diff --git a/twingate/internal/test/client/remote-network_test.go b/twingate/internal/test/client/remote-network_test.go index 5f43666f..37df276f 100644 --- a/twingate/internal/test/client/remote-network_test.go +++ b/twingate/internal/test/client/remote-network_test.go @@ -497,7 +497,7 @@ func TestClientNetworkReadAllOk(t *testing.T) { ), ) - networks, err := client.ReadRemoteNetworks(context.Background()) + networks, err := client.ReadRemoteNetworks(context.Background(), "", "") assert.NoError(t, err) assert.EqualValues(t, expected, networks) @@ -511,7 +511,7 @@ func TestClientNetworkReadAllRequestError(t *testing.T) { httpmock.RegisterResponder("POST", client.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - networks, err := client.ReadRemoteNetworks(context.Background()) + networks, err := client.ReadRemoteNetworks(context.Background(), "", "") assert.Nil(t, networks) assert.EqualError(t, err, graphqlErr(client, "failed to read remote network with id All", errBadRequest)) @@ -552,7 +552,7 @@ func TestClientNetworkReadAllEmptyResponse(t *testing.T) { ), ) - networks, err := client.ReadRemoteNetworks(context.Background()) + networks, err := client.ReadRemoteNetworks(context.Background(), "", "") assert.Nil(t, networks) assert.EqualError(t, err, `failed to read remote network: query result is empty`) @@ -582,7 +582,7 @@ func TestClientNetworkReadAllRequestErrorOnPageFetch(t *testing.T) { ), ) - networks, err := client.ReadRemoteNetworks(context.Background()) + networks, err := client.ReadRemoteNetworks(context.Background(), "", "") assert.Nil(t, networks) assert.EqualError(t, err, graphqlErr(client, "failed to read remote network", errBadRequest)) diff --git a/twingate/internal/test/client/security-policy_test.go b/twingate/internal/test/client/security-policy_test.go index 4dbae951..8c647f93 100644 --- a/twingate/internal/test/client/security-policy_test.go +++ b/twingate/internal/test/client/security-policy_test.go @@ -187,7 +187,7 @@ func TestClientSecurityPoliciesReadOk(t *testing.T) { ), ) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") assert.NoError(t, err) assert.Equal(t, expected, securityPolicies) @@ -201,7 +201,7 @@ func TestClientSecurityPoliciesReadRequestError(t *testing.T) { httpmock.RegisterResponder("POST", c.GraphqlServerURL, httpmock.NewErrorResponder(errBadRequest)) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") assert.Nil(t, securityPolicies) assert.EqualError(t, err, graphqlErr(c, "failed to read security policy", errBadRequest)) @@ -240,7 +240,7 @@ func TestClientSecurityPoliciesReadEmptyResponse(t *testing.T) { httpmock.RegisterResponder("POST", c.GraphqlServerURL, httpmock.NewStringResponder(http.StatusOK, resp)) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") httpmock.Reset() @@ -281,7 +281,7 @@ func TestClientSecurityPoliciesReadRequestErrorOnFetching(t *testing.T) { ), ) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") assert.Nil(t, securityPolicies) assert.EqualError(t, err, graphqlErr(c, "failed to read security policy", errBadRequest)) @@ -343,7 +343,7 @@ func TestClientSecurityPoliciesReadEmptyResultOnFetching(t *testing.T) { ), ) - securityPolicies, err := c.ReadSecurityPolicies(context.Background()) + securityPolicies, err := c.ReadSecurityPolicies(context.Background(), "", "") httpmock.Reset() diff --git a/twingate/internal/test/sweepers/connector_test.go b/twingate/internal/test/sweepers/connector_test.go index 3fd63f94..4c0c1da7 100644 --- a/twingate/internal/test/sweepers/connector_test.go +++ b/twingate/internal/test/sweepers/connector_test.go @@ -3,6 +3,8 @@ package sweepers import ( "context" "errors" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/attr" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -15,7 +17,7 @@ func init() { Name: resourceConnector, F: newTestSweeper(resourceConnector, func(c *client.Client, ctx context.Context) ([]Resource, error) { - resources, err := c.ReadConnectors(ctx) + resources, err := c.ReadConnectors(ctx, test.Prefix(), attr.FilterByPrefix) if err != nil && !errors.Is(err, client.ErrGraphqlResultIsEmpty) { return nil, err } diff --git a/twingate/internal/test/sweepers/remote-network_test.go b/twingate/internal/test/sweepers/remote-network_test.go index be43eda3..5254291e 100644 --- a/twingate/internal/test/sweepers/remote-network_test.go +++ b/twingate/internal/test/sweepers/remote-network_test.go @@ -14,7 +14,7 @@ func init() { Name: resourceRemoteNetwork, F: newTestSweeper(resourceRemoteNetwork, func(client *client.Client, ctx context.Context) ([]Resource, error) { - resources, err := client.ReadRemoteNetworks(ctx) + resources, err := client.ReadRemoteNetworks(ctx, "", "") if err != nil { return nil, err } diff --git a/twingate/internal/test/sweepers/sweeper_test.go b/twingate/internal/test/sweepers/sweeper_test.go index 58357976..711e4768 100644 --- a/twingate/internal/test/sweepers/sweeper_test.go +++ b/twingate/internal/test/sweepers/sweeper_test.go @@ -3,6 +3,7 @@ package sweepers import ( "context" "fmt" + "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "log" "os" "strings" @@ -11,7 +12,6 @@ import ( "github.com/Twingate/terraform-provider-twingate/twingate" "github.com/Twingate/terraform-provider-twingate/twingate/internal/client" - "github.com/Twingate/terraform-provider-twingate/twingate/internal/test" "github.com/hashicorp/terraform-plugin-testing/helper/resource" )