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/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/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/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/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/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/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/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/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/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" )