From b1c0f85e7d9e6f6524a0399dd490af92a9a81ba2 Mon Sep 17 00:00:00 2001 From: ehab-eb Date: Sun, 19 Nov 2023 15:25:47 +0100 Subject: [PATCH] Added `filter` block to `databricks_instance_profiles` data source --- aws/data_instance_profiles.go | 61 +- aws/data_instance_profiles_test.go | 556 +++++++++++++++++- docs/data-sources/instance_profiles.md | 53 +- .../acceptance/data_instance_profiles_test.go | 17 + 4 files changed, 659 insertions(+), 28 deletions(-) diff --git a/aws/data_instance_profiles.go b/aws/data_instance_profiles.go index 8544116d54..0d8d8fc616 100644 --- a/aws/data_instance_profiles.go +++ b/aws/data_instance_profiles.go @@ -2,36 +2,81 @@ package aws import ( "context" + "encoding/json" + "fmt" + "reflect" + "regexp" "strings" "github.com/databricks/databricks-sdk-go" "github.com/databricks/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "golang.org/x/exp/slices" ) +func instanceProfileMatchesFilter(ip *instanceProfileData, filter *instanceProfileFilter) bool { + var ipMap map[string]interface{} + m, _ := json.Marshal(ip) + _ = json.Unmarshal(m, &ipMap) + val := ipMap[filter.Name] + stringVal := fmt.Sprint(val) + re := regexp.MustCompile(filter.Pattern) + return re.Match([]byte(regexp.QuoteMeta(stringVal))) +} + +type instanceProfileData struct { + Name string `json:"name"` + Arn string `json:"arn"` + RoleArn string `json:"role_arn"` + IsMeta bool `json:"is_meta"` +} + +type instanceProfileFilter struct { + Name string `json:"name"` + Pattern string `json:"pattern"` +} + func DataSourceInstanceProfiles() *schema.Resource { - type instanceProfileData struct { - Name string `json:"name,omitempty" tf:"computed"` - Arn string `json:"arn,omitempty" tf:"computed"` - RoleArn string `json:"role_arn,omitempty" tf:"computed"` - IsMeta bool `json:"is_meta,omitempty" tf:"computed"` - } return common.WorkspaceData(func(ctx context.Context, data *struct { InstanceProfiles []instanceProfileData `json:"instance_profiles,omitempty" tf:"computed"` + Filter instanceProfileFilter `json:"filter,omitempty" tf:"optional"` }, w *databricks.WorkspaceClient) error { + + if data.Filter != (instanceProfileFilter{}) { + if data.Filter.Pattern == "" { + return fmt.Errorf("field `pattern` cannot be empty") + } + var fieldNames []string + val := reflect.ValueOf(instanceProfileData{}) + for i := 0; i < val.Type().NumField(); i++ { + fieldNames = append(fieldNames, val.Type().Field(i).Tag.Get("json")) + } + if !slices.Contains(fieldNames, data.Filter.Name) { + if data.Filter.Name == "" { + return fmt.Errorf("field `name` cannot be empty") + } + return fmt.Errorf("`%s` is not a valid value for the name field. Must be one of [%s]", data.Filter.Name, strings.Join(fieldNames, ", ")) + } + } + instanceProfiles, err := w.InstanceProfiles.ListAll(ctx) if err != nil { return err } + for _, v := range instanceProfiles { arnSlices := strings.Split(v.InstanceProfileArn, "/") name := arnSlices[len(arnSlices)-1] - data.InstanceProfiles = append(data.InstanceProfiles, instanceProfileData{ + ipData := instanceProfileData{ Name: name, Arn: v.InstanceProfileArn, RoleArn: v.IamRoleArn, IsMeta: v.IsMetaInstanceProfile, - }) + } + if data.Filter != (instanceProfileFilter{}) && !instanceProfileMatchesFilter(&ipData, &data.Filter) { + continue + } + data.InstanceProfiles = append(data.InstanceProfiles, ipData) } return nil }) diff --git a/aws/data_instance_profiles_test.go b/aws/data_instance_profiles_test.go index b1e3b76ba1..90e45cbea7 100644 --- a/aws/data_instance_profiles_test.go +++ b/aws/data_instance_profiles_test.go @@ -5,33 +5,41 @@ import ( "github.com/databricks/databricks-sdk-go/service/compute" "github.com/databricks/terraform-provider-databricks/qa" + "github.com/stretchr/testify/assert" ) +var testResponse compute.ListInstanceProfilesResponse = compute.ListInstanceProfilesResponse{ + InstanceProfiles: []compute.InstanceProfile{ + { + IamRoleArn: "arn:aws:iam::123456789012:role/S3Access", + InstanceProfileArn: "arn:aws:iam::123456789012:instance-profile/S3Access", + IsMetaInstanceProfile: true, + }, + { + IamRoleArn: "arn:aws:iam::123456789098:role/KMSAccess", + InstanceProfileArn: "arn:aws:iam::123456789098:instance-profile/KMSAccess", + IsMetaInstanceProfile: false, + }, + { + InstanceProfileArn: "arn:aws:iam::123456789098:instance-profile/different", + IamRoleArn: "arn:aws:iam::123456789098:role/value", + IsMetaInstanceProfile: false, + }, + { + IamRoleArn: "arn:aws:iam::123456789098:role/Accesses", + InstanceProfileArn: "arn:aws:iam::123456789098:instance-profile/Accesses", + IsMetaInstanceProfile: false, + }, + }, +} + func TestInstanceProfilesData(t *testing.T) { qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ { Method: "GET", Resource: "/api/2.0/instance-profiles/list", - Response: compute.ListInstanceProfilesResponse{ - InstanceProfiles: []compute.InstanceProfile{ - { - IamRoleArn: "arn:aws:iam::123456789012:role/S3Access", - InstanceProfileArn: "arn:aws:iam::123456789012:instance-profile/S3Access", - IsMetaInstanceProfile: true, - }, - { - IamRoleArn: "arn:aws:iam::123456789098:role/KMSAccess", - InstanceProfileArn: "arn:aws:iam::123456789098:instance-profile/KMSAccess", - IsMetaInstanceProfile: false, - }, - { - InstanceProfileArn: "arn:aws:iam::123456789098:instance-profile/different", - IamRoleArn: "arn:aws:iam::123456789098:role/value", - IsMetaInstanceProfile: false, - }, - }, - }, + Response: testResponse, }, }, Resource: DataSourceInstanceProfiles(), @@ -58,6 +66,12 @@ func TestInstanceProfilesData(t *testing.T) { "role_arn": "arn:aws:iam::123456789098:role/value", "is_meta": false, }, + map[string]interface{}{ + "name": "Accesses", + "arn": "arn:aws:iam::123456789098:instance-profile/Accesses", + "role_arn": "arn:aws:iam::123456789098:role/Accesses", + "is_meta": false, + }, }, }) } @@ -125,3 +139,507 @@ func TestInstanceProfilesDataDuplicate(t *testing.T) { }, }) } + +func TestInstanceProfilesDataFilterContains(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "name" + pattern = "Access" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{ + map[string]interface{}{ + "name": "S3Access", + "arn": "arn:aws:iam::123456789012:instance-profile/S3Access", + "role_arn": "arn:aws:iam::123456789012:role/S3Access", + "is_meta": true, + }, + map[string]interface{}{ + "name": "KMSAccess", + "arn": "arn:aws:iam::123456789098:instance-profile/KMSAccess", + "role_arn": "arn:aws:iam::123456789098:role/KMSAccess", + "is_meta": false, + }, + map[string]interface{}{ + "name": "Accesses", + "arn": "arn:aws:iam::123456789098:instance-profile/Accesses", + "role_arn": "arn:aws:iam::123456789098:role/Accesses", + "is_meta": false, + }, + }, + }) +} + +func TestInstanceProfilesDataFilterExactEmpty(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "name" + pattern = "^Access$" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{}, + }) +} + +func TestInstanceProfilesDataFilterExact(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "name" + pattern = "^KMSAccess$" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{ + map[string]interface{}{ + "name": "KMSAccess", + "arn": "arn:aws:iam::123456789098:instance-profile/KMSAccess", + "role_arn": "arn:aws:iam::123456789098:role/KMSAccess", + "is_meta": false, + }, + }, + }) +} + +func TestInstanceProfilesDataFilterCase(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "role_arn" + pattern = "kms" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{}, + }) +} + +func TestInstanceProfilesDataFilterEndsWith(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "name" + pattern = "Access$" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{ + map[string]interface{}{ + "name": "S3Access", + "arn": "arn:aws:iam::123456789012:instance-profile/S3Access", + "role_arn": "arn:aws:iam::123456789012:role/S3Access", + "is_meta": true, + }, + map[string]interface{}{ + "name": "KMSAccess", + "arn": "arn:aws:iam::123456789098:instance-profile/KMSAccess", + "role_arn": "arn:aws:iam::123456789098:role/KMSAccess", + "is_meta": false, + }, + }, + }) +} + +func TestInstanceProfilesDataFilterName(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "name" + pattern = "Access$" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{ + map[string]interface{}{ + "name": "S3Access", + "arn": "arn:aws:iam::123456789012:instance-profile/S3Access", + "role_arn": "arn:aws:iam::123456789012:role/S3Access", + "is_meta": true, + }, + map[string]interface{}{ + "name": "KMSAccess", + "arn": "arn:aws:iam::123456789098:instance-profile/KMSAccess", + "role_arn": "arn:aws:iam::123456789098:role/KMSAccess", + "is_meta": false, + }, + }, + }) +} +func TestInstanceProfilesDataFilterArn(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "arn" + pattern = "arn:aws:iam::123456789098:instance-profile/different" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{ + map[string]interface{}{ + "name": "different", + "arn": "arn:aws:iam::123456789098:instance-profile/different", + "role_arn": "arn:aws:iam::123456789098:role/value", + "is_meta": false, + }, + }, + }) +} + +func TestInstanceProfilesDataFilterRoleArn(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "role_arn" + pattern = "KMS" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{ + map[string]interface{}{ + "name": "KMSAccess", + "arn": "arn:aws:iam::123456789098:instance-profile/KMSAccess", + "role_arn": "arn:aws:iam::123456789098:role/KMSAccess", + "is_meta": false, + }, + }, + }) +} + +func TestInstanceProfilesDataFilterIsMeta(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "is_meta" + pattern = "true" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{ + map[string]interface{}{ + "name": "S3Access", + "arn": "arn:aws:iam::123456789012:instance-profile/S3Access", + "role_arn": "arn:aws:iam::123456789012:role/S3Access", + "is_meta": true, + }, + }, + }) +} + +func TestInstanceProfilesDataFilterBadName(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "does_not_exist" + pattern = "value" + }`, + }.Apply(t) + assert.Error(t, err) + qa.AssertErrorStartsWith(t, err, "`does_not_exist` is not a valid value for the name field. Must be one of [") +} + +func TestInstanceProfilesDataFilterEmptyBlock(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter {}`, + }.Apply(t) + assert.Error(t, err) + qa.AssertErrorStartsWith(t, err, "invalid config supplied") +} + +func TestInstanceProfilesDataFilterNameOnly(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "does_not_exist" + }`, + }.Apply(t) + assert.Error(t, err) + qa.AssertErrorStartsWith(t, err, "invalid config supplied") +} + +func TestInstanceProfilesDataFilterPatternOnly(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + pattern = "val" + }`, + }.Apply(t) + assert.Error(t, err) + qa.AssertErrorStartsWith(t, err, "invalid config supplied") +} + +func TestInstanceProfilesDataFilterPatternEmpty(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "name" + pattern = "" + }`, + }.ExpectError(t, "field `pattern` cannot be empty") +} + +func TestInstanceProfilesDataFilterNameEmpty(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "" + pattern = "Access" + }`, + }.ExpectError(t, "field `name` cannot be empty") +} + +func TestInstanceProfilesDataFilterBadRegex(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "arn" + pattern = "*" + }`, + }.ExpectError(t, "panic: regexp: Compile(`*`): error parsing regexp: missing argument to repetition operator: `*`") +} + +func TestInstanceProfilesDataFilterMultiple(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "arn" + pattern = "KMS" + } + filter { + name = "is_meta" + pattern = "false" + }`, + }.Apply(t) + assert.Error(t, err) + qa.AssertErrorStartsWith(t, err, "invalid config supplied") +} + +func TestInstanceProfilesDataFilterEmptyArgs(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/instance-profiles/list", + Response: testResponse, + }, + }, + Resource: DataSourceInstanceProfiles(), + Read: true, + NonWritable: true, + ID: "_", + HCL: ` + filter { + name = "" + pattern = "" + }`, + }.ApplyAndExpectData(t, map[string]any{ + "instance_profiles": []interface{}{ + map[string]interface{}{ + "name": "S3Access", + "arn": "arn:aws:iam::123456789012:instance-profile/S3Access", + "role_arn": "arn:aws:iam::123456789012:role/S3Access", + "is_meta": true, + }, + map[string]interface{}{ + "name": "KMSAccess", + "arn": "arn:aws:iam::123456789098:instance-profile/KMSAccess", + "role_arn": "arn:aws:iam::123456789098:role/KMSAccess", + "is_meta": false, + }, + map[string]interface{}{ + "name": "different", + "arn": "arn:aws:iam::123456789098:instance-profile/different", + "role_arn": "arn:aws:iam::123456789098:role/value", + "is_meta": false, + }, + map[string]interface{}{ + "name": "Accesses", + "arn": "arn:aws:iam::123456789098:instance-profile/Accesses", + "role_arn": "arn:aws:iam::123456789098:role/Accesses", + "is_meta": false, + }, + }, + }) +} diff --git a/docs/data-sources/instance_profiles.md b/docs/data-sources/instance_profiles.md index 1d4172446e..c4dd29481e 100644 --- a/docs/data-sources/instance_profiles.md +++ b/docs/data-sources/instance_profiles.md @@ -18,10 +18,61 @@ output "all_instance_profiles" { value = data.databricks_instance_profiles.all.instance_profiles } ``` +**Filter Results** + +Starts With: + +```hcl +data "databricks_instance_profiles" "all" { + filter { + name = "name" + pattern = "^aws" + } +} +``` + +Ends With: + +```hcl +data "databricks_instance_profiles" "all" { + filter { + name = "arn" + pattern = "prod$" + } +} +``` + +Contains: + +```hcl +data "databricks_instance_profiles" "all" { + filter { + name = "role_arn" + pattern = "prod" + } +} +``` + +Equals: + +```hcl +data "databricks_instance_profiles" "all" { + filter { + name = "is_meta" + pattern = "^false$" + } +} +``` ## Argument Reference -There are no arguments available for this data source. +* `filter` - (Optional) Configuration block for filtering. Detailed below. + +### `filter` Configuration Block +The filter configuration block supports the following arguments: + +* `name` - (Required) Name of the filter field. Valid values can be found in the [attribute reference](#instance_profiles) below. +* `pattern` - (Required) Regex pattern to filter using. Follows the [Go Regexp syntax](https://pkg.go.dev/regexp/syntax) ## Attribute Reference diff --git a/internal/acceptance/data_instance_profiles_test.go b/internal/acceptance/data_instance_profiles_test.go index e5e9544f4e..ddca11554a 100644 --- a/internal/acceptance/data_instance_profiles_test.go +++ b/internal/acceptance/data_instance_profiles_test.go @@ -20,3 +20,20 @@ func TestAccDataSourceInstanceProfiles(t *testing.T) { }`, }) } + +func TestAccDataSourceInstanceProfilesFilter(t *testing.T) { + GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE") + workspaceLevel(t, step{ + Template: ` + data "databricks_instance_profiles" "this" { + filter { + name = "arn" + pattern = "^{TEST_EC2_INSTANCE_PROFILE}$" + } + } + + output "instance_profile" { + value = data.databricks_instance_profiles.this.instance_profiles[0] + }`, + }) +}