From d571e64ac1a659a12e3413da6a7f16f834743d5d Mon Sep 17 00:00:00 2001 From: LBGarber Date: Wed, 19 Jan 2022 13:02:18 -0500 Subject: [PATCH 1/4] Overhaul filter logic --- go.mod | 2 +- linode/helper/filter.go | 241 +++++++++++++++--- linode/images/datasource.go | 62 ++--- linode/images/datasource_test.go | 8 + linode/images/schema_datasource.go | 23 +- linode/images/tmpl/data_clientfilter.gotf | 27 ++ linode/images/tmpl/template.go | 5 + linode/instance/datasource.go | 37 +-- linode/instancetypes/datasource.go | 50 ++-- linode/instancetypes/schema_datasource.go | 16 +- linode/stackscripts/datasource.go | 64 ++--- linode/stackscripts/datasource_test.go | 7 + linode/stackscripts/schema_datasource.go | 18 +- .../stackscripts/tmpl/data_clientfilter.gotf | 39 +++ linode/stackscripts/tmpl/template.go | 8 + linode/vlan/datasource.go | 48 ++-- linode/vlan/schema_datasource.go | 10 +- website/docs/d/images.html.md | 8 + website/docs/d/instances.html.md | 6 + website/docs/d/stackscripts.html.md | 8 + 20 files changed, 468 insertions(+), 219 deletions(-) create mode 100644 linode/images/tmpl/data_clientfilter.gotf create mode 100644 linode/stackscripts/tmpl/data_clientfilter.gotf diff --git a/go.mod b/go.mod index c786923c8..6b8f4436c 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/linode/terraform-provider-linode require ( github.com/aws/aws-sdk-go v1.42.16 + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 github.com/linode/linodego v1.2.1 github.com/linode/linodego/k8s v0.0.0-20200831124119-58d5d5bb7947 @@ -26,7 +27,6 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-hclog v0.16.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.1 // indirect diff --git a/linode/helper/filter.go b/linode/helper/filter.go index 05cc41f62..0d9d17dce 100644 --- a/linode/helper/filter.go +++ b/linode/helper/filter.go @@ -1,35 +1,62 @@ package helper import ( + "context" "encoding/base64" "encoding/json" "fmt" + "log" + "reflect" "regexp" + "strconv" "strings" "time" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/linode/linodego" "golang.org/x/crypto/sha3" ) +// FilterConfig stores a map of FilterAttributes for a resource. +type FilterConfig map[string]FilterAttribute + // FilterTypeFunc is a function that takes in a filter name and value, // and returns the value converted to the correct filter type. -type FilterTypeFunc func(filterName string, value string) (interface{}, error) +type FilterTypeFunc func(value string) (interface{}, error) + +// FilterListFunc wraps a linodego list function. +type FilterListFunc func(context.Context, *linodego.Client, *linodego.ListOptions) ([]interface{}, error) + +// FilterFlattenFunc flattens an object into a map[string]interface{}. +type FilterFlattenFunc func(object interface{}) map[string]interface{} + +// FilterAttribute stores various configuration options about a single +// filterable field. +type FilterAttribute struct { + // Whether this field can be filtered on at an API level. + // If false, this filter will be handled on the client. + APIFilterable bool + + // Converts the filter string to the correct type. + TypeFunc FilterTypeFunc +} // FilterSchema should be referenced in a schema configuration in order to // enable filter functionality -func FilterSchema(validFilters []string) *schema.Schema { +func FilterSchema(filterConfig FilterConfig) *schema.Schema { return &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Description: "The name of the attribute to filter on.", - ValidateFunc: validation.StringInSlice(validFilters, false), - Required: true, + Type: schema.TypeString, + Description: "The name of the attribute to filter on.", + ValidateDiagFunc: filterValidateFunc(filterConfig, false), + Required: true, }, "values": { Type: schema.TypeList, @@ -52,12 +79,12 @@ func FilterSchema(validFilters []string) *schema.Schema { // OrderBySchema should be referenced in a schema configuration in order to // enable filter ordering functionality -func OrderBySchema(validFilters []string) *schema.Schema { +func OrderBySchema(filterConfig FilterConfig) *schema.Schema { return &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice(validFilters, false), - Description: "The attribute to order the results by.", + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: filterValidateFunc(filterConfig, true), + Description: "The attribute to order the results by.", } } @@ -89,7 +116,7 @@ func GetFilterID(d *schema.ResourceData) (string, error) { } // ConstructFilterString constructs a Linode filter JSON string from each filter element in the schema -func ConstructFilterString(d *schema.ResourceData, typeFunc FilterTypeFunc) (string, error) { +func ConstructFilterString(d *schema.ResourceData, filterConfig FilterConfig) (string, error) { filters := d.Get("filter").([]interface{}) resultMap := make(map[string]interface{}) @@ -111,10 +138,15 @@ func ConstructFilterString(d *schema.ResourceData, typeFunc FilterTypeFunc) (str continue } + // Defer this logic to the client if not API-filterable + if cfg, ok := filterConfig[name]; !ok || !cfg.APIFilterable { + continue + } + subFilter := make([]interface{}, len(values)) for i, value := range values { - value, err := typeFunc(name, value.(string)) + value, err := filterConfig[name].TypeFunc(value.(string)) if err != nil { return "", err } @@ -146,17 +178,23 @@ func ConstructFilterString(d *schema.ResourceData, typeFunc FilterTypeFunc) (str return "", err } + log.Println("[INFO]", string(result)) + return string(result), nil } // FilterResults filters the given results on the client-side filters present in the resource -func FilterResults(d *schema.ResourceData, items []interface{}) ([]map[string]interface{}, error) { +func FilterResults( + d *schema.ResourceData, + filterConfig FilterConfig, + items []interface{}) ([]map[string]interface{}, error) { + result := make([]map[string]interface{}, 0) for _, item := range items { item := item.(map[string]interface{}) - match, err := itemMatchesFilter(d, item) + match, err := itemMatchesFilter(d, filterConfig, item) if err != nil { return nil, err } @@ -171,7 +209,65 @@ func FilterResults(d *schema.ResourceData, items []interface{}) ([]map[string]in return result, nil } -func itemMatchesFilter(d *schema.ResourceData, item map[string]interface{}) (bool, error) { +func FilterResource( + ctx context.Context, + d *schema.ResourceData, + meta interface{}, + filterConfig FilterConfig, + listFunc FilterListFunc, + flattenFunc FilterFlattenFunc, +) ([]map[string]interface{}, error) { + client := meta.(*ProviderMeta).Client + + filterID, err := GetFilterID(d) + if err != nil { + return nil, fmt.Errorf("failed to generate filter id: %s", err) + } + + filter, err := ConstructFilterString(d, filterConfig) + if err != nil { + return nil, fmt.Errorf("failed to construct filter: %s", err) + } + + items, err := listFunc(ctx, &client, &linodego.ListOptions{ + Filter: filter, + }) + if err != nil { + return nil, fmt.Errorf("failed to list linode items: %s", err) + } + + itemsFlattened := make([]interface{}, len(items)) + for i, image := range items { + itemsFlattened[i] = flattenFunc(image) + } + + itemsFiltered, err := FilterResults(d, filterConfig, itemsFlattened) + if err != nil { + return nil, fmt.Errorf("failed to filter returned data: %s", err) + } + + d.SetId(filterID) + + return itemsFiltered, nil +} + +func FilterLatest(d *schema.ResourceData, items []map[string]interface{}) []map[string]interface{} { + if !d.Get("latest").(bool) { + return items + } + + if item := GetLatestCreated(items); item != nil { + return []map[string]interface{}{GetLatestCreated(items)} + } + + return []map[string]interface{}{} +} + +func itemMatchesFilter( + d *schema.ResourceData, + filterConfig FilterConfig, + item map[string]interface{}) (bool, error) { + filters := d.Get("filter").([]interface{}) for _, filter := range filters { @@ -181,16 +277,12 @@ func itemMatchesFilter(d *schema.ResourceData, item map[string]interface{}) (boo values := filter["values"].([]interface{}) matchBy := filter["match_by"].(string) - if matchBy == "exact" { - continue - } - itemValue, ok := item[name] if !ok { return false, fmt.Errorf("\"%v\" is not a valid attribute", name) } - valid, err := validateFilter(matchBy, name, ExpandStringList(values), itemValue) + valid, err := validateFilter(filterConfig, matchBy, name, ExpandStringList(values), itemValue) if err != nil { return false, err } @@ -203,11 +295,16 @@ func itemMatchesFilter(d *schema.ResourceData, item map[string]interface{}) (boo return true, nil } -func validateFilter(matchBy, name string, values []string, itemValue interface{}) (bool, error) { +func validateFilter( + filterConfig FilterConfig, + matchBy, name string, + values []string, + itemValue interface{}) (bool, error) { + // Filter recursively on lists (tags, etc.) if items, ok := itemValue.([]string); ok { for _, item := range items { - valid, err := validateFilter(matchBy, name, values, item) + valid, err := validateFilter(filterConfig, matchBy, name, values, item) if err != nil { return false, err } @@ -220,25 +317,33 @@ func validateFilter(matchBy, name string, values []string, itemValue interface{} return false, nil } - // Only string attributes should be considered - itemValueStr, ok := itemValue.(string) - if !ok { - return false, fmt.Errorf("\"%s\" is not a string", name) + cfg := filterConfig[name] + + valuesNormalized := make([]interface{}, len(values)) + for i := range valuesNormalized { + n, err := cfg.TypeFunc(values[i]) + if err != nil { + return false, err + } + + valuesNormalized[i] = n } switch matchBy { + case "exact": + return validateFilterExact(name, valuesNormalized, itemValue) case "substring", "sub": - return validateFilterSubstring(values, itemValueStr) + return validateFilterSubstring(name, valuesNormalized, itemValue) case "re", "regex": - return validateFilterRegex(values, itemValueStr) + return validateFilterRegex(name, valuesNormalized, itemValue) } return true, nil } -func validateFilterSubstring(values []string, result string) (bool, error) { +func validateFilterExact(name string, values []interface{}, result interface{}) (bool, error) { for _, value := range values { - if strings.Contains(result, value) { + if reflect.DeepEqual(result, value) { return true, nil } } @@ -246,14 +351,36 @@ func validateFilterSubstring(values []string, result string) (bool, error) { return false, nil } -func validateFilterRegex(values []string, result string) (bool, error) { +func validateFilterSubstring(name string, values []interface{}, result interface{}) (bool, error) { + itemValueStr, ok := result.(string) + if !ok { + return false, fmt.Errorf("\"%s\" is not a string (type %s) and cannot be filtered on substring", + name, reflect.TypeOf(result)) + } + for _, value := range values { - r, err := regexp.Compile(value) + if strings.Contains(itemValueStr, value.(string)) { + return true, nil + } + } + + return false, nil +} + +func validateFilterRegex(name string, values []interface{}, result interface{}) (bool, error) { + itemValueStr, ok := result.(string) + if !ok { + return false, fmt.Errorf("\"%s\" is not a string (type %s) and cannot be filtered on regex", + name, reflect.TypeOf(result)) + } + + for _, value := range values { + r, err := regexp.Compile(value.(string)) if err != nil { return false, fmt.Errorf("failed to compile regex: %s", err) } - if r.MatchString(result) { + if r.MatchString(itemValueStr) { return true, nil } } @@ -286,3 +413,49 @@ func GetLatestCreated(data []map[string]interface{}) map[string]interface{} { return latestEntity } + +func filterValidateFunc(filterConfig FilterConfig, apiOnly bool) schema.SchemaValidateDiagFunc { + return func(i interface{}, path cty.Path) diag.Diagnostics { + val := i.(string) + + cfg, ok := filterConfig[val] + + if !ok { + validFilters := make([]string, 0) + for k := range filterConfig { + validFilters = append(validFilters, k) + } + + return diag.Errorf("\"%s\" is not a filterable field. Valid filters: %s", + val, strings.Join(validFilters, ", ")) + } + + if apiOnly && !cfg.APIFilterable { + validFilters := make([]string, 0) + for k, v := range filterConfig { + if !v.APIFilterable { + continue + } + + validFilters = append(validFilters, k) + } + + return diag.Errorf("\"%s\" is an unsupported filter for this field. Valid filters: %s", + val, strings.Join(validFilters, ", ")) + } + + return nil + } +} + +func FilterTypeString(value string) (interface{}, error) { + return value, nil +} + +func FilterTypeInt(value string) (interface{}, error) { + return strconv.Atoi(value) +} + +func FilterTypeBool(value string) (interface{}, error) { + return strconv.ParseBool(value) +} diff --git a/linode/images/datasource.go b/linode/images/datasource.go index 6bcf3de29..a2ea04946 100644 --- a/linode/images/datasource.go +++ b/linode/images/datasource.go @@ -2,7 +2,6 @@ package images import ( "context" - "strconv" "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -19,52 +18,37 @@ func DataSource() *schema.Resource { } func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*helper.ProviderMeta).Client - - latestFlag := d.Get("latest").(bool) - - filterID, err := helper.GetFilterID(d) + results, err := helper.FilterResource(ctx, d, meta, filterConfig, listImages, flattenImage) if err != nil { - return diag.Errorf("failed to generate filter id: %s", err) + return nil } - filter, err := helper.ConstructFilterString(d, imageValueToFilterType) - if err != nil { - return diag.Errorf("failed to construct filter: %s", err) - } + results = helper.FilterLatest(d, results) - images, err := client.ListImages(ctx, &linodego.ListOptions{ - Filter: filter, - }) - if err != nil { - return diag.Errorf("failed to list linode images: %s", err) - } + d.Set("images", results) - imagesFlattened := make([]interface{}, len(images)) - for i, image := range images { - imagesFlattened[i] = flattenImage(&image) - } + return nil +} - imagesFiltered, err := helper.FilterResults(d, imagesFlattened) +func listImages( + ctx context.Context, client *linodego.Client, options *linodego.ListOptions) ([]interface{}, error) { + images, err := client.ListImages(ctx, options) if err != nil { - return diag.Errorf("failed to filter returned images: %s", err) + return nil, err } - if latestFlag { - latestImage := helper.GetLatestCreated(imagesFiltered) + result := make([]interface{}, len(images)) - if latestImage != nil { - imagesFiltered = []map[string]interface{}{latestImage} - } + for i, v := range images { + result[i] = v } - d.SetId(filterID) - d.Set("images", imagesFiltered) - - return nil + return result, nil } -func flattenImage(image *linodego.Image) map[string]interface{} { +func flattenImage(data interface{}) map[string]interface{} { + image := data.(linodego.Image) + result := make(map[string]interface{}) result["id"] = image.ID @@ -88,15 +72,3 @@ func flattenImage(image *linodego.Image) map[string]interface{} { return result } - -func imageValueToFilterType(filterName, value string) (interface{}, error) { - switch filterName { - case "deprecated", "is_public": - return strconv.ParseBool(value) - - case "size": - return strconv.Atoi(value) - } - - return value, nil -} diff --git a/linode/images/datasource_test.go b/linode/images/datasource_test.go index 43d648fc1..293f48c12 100644 --- a/linode/images/datasource_test.go +++ b/linode/images/datasource_test.go @@ -81,6 +81,14 @@ func TestAccDataSourceImages_basic(t *testing.T) { acceptance.CheckResourceAttrContains(resourceName, "images.0.label", "Alpine"), ), }, + + { + Config: tmpl.DataClientFilter(t, imageName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "images.#", "1"), + acceptance.CheckResourceAttrContains(resourceName, "images.0.label", imageName), + ), + }, }, }) } diff --git a/linode/images/schema_datasource.go b/linode/images/schema_datasource.go index 6d20466c9..9a9d5131f 100644 --- a/linode/images/schema_datasource.go +++ b/linode/images/schema_datasource.go @@ -2,11 +2,28 @@ package images import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/linode/linodego" "github.com/linode/terraform-provider-linode/linode/helper" "github.com/linode/terraform-provider-linode/linode/image" ) -var filterableFields = []string{"deprecated", "is_public", "label", "size", "vendor"} +var filterConfig = helper.FilterConfig{ + "deprecated": {APIFilterable: true, TypeFunc: helper.FilterTypeBool}, + "is_public": {APIFilterable: true, TypeFunc: helper.FilterTypeBool}, + "label": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + "size": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, + "type": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + "vendor": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + + "created_by": {TypeFunc: helper.FilterTypeString}, + "id": {TypeFunc: helper.FilterTypeString}, + "status": { + TypeFunc: func(value string) (interface{}, error) { + return linodego.ImageStatus(value), nil + }, + }, + "description": {TypeFunc: helper.FilterTypeString}, +} var dataSourceSchema = map[string]*schema.Schema{ "latest": { @@ -15,9 +32,9 @@ var dataSourceSchema = map[string]*schema.Schema{ Optional: true, Default: false, }, - "order_by": helper.OrderBySchema(filterableFields), + "order_by": helper.OrderBySchema(filterConfig), "order": helper.OrderSchema(), - "filter": helper.FilterSchema(filterableFields), + "filter": helper.FilterSchema(filterConfig), "images": { Type: schema.TypeList, Description: "The returned list of Images.", diff --git a/linode/images/tmpl/data_clientfilter.gotf b/linode/images/tmpl/data_clientfilter.gotf new file mode 100644 index 000000000..c39b6ba87 --- /dev/null +++ b/linode/images/tmpl/data_clientfilter.gotf @@ -0,0 +1,27 @@ +{{ define "images_data_clientfilter" }} + +{{ template "images_data_base" . }} + +data "linode_images" "foobar" { + filter { + name = "id" + values = [linode_image.foobar.id] + } + + filter { + name = "status" + values = [linode_image.foobar.status] + } + + filter { + name = "description" + values = [linode_image.foobar.description] + } + + filter { + name = "created_by" + values = [linode_image.foobar.created_by] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/images/tmpl/template.go b/linode/images/tmpl/template.go index 574b018ad..111cae5ad 100644 --- a/linode/images/tmpl/template.go +++ b/linode/images/tmpl/template.go @@ -34,3 +34,8 @@ func DataSubstring(t *testing.T, image string) string { return acceptance.ExecuteTemplate(t, "images_data_substring", TemplateData{Image: image}) } + +func DataClientFilter(t *testing.T, image string) string { + return acceptance.ExecuteTemplate(t, + "images_data_clientfilter", TemplateData{Image: image}) +} diff --git a/linode/instance/datasource.go b/linode/instance/datasource.go index 438a05e73..e6fa520f4 100644 --- a/linode/instance/datasource.go +++ b/linode/instance/datasource.go @@ -2,7 +2,6 @@ package instance import ( "context" - "strconv" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -10,7 +9,23 @@ import ( "github.com/linode/terraform-provider-linode/linode/helper" ) -var filterableFields = []string{"group", "id", "image", "label", "region", "tags"} +var filterConfig = map[string]helper.FilterAttribute{ + "group": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + "id": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, + "image": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + "label": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + "region": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + + // Tags must be filtered on the client + "tags": {TypeFunc: helper.FilterTypeString}, + "status": { + TypeFunc: func(value string) (interface{}, error) { + return linodego.InstanceStatus(value), nil + }, + }, + "type": {TypeFunc: helper.FilterTypeString}, + "watchdog_enabled": {TypeFunc: helper.FilterTypeBool}, +} func dataSourceInstance() *schema.Resource { return &schema.Resource{ @@ -22,8 +37,8 @@ func DataSource() *schema.Resource { return &schema.Resource{ ReadContext: readDataSource, Schema: map[string]*schema.Schema{ - "filter": helper.FilterSchema(filterableFields), - "order_by": helper.OrderBySchema(filterableFields), + "filter": helper.FilterSchema(filterConfig), + "order_by": helper.OrderBySchema(filterConfig), "order": helper.OrderSchema(), "instances": { Type: schema.TypeList, @@ -43,7 +58,7 @@ func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{ return diag.Errorf("failed to generate filter id: %s", err) } - filter, err := helper.ConstructFilterString(d, instanceValueToFilterType) + filter, err := helper.ConstructFilterString(d, filterConfig) if err != nil { return diag.Errorf("failed to construct filter: %s", err) } @@ -70,7 +85,7 @@ func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{ flattenedInstances[i] = instanceMap } - instancesFiltered, err := helper.FilterResults(d, flattenedInstances) + instancesFiltered, err := helper.FilterResults(d, filterConfig, flattenedInstances) if err != nil { return diag.Errorf("failed to filter returned instances: %s", err) } @@ -92,13 +107,3 @@ func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{ return nil } - -// instanceValueToFilterType converts the given value to the correct type depending on the filter name. -func instanceValueToFilterType(filterName, value string) (interface{}, error) { - switch filterName { - case "id": - return strconv.Atoi(value) - } - - return value, nil -} diff --git a/linode/instancetypes/datasource.go b/linode/instancetypes/datasource.go index 898d5e4e3..7f447995c 100644 --- a/linode/instancetypes/datasource.go +++ b/linode/instancetypes/datasource.go @@ -7,7 +7,6 @@ import ( "github.com/linode/terraform-provider-linode/linode/helper" "context" - "strconv" ) func DataSource() *schema.Resource { @@ -18,43 +17,35 @@ func DataSource() *schema.Resource { } func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*helper.ProviderMeta).Client - - filterID, err := helper.GetFilterID(d) + results, err := helper.FilterResource(ctx, d, meta, filterConfig, listTypes, flattenType) if err != nil { - return diag.Errorf("failed to generate filter id: %s", err) + return nil } - filter, err := helper.ConstructFilterString(d, typeValueToFilterType) - if err != nil { - return diag.Errorf("failed to construct filter: %s", err) - } + d.Set("types", results) - types, err := client.ListTypes(ctx, &linodego.ListOptions{ - Filter: filter, - }) + return nil +} +func listTypes( + ctx context.Context, client *linodego.Client, options *linodego.ListOptions) ([]interface{}, error) { + types, err := client.ListTypes(ctx, options) if err != nil { - return diag.Errorf("failed to list linode types: %s", err) + return nil, err } - typesFlattened := make([]interface{}, len(types)) - for i, t := range types { - typesFlattened[i] = flattenType(&t) - } + result := make([]interface{}, len(types)) - typesFiltered, err := helper.FilterResults(d, typesFlattened) - if err != nil { - return diag.Errorf("failed to filter returned types: %s", err) + for i, v := range types { + result[i] = v } - d.SetId(filterID) - d.Set("types", typesFiltered) - - return nil + return result, nil } -func flattenType(t *linodego.LinodeType) map[string]interface{} { +func flattenType(data interface{}) map[string]interface{} { + t := data.(linodego.LinodeType) + result := make(map[string]interface{}) result["id"] = t.ID @@ -90,12 +81,3 @@ func flattenType(t *linodego.LinodeType) map[string]interface{} { return result } - -func typeValueToFilterType(filterName, value string) (interface{}, error) { - switch filterName { - case "disk", "gpus", "memory", "transfer", "vcpus": - return strconv.Atoi(value) - } - - return value, nil -} diff --git a/linode/instancetypes/schema_datasource.go b/linode/instancetypes/schema_datasource.go index 839066c38..c187a2ba7 100644 --- a/linode/instancetypes/schema_datasource.go +++ b/linode/instancetypes/schema_datasource.go @@ -6,13 +6,21 @@ import ( "github.com/linode/terraform-provider-linode/linode/instancetype" ) -var filterableFields = []string{"class", "disk", "gpus", "label", - "memory", "network_out", "transfer", "vcpus"} +var filterConfig = map[string]helper.FilterAttribute{ + "class": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + "disk": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, + "gpus": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, + "label": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + "memory": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, + "network_out": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, + "transfer": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, + "vcpus": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, +} var dataSourceSchema = map[string]*schema.Schema{ - "order_by": helper.OrderBySchema(filterableFields), + "order_by": helper.OrderBySchema(filterConfig), "order": helper.OrderSchema(), - "filter": helper.FilterSchema(filterableFields), + "filter": helper.FilterSchema(filterConfig), "types": { Type: schema.TypeList, Description: "The returned list of Types.", diff --git a/linode/stackscripts/datasource.go b/linode/stackscripts/datasource.go index 96fd4500e..f42aca46b 100644 --- a/linode/stackscripts/datasource.go +++ b/linode/stackscripts/datasource.go @@ -2,7 +2,6 @@ package stackscripts import ( "context" - "strconv" "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -20,52 +19,37 @@ func DataSource() *schema.Resource { } func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*helper.ProviderMeta).Client - - latestFlag := d.Get("latest").(bool) - - filterID, err := helper.GetFilterID(d) + results, err := helper.FilterResource(ctx, d, meta, filterConfig, listStackScripts, flattenStackScript) if err != nil { - return diag.Errorf("failed to generate filter id: %s", err) + return nil } - filter, err := helper.ConstructFilterString(d, scriptValueToFilterType) - if err != nil { - return diag.Errorf("failed to construct filter: %s", err) - } + results = helper.FilterLatest(d, results) - scripts, err := client.ListStackscripts(ctx, &linodego.ListOptions{ - Filter: filter, - }) - if err != nil { - return diag.Errorf("failed to list linode scripts: %s", err) - } + d.Set("stackscripts", results) - scriptsFlattened := make([]interface{}, len(scripts)) - for i, image := range scripts { - scriptsFlattened[i] = flattenStackScript(&image) - } + return nil +} - scriptsFiltered, err := helper.FilterResults(d, scriptsFlattened) +func listStackScripts( + ctx context.Context, client *linodego.Client, options *linodego.ListOptions) ([]interface{}, error) { + scripts, err := client.ListStackscripts(ctx, options) if err != nil { - return diag.Errorf("failed to filter returned scripts: %s", err) + return nil, err } - if latestFlag { - latestScript := helper.GetLatestCreated(scriptsFiltered) + result := make([]interface{}, len(scripts)) - if latestScript != nil { - scriptsFiltered = []map[string]interface{}{latestScript} - } + for i, v := range scripts { + result[i] = v } - d.SetId(filterID) - d.Set("stackscripts", scriptsFiltered) - - return nil + return result, nil } -func flattenStackScript(script *linodego.Stackscript) map[string]interface{} { +func flattenStackScript(data interface{}) map[string]interface{} { + script := data.(linodego.Stackscript) + result := make(map[string]interface{}) result["id"] = script.ID @@ -88,19 +72,7 @@ func flattenStackScript(script *linodego.Stackscript) map[string]interface{} { result["updated"] = script.Updated.Format(time.RFC3339) } - result["user_defined_fields"] = stackscript.GetStackScriptUserDefinedFields(script) + result["user_defined_fields"] = stackscript.GetStackScriptUserDefinedFields(&script) return result } - -func scriptValueToFilterType(filterName, value string) (interface{}, error) { - switch filterName { - case "deprecated", "mine": - return strconv.ParseBool(value) - - case "deployments_total": - return strconv.Atoi(value) - } - - return value, nil -} diff --git a/linode/stackscripts/datasource_test.go b/linode/stackscripts/datasource_test.go index 8a5bf5aac..4034f3172 100644 --- a/linode/stackscripts/datasource_test.go +++ b/linode/stackscripts/datasource_test.go @@ -45,6 +45,13 @@ func TestAccDataSourceStackscripts_basic(t *testing.T) { validateStackscript(resourceName, stackScriptName), ), }, + { + Config: tmpl.DataClientFilter(t, stackScriptName, basicStackScript), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "stackscripts.#", "1"), + validateStackscript(resourceName, stackScriptName), + ), + }, }, }) } diff --git a/linode/stackscripts/schema_datasource.go b/linode/stackscripts/schema_datasource.go index 2950d9ed5..bd2fcf951 100644 --- a/linode/stackscripts/schema_datasource.go +++ b/linode/stackscripts/schema_datasource.go @@ -5,8 +5,18 @@ import ( "github.com/linode/terraform-provider-linode/linode/helper" ) -var filterableFields = []string{"deployments_total", "description", - "is_public", "label", "mine", "rev_note"} +var filterConfig = map[string]helper.FilterAttribute{ + "deployments_total": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, + "description": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + "is_public": {APIFilterable: true, TypeFunc: helper.FilterTypeBool}, + "label": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, + + "rev_note": {TypeFunc: helper.FilterTypeString}, + "mine": {TypeFunc: helper.FilterTypeBool}, + "deployments_active": {TypeFunc: helper.FilterTypeInt}, + "images": {TypeFunc: helper.FilterTypeString}, + "username": {TypeFunc: helper.FilterTypeString}, +} var dataSourceSchema = map[string]*schema.Schema{ "latest": { @@ -15,9 +25,9 @@ var dataSourceSchema = map[string]*schema.Schema{ Optional: true, Default: false, }, - "order_by": helper.OrderBySchema(filterableFields), + "order_by": helper.OrderBySchema(filterConfig), "order": helper.OrderSchema(), - "filter": helper.FilterSchema(filterableFields), + "filter": helper.FilterSchema(filterConfig), "stackscripts": { Type: schema.TypeList, Description: "The returned list of StackScripts.", diff --git a/linode/stackscripts/tmpl/data_clientfilter.gotf b/linode/stackscripts/tmpl/data_clientfilter.gotf new file mode 100644 index 000000000..6f4e1bcb3 --- /dev/null +++ b/linode/stackscripts/tmpl/data_clientfilter.gotf @@ -0,0 +1,39 @@ +{{ define "stackscripts_data_clientfilter" }} + +resource "linode_stackscript" "stackscript" { + label = "{{.Label}}" + script = < Date: Fri, 21 Jan 2022 11:08:16 -0500 Subject: [PATCH 2/4] Make functions relative to FilterConfig --- linode/helper/filter.go | 217 +++++++++--------- linode/images/datasource.go | 5 +- linode/images/schema_datasource.go | 6 +- linode/instance/datasource.go | 14 +- linode/instance/datasource_test.go | 48 ++-- linode/instance/tmpl/template.go | 20 +- .../tmpl/templates/data_clientfilter.gotf | 30 +++ .../tmpl/templates/data_multiple.gotf | 38 +-- .../tmpl/templates/data_multiple_base.gotf | 14 ++ .../tmpl/templates/data_multiple_order.gotf | 32 +++ .../tmpl/templates/data_multiple_regex.gotf | 12 +- .../tmpl/templates/disk_multiple_order.gotf | 74 ------ linode/instancetypes/datasource.go | 6 +- linode/instancetypes/schema_datasource.go | 8 +- linode/stackscripts/datasource.go | 5 +- linode/stackscripts/schema_datasource.go | 8 +- linode/vlan/datasource.go | 12 +- linode/vlan/schema_datasource.go | 2 +- 18 files changed, 247 insertions(+), 304 deletions(-) create mode 100644 linode/instance/tmpl/templates/data_clientfilter.gotf create mode 100644 linode/instance/tmpl/templates/data_multiple_base.gotf create mode 100644 linode/instance/tmpl/templates/data_multiple_order.gotf delete mode 100644 linode/instance/tmpl/templates/disk_multiple_order.gotf diff --git a/linode/helper/filter.go b/linode/helper/filter.go index 0d9d17dce..0c1da1b21 100644 --- a/linode/helper/filter.go +++ b/linode/helper/filter.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "log" "reflect" "regexp" "strconv" @@ -46,7 +45,7 @@ type FilterAttribute struct { // FilterSchema should be referenced in a schema configuration in order to // enable filter functionality -func FilterSchema(filterConfig FilterConfig) *schema.Schema { +func (f FilterConfig) FilterSchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -55,7 +54,7 @@ func FilterSchema(filterConfig FilterConfig) *schema.Schema { "name": { Type: schema.TypeString, Description: "The name of the attribute to filter on.", - ValidateDiagFunc: filterValidateFunc(filterConfig, false), + ValidateDiagFunc: f.ValidateDiagFunc(false), Required: true, }, "values": { @@ -79,16 +78,16 @@ func FilterSchema(filterConfig FilterConfig) *schema.Schema { // OrderBySchema should be referenced in a schema configuration in order to // enable filter ordering functionality -func OrderBySchema(filterConfig FilterConfig) *schema.Schema { +func (f FilterConfig) OrderBySchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeString, Optional: true, - ValidateDiagFunc: filterValidateFunc(filterConfig, true), + ValidateDiagFunc: f.ValidateDiagFunc(true), Description: "The attribute to order the results by.", } } -func OrderSchema() *schema.Schema { +func (f FilterConfig) OrderSchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -98,25 +97,8 @@ func OrderSchema() *schema.Schema { } } -// GetFilterID creates a unique ID specific to the current filter data source -func GetFilterID(d *schema.ResourceData) (string, error) { - idMap := map[string]interface{}{ - "filter": d.Get("filter"), - "order": d.Get("order"), - "order_by": d.Get("order_by"), - } - - result, err := json.Marshal(idMap) - if err != nil { - return "", err - } - - hash := sha3.Sum512(result) - return base64.StdEncoding.EncodeToString(hash[:]), nil -} - // ConstructFilterString constructs a Linode filter JSON string from each filter element in the schema -func ConstructFilterString(d *schema.ResourceData, filterConfig FilterConfig) (string, error) { +func (f FilterConfig) ConstructFilterString(d *schema.ResourceData) (string, error) { filters := d.Get("filter").([]interface{}) resultMap := make(map[string]interface{}) @@ -139,14 +121,14 @@ func ConstructFilterString(d *schema.ResourceData, filterConfig FilterConfig) (s } // Defer this logic to the client if not API-filterable - if cfg, ok := filterConfig[name]; !ok || !cfg.APIFilterable { + if cfg, ok := f[name]; !ok || !cfg.APIFilterable { continue } subFilter := make([]interface{}, len(values)) for i, value := range values { - value, err := filterConfig[name].TypeFunc(value.(string)) + value, err := f[name].TypeFunc(value.(string)) if err != nil { return "", err } @@ -178,15 +160,12 @@ func ConstructFilterString(d *schema.ResourceData, filterConfig FilterConfig) (s return "", err } - log.Println("[INFO]", string(result)) - return string(result), nil } -// FilterResults filters the given results on the client-side filters present in the resource -func FilterResults( +// FilterResults filters the given results on the client-side filters present in the resource. +func (f FilterConfig) FilterResults( d *schema.ResourceData, - filterConfig FilterConfig, items []interface{}) ([]map[string]interface{}, error) { result := make([]map[string]interface{}, 0) @@ -194,7 +173,7 @@ func FilterResults( for _, item := range items { item := item.(map[string]interface{}) - match, err := itemMatchesFilter(d, filterConfig, item) + match, err := f.itemMatchesFilter(d, item) if err != nil { return nil, err } @@ -209,26 +188,27 @@ func FilterResults( return result, nil } -func FilterResource( +// FilterDataSource should be run from inside the ReadContext function of a data source. +func (f FilterConfig) FilterDataSource( ctx context.Context, d *schema.ResourceData, meta interface{}, - filterConfig FilterConfig, listFunc FilterListFunc, flattenFunc FilterFlattenFunc, ) ([]map[string]interface{}, error) { client := meta.(*ProviderMeta).Client - filterID, err := GetFilterID(d) + filterID, err := f.GetFilterID(d) if err != nil { return nil, fmt.Errorf("failed to generate filter id: %s", err) } - filter, err := ConstructFilterString(d, filterConfig) + filter, err := f.ConstructFilterString(d) if err != nil { return nil, fmt.Errorf("failed to construct filter: %s", err) } + // Call linode list function defined by data source items, err := listFunc(ctx, &client, &linodego.ListOptions{ Filter: filter, }) @@ -241,7 +221,7 @@ func FilterResource( itemsFlattened[i] = flattenFunc(image) } - itemsFiltered, err := FilterResults(d, filterConfig, itemsFlattened) + itemsFiltered, err := f.FilterResults(d, itemsFlattened) if err != nil { return nil, fmt.Errorf("failed to filter returned data: %s", err) } @@ -251,21 +231,101 @@ func FilterResource( return itemsFiltered, nil } -func FilterLatest(d *schema.ResourceData, items []map[string]interface{}) []map[string]interface{} { +// GetValidFilters returns a slice of valid filters for the filter config. +func (f FilterConfig) GetValidFilters(apiOnly bool) []string { + result := make([]string, 0) + + for k, v := range f { + if apiOnly && !v.APIFilterable { + continue + } + + result = append(result, k) + } + + return result +} + +// ValidateDiagFunc should be plugged into the `filter` field of a filterable data source. +func (f FilterConfig) ValidateDiagFunc(apiOnly bool) schema.SchemaValidateDiagFunc { + return func(i interface{}, path cty.Path) diag.Diagnostics { + val := i.(string) + + cfg, ok := f[val] + + if !ok { + return diag.Errorf("\"%s\" is not a filterable field. Valid filters: %s", + val, strings.Join(f.GetValidFilters(false), ", ")) + } + + if apiOnly && !cfg.APIFilterable { + return diag.Errorf("\"%s\" is an unsupported filter for this field. Valid filters: %s", + val, strings.Join(f.GetValidFilters(true), ", ")) + } + + return nil + } +} + +// GetFilterID creates a unique ID specific to the current filter data source +func (f FilterConfig) GetFilterID(d *schema.ResourceData) (string, error) { + idMap := map[string]interface{}{ + "filter": d.Get("filter"), + "order": d.Get("order"), + "order_by": d.Get("order_by"), + } + + result, err := json.Marshal(idMap) + if err != nil { + return "", err + } + + hash := sha3.Sum512(result) + return base64.StdEncoding.EncodeToString(hash[:]), nil +} + +// FilterLatest returns only the latest element in the given slice only if `latest` == true. +func (f FilterConfig) FilterLatest(d *schema.ResourceData, items []map[string]interface{}) []map[string]interface{} { if !d.Get("latest").(bool) { return items } - if item := GetLatestCreated(items); item != nil { - return []map[string]interface{}{GetLatestCreated(items)} + if item := f.GetLatestCreated(items); item != nil { + return []map[string]interface{}{f.GetLatestCreated(items)} } return []map[string]interface{}{} } -func itemMatchesFilter( +// GetLatestCreated returns only the latest element in the given slice. +func (f FilterConfig) GetLatestCreated(data []map[string]interface{}) map[string]interface{} { + var latestCreated time.Time + var latestEntity map[string]interface{} + + for _, image := range data { + created, ok := image["created"] + if !ok { + continue + } + + createdTime, err := time.Parse(time.RFC3339, created.(string)) + if err != nil { + return nil + } + + if latestEntity != nil && !createdTime.After(latestCreated) { + continue + } + + latestCreated = createdTime + latestEntity = image + } + + return latestEntity +} + +func (f FilterConfig) itemMatchesFilter( d *schema.ResourceData, - filterConfig FilterConfig, item map[string]interface{}) (bool, error) { filters := d.Get("filter").([]interface{}) @@ -282,7 +342,7 @@ func itemMatchesFilter( return false, fmt.Errorf("\"%v\" is not a valid attribute", name) } - valid, err := validateFilter(filterConfig, matchBy, name, ExpandStringList(values), itemValue) + valid, err := f.validateFilter(matchBy, name, ExpandStringList(values), itemValue) if err != nil { return false, err } @@ -295,8 +355,7 @@ func itemMatchesFilter( return true, nil } -func validateFilter( - filterConfig FilterConfig, +func (f FilterConfig) validateFilter( matchBy, name string, values []string, itemValue interface{}) (bool, error) { @@ -304,7 +363,7 @@ func validateFilter( // Filter recursively on lists (tags, etc.) if items, ok := itemValue.([]string); ok { for _, item := range items { - valid, err := validateFilter(filterConfig, matchBy, name, values, item) + valid, err := f.validateFilter(matchBy, name, values, item) if err != nil { return false, err } @@ -317,7 +376,7 @@ func validateFilter( return false, nil } - cfg := filterConfig[name] + cfg := f[name] valuesNormalized := make([]interface{}, len(values)) for i := range valuesNormalized { @@ -331,7 +390,7 @@ func validateFilter( switch matchBy { case "exact": - return validateFilterExact(name, valuesNormalized, itemValue) + return validateFilterExact(valuesNormalized, itemValue) case "substring", "sub": return validateFilterSubstring(name, valuesNormalized, itemValue) case "re", "regex": @@ -341,7 +400,7 @@ func validateFilter( return true, nil } -func validateFilterExact(name string, values []interface{}, result interface{}) (bool, error) { +func validateFilterExact(values []interface{}, result interface{}) (bool, error) { for _, value := range values { if reflect.DeepEqual(result, value) { return true, nil @@ -388,66 +447,6 @@ func validateFilterRegex(name string, values []interface{}, result interface{}) return false, nil } -func GetLatestCreated(data []map[string]interface{}) map[string]interface{} { - var latestCreated time.Time - var latestEntity map[string]interface{} - - for _, image := range data { - created, ok := image["created"] - if !ok { - continue - } - - createdTime, err := time.Parse(time.RFC3339, created.(string)) - if err != nil { - return nil - } - - if latestEntity != nil && !createdTime.After(latestCreated) { - continue - } - - latestCreated = createdTime - latestEntity = image - } - - return latestEntity -} - -func filterValidateFunc(filterConfig FilterConfig, apiOnly bool) schema.SchemaValidateDiagFunc { - return func(i interface{}, path cty.Path) diag.Diagnostics { - val := i.(string) - - cfg, ok := filterConfig[val] - - if !ok { - validFilters := make([]string, 0) - for k := range filterConfig { - validFilters = append(validFilters, k) - } - - return diag.Errorf("\"%s\" is not a filterable field. Valid filters: %s", - val, strings.Join(validFilters, ", ")) - } - - if apiOnly && !cfg.APIFilterable { - validFilters := make([]string, 0) - for k, v := range filterConfig { - if !v.APIFilterable { - continue - } - - validFilters = append(validFilters, k) - } - - return diag.Errorf("\"%s\" is an unsupported filter for this field. Valid filters: %s", - val, strings.Join(validFilters, ", ")) - } - - return nil - } -} - func FilterTypeString(value string) (interface{}, error) { return value, nil } diff --git a/linode/images/datasource.go b/linode/images/datasource.go index a2ea04946..4d9aee876 100644 --- a/linode/images/datasource.go +++ b/linode/images/datasource.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/linode/linodego" - "github.com/linode/terraform-provider-linode/linode/helper" ) func DataSource() *schema.Resource { @@ -18,12 +17,12 @@ func DataSource() *schema.Resource { } func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - results, err := helper.FilterResource(ctx, d, meta, filterConfig, listImages, flattenImage) + results, err := filterConfig.FilterDataSource(ctx, d, meta, listImages, flattenImage) if err != nil { return nil } - results = helper.FilterLatest(d, results) + results = filterConfig.FilterLatest(d, results) d.Set("images", results) diff --git a/linode/images/schema_datasource.go b/linode/images/schema_datasource.go index 9a9d5131f..11276f1df 100644 --- a/linode/images/schema_datasource.go +++ b/linode/images/schema_datasource.go @@ -32,9 +32,9 @@ var dataSourceSchema = map[string]*schema.Schema{ Optional: true, Default: false, }, - "order_by": helper.OrderBySchema(filterConfig), - "order": helper.OrderSchema(), - "filter": helper.FilterSchema(filterConfig), + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), + "filter": filterConfig.FilterSchema(), "images": { Type: schema.TypeList, Description: "The returned list of Images.", diff --git a/linode/instance/datasource.go b/linode/instance/datasource.go index e6fa520f4..ef7bcd9cf 100644 --- a/linode/instance/datasource.go +++ b/linode/instance/datasource.go @@ -9,7 +9,7 @@ import ( "github.com/linode/terraform-provider-linode/linode/helper" ) -var filterConfig = map[string]helper.FilterAttribute{ +var filterConfig = helper.FilterConfig{ "group": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, "id": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, "image": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, @@ -37,9 +37,9 @@ func DataSource() *schema.Resource { return &schema.Resource{ ReadContext: readDataSource, Schema: map[string]*schema.Schema{ - "filter": helper.FilterSchema(filterConfig), - "order_by": helper.OrderBySchema(filterConfig), - "order": helper.OrderSchema(), + "filter": filterConfig.FilterSchema(), + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), "instances": { Type: schema.TypeList, Description: "The returned list of Instances.", @@ -53,12 +53,12 @@ func DataSource() *schema.Resource { func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*helper.ProviderMeta).Client - filterID, err := helper.GetFilterID(d) + filterID, err := filterConfig.GetFilterID(d) if err != nil { return diag.Errorf("failed to generate filter id: %s", err) } - filter, err := helper.ConstructFilterString(d, filterConfig) + filter, err := filterConfig.ConstructFilterString(d) if err != nil { return diag.Errorf("failed to construct filter: %s", err) } @@ -85,7 +85,7 @@ func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{ flattenedInstances[i] = instanceMap } - instancesFiltered, err := helper.FilterResults(d, filterConfig, flattenedInstances) + instancesFiltered, err := filterConfig.FilterResults(d, flattenedInstances) if err != nil { return diag.Errorf("failed to filter returned instances: %s", err) } diff --git a/linode/instance/datasource_test.go b/linode/instance/datasource_test.go index f3cb4e663..86dec0437 100644 --- a/linode/instance/datasource_test.go +++ b/linode/instance/datasource_test.go @@ -43,8 +43,11 @@ func TestAccDataSourceInstances_basic(t *testing.T) { func TestAccDataSourceInstances_multipleInstances(t *testing.T) { resName := "data.linode_instances.foobar" + resNameDesc := "data.linode_instances.desc" + resNameAsc := "data.linode_instances.asc" + instanceName := acctest.RandomWithPrefix("tf_test") - groupName := acctest.RandomWithPrefix("tf_test") + tagName := acctest.RandomWithPrefix("tf_test") resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, @@ -52,55 +55,32 @@ func TestAccDataSourceInstances_multipleInstances(t *testing.T) { CheckDestroy: acceptance.CheckInstanceDestroy, Steps: []resource.TestStep{ { - Config: tmpl.DataMultiple(t, instanceName, groupName), + Config: tmpl.DataMultiple(t, instanceName, tagName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resName, "instances.#", "3"), ), }, - }, - }) -} - -func TestAccDataSourceInstances_order(t *testing.T) { - resNameDesc := "data.linode_instances.desc" - resNameAsc := "data.linode_instances.asc" - - instanceName := acctest.RandomWithPrefix("tf_test") - groupName := acctest.RandomWithPrefix("tf_test") - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.TestAccProviders, - CheckDestroy: acceptance.CheckInstanceDestroy, - Steps: []resource.TestStep{ { - Config: tmpl.DataMultipleOrder(t, instanceName, groupName), + Config: tmpl.DataMultipleOrder(t, instanceName, tagName), Check: resource.ComposeTestCheckFunc( // Ensure order is correctly appended to filter resource.TestCheckResourceAttr(resNameDesc, "instances.#", "3"), resource.TestCheckResourceAttr(resNameAsc, "instances.#", "3"), ), }, - }, - }) -} - -func TestAccDataSourceInstances_regex(t *testing.T) { - resName := "data.linode_instances.foobar" - instanceName := acctest.RandomWithPrefix("tf_test") - groupName := acctest.RandomWithPrefix("tf_test") - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.TestAccProviders, - CheckDestroy: acceptance.CheckInstanceDestroy, - Steps: []resource.TestStep{ { - Config: tmpl.DataMultipleRegex(t, instanceName, groupName), + Config: tmpl.DataMultipleRegex(t, instanceName, tagName), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resName, "instances.#", "3"), ), }, + { + Config: tmpl.DataClientFilter(t, instanceName, tagName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resName, "instances.#", "1"), + resource.TestCheckResourceAttr(resName, "instances.0.status", "running"), + ), + }, }, }) } diff --git a/linode/instance/tmpl/template.go b/linode/instance/tmpl/template.go index e7ffc7485..098428ef4 100644 --- a/linode/instance/tmpl/template.go +++ b/linode/instance/tmpl/template.go @@ -321,26 +321,34 @@ func DataBasic(t *testing.T, label string) string { }) } -func DataMultiple(t *testing.T, label, group string) string { +func DataMultiple(t *testing.T, label, tag string) string { return acceptance.ExecuteTemplate(t, "instance_data_multiple", TemplateData{ Label: label, - Group: group, + Tag: tag, }) } -func DataMultipleOrder(t *testing.T, label, group string) string { +func DataMultipleOrder(t *testing.T, label, tag string) string { return acceptance.ExecuteTemplate(t, "instance_data_multiple_order", TemplateData{ Label: label, - Group: group, + Tag: tag, }) } -func DataMultipleRegex(t *testing.T, label, group string) string { +func DataMultipleRegex(t *testing.T, label, tag string) string { return acceptance.ExecuteTemplate(t, "instance_data_multiple_regex", TemplateData{ Label: label, - Group: group, + Tag: tag, + }) +} + +func DataClientFilter(t *testing.T, label, tag string) string { + return acceptance.ExecuteTemplate(t, + "instance_data_clientfilter", TemplateData{ + Label: label, + Tag: tag, }) } diff --git a/linode/instance/tmpl/templates/data_clientfilter.gotf b/linode/instance/tmpl/templates/data_clientfilter.gotf new file mode 100644 index 000000000..8f2b5de5c --- /dev/null +++ b/linode/instance/tmpl/templates/data_clientfilter.gotf @@ -0,0 +1,30 @@ +{{ define "instance_data_clientfilter" }} + +resource "linode_instance" "foobar" { + count = 3 + + label = "{{.Label}}-${count.index}" + tags = ["{{ .Tag }}-${count.index}"] + type = "g6-nanode-1" + image = "linode/ubuntu18.04" + region = "us-east" + root_pass = "terraform-test" +} + +data "linode_instances" "foobar" { + depends_on = [ + linode_instance.foobar, + ] + + filter { + name = "tags" + values = linode_instance.foobar[0].tags + } + + filter { + name = "status" + values = ["running"] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/instance/tmpl/templates/data_multiple.gotf b/linode/instance/tmpl/templates/data_multiple.gotf index edd392d1b..704aeefc7 100644 --- a/linode/instance/tmpl/templates/data_multiple.gotf +++ b/linode/instance/tmpl/templates/data_multiple.gotf @@ -1,45 +1,15 @@ {{ define "instance_data_multiple" }} -resource "linode_instance" "foobar-0" { - label = "{{.Label}}-0" - group = "{{.Group}}" - tags = ["cool", "cooler"] - type = "g6-nanode-1" - image = "linode/ubuntu18.04" - region = "us-east" - root_pass = "terraform-test" -} - -resource "linode_instance" "foobar-1" { - label = "{{.Label}}-1" - group = "{{.Group}}" - tags = ["cool", "cooler"] - type = "g6-nanode-1" - image = "linode/ubuntu18.04" - region = "us-east" - root_pass = "terraform-test" -} - -resource "linode_instance" "foobar-2" { - label = "{{.Label}}-2" - group = "{{.Group}}" - tags = ["cool", "cooler"] - type = "g6-nanode-1" - image = "linode/ubuntu18.04" - region = "us-east" - root_pass = "terraform-test" -} +{{ template "instance_data_multiple_base" . }} data "linode_instances" "foobar" { depends_on = [ - linode_instance.foobar-0, - linode_instance.foobar-1, - linode_instance.foobar-2 + linode_instance.foobar, ] filter { - name = "group" - values = [linode_instance.foobar-0.group] + name = "tags" + values = ["{{ .Tag }}"] } } diff --git a/linode/instance/tmpl/templates/data_multiple_base.gotf b/linode/instance/tmpl/templates/data_multiple_base.gotf new file mode 100644 index 000000000..148ebca69 --- /dev/null +++ b/linode/instance/tmpl/templates/data_multiple_base.gotf @@ -0,0 +1,14 @@ +{{ define "instance_data_multiple_base" }} + +resource "linode_instance" "foobar" { + count = 3 + + label = "{{.Label}}-${count.index}" + tags = ["{{ .Tag }}"] + type = "g6-nanode-1" + image = "linode/ubuntu18.04" + region = "us-east" + root_pass = "terraform-test" +} + +{{ end }} \ No newline at end of file diff --git a/linode/instance/tmpl/templates/data_multiple_order.gotf b/linode/instance/tmpl/templates/data_multiple_order.gotf new file mode 100644 index 000000000..e64fea788 --- /dev/null +++ b/linode/instance/tmpl/templates/data_multiple_order.gotf @@ -0,0 +1,32 @@ +{{ define "instance_data_multiple_order" }} + +{{ template "instance_data_multiple_base" . }} + +data "linode_instances" "asc" { + depends_on = [ + linode_instance.foobar + ] + + order_by = "id" + + filter { + name = "tags" + values = ["{{ .Tag }}"] + } +} + +data "linode_instances" "desc" { + depends_on = [ + linode_instance.foobar + ] + + order_by = "id" + order = "desc" + + filter { + name = "tags" + values = ["{{ .Tag }}"] + } +} + +{{ end }} \ No newline at end of file diff --git a/linode/instance/tmpl/templates/data_multiple_regex.gotf b/linode/instance/tmpl/templates/data_multiple_regex.gotf index bbb2589f3..95d57b0c5 100644 --- a/linode/instance/tmpl/templates/data_multiple_regex.gotf +++ b/linode/instance/tmpl/templates/data_multiple_regex.gotf @@ -1,16 +1,6 @@ {{ define "instance_data_multiple_regex" }} -resource "linode_instance" "foobar" { - count = 3 - - label = "{{.Label}}-${count.index}" - group = "{{.Group}}" - tags = ["cool", "cooler"] - type = "g6-nanode-1" - image = "linode/ubuntu18.04" - region = "us-east" - root_pass = "terraform-test" -} +{{ template "instance_data_multiple_base" . }} data "linode_instances" "foobar" { depends_on = [ diff --git a/linode/instance/tmpl/templates/disk_multiple_order.gotf b/linode/instance/tmpl/templates/disk_multiple_order.gotf deleted file mode 100644 index 12bdb236d..000000000 --- a/linode/instance/tmpl/templates/disk_multiple_order.gotf +++ /dev/null @@ -1,74 +0,0 @@ -{{ define "instance_data_multiple_order" }} - -resource "linode_instance" "foobar-0" { - label = "{{.Label}}-0" - group = "{{.Group}}" - tags = ["cool", "cooler"] - type = "g6-nanode-1" - image = "linode/ubuntu18.04" - region = "us-east" - root_pass = "terraform-test" -} - -resource "linode_instance" "foobar-1" { - label = "{{.Label}}-1" - group = "{{.Group}}" - tags = ["cool", "cooler"] - type = "g6-nanode-1" - image = "linode/ubuntu18.04" - region = "us-east" - root_pass = "terraform-test" -} - -resource "linode_instance" "foobar-2" { - label = "{{.Label}}-2" - group = "{{.Group}}" - tags = ["cool", "cooler"] - type = "g6-nanode-1" - image = "linode/ubuntu18.04" - region = "us-east" - root_pass = "terraform-test" -} - -data "linode_instances" "asc" { - depends_on = [ - linode_instance.foobar-0, - linode_instance.foobar-1, - linode_instance.foobar-2 - ] - - order_by = "id" - - filter { - name = "group" - values = [linode_instance.foobar-0.group] - } - - filter { - name = "tags" - values = ["cool", "cooler"] - } -} - -data "linode_instances" "desc" { - depends_on = [ - linode_instance.foobar-0, - linode_instance.foobar-1, - linode_instance.foobar-2 - ] - - order_by = "id" - order = "desc" - - filter { - name = "group" - values = [linode_instance.foobar-0.group] - } - - filter { - name = "tags" - values = ["cool", "cooler"] - } -} - -{{ end }} \ No newline at end of file diff --git a/linode/instancetypes/datasource.go b/linode/instancetypes/datasource.go index 7f447995c..e1660396d 100644 --- a/linode/instancetypes/datasource.go +++ b/linode/instancetypes/datasource.go @@ -1,12 +1,10 @@ package instancetypes import ( + "context" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/linode/linodego" - "github.com/linode/terraform-provider-linode/linode/helper" - - "context" ) func DataSource() *schema.Resource { @@ -17,7 +15,7 @@ func DataSource() *schema.Resource { } func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - results, err := helper.FilterResource(ctx, d, meta, filterConfig, listTypes, flattenType) + results, err := filterConfig.FilterDataSource(ctx, d, meta, listTypes, flattenType) if err != nil { return nil } diff --git a/linode/instancetypes/schema_datasource.go b/linode/instancetypes/schema_datasource.go index c187a2ba7..8203ef79b 100644 --- a/linode/instancetypes/schema_datasource.go +++ b/linode/instancetypes/schema_datasource.go @@ -6,7 +6,7 @@ import ( "github.com/linode/terraform-provider-linode/linode/instancetype" ) -var filterConfig = map[string]helper.FilterAttribute{ +var filterConfig = helper.FilterConfig{ "class": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, "disk": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, "gpus": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, @@ -18,9 +18,9 @@ var filterConfig = map[string]helper.FilterAttribute{ } var dataSourceSchema = map[string]*schema.Schema{ - "order_by": helper.OrderBySchema(filterConfig), - "order": helper.OrderSchema(), - "filter": helper.FilterSchema(filterConfig), + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), + "filter": filterConfig.FilterSchema(), "types": { Type: schema.TypeList, Description: "The returned list of Types.", diff --git a/linode/stackscripts/datasource.go b/linode/stackscripts/datasource.go index f42aca46b..ef788e899 100644 --- a/linode/stackscripts/datasource.go +++ b/linode/stackscripts/datasource.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/linode/linodego" - "github.com/linode/terraform-provider-linode/linode/helper" "github.com/linode/terraform-provider-linode/linode/stackscript" ) @@ -19,12 +18,12 @@ func DataSource() *schema.Resource { } func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - results, err := helper.FilterResource(ctx, d, meta, filterConfig, listStackScripts, flattenStackScript) + results, err := filterConfig.FilterDataSource(ctx, d, meta, listStackScripts, flattenStackScript) if err != nil { return nil } - results = helper.FilterLatest(d, results) + results = filterConfig.FilterLatest(d, results) d.Set("stackscripts", results) diff --git a/linode/stackscripts/schema_datasource.go b/linode/stackscripts/schema_datasource.go index bd2fcf951..bbd09e993 100644 --- a/linode/stackscripts/schema_datasource.go +++ b/linode/stackscripts/schema_datasource.go @@ -5,7 +5,7 @@ import ( "github.com/linode/terraform-provider-linode/linode/helper" ) -var filterConfig = map[string]helper.FilterAttribute{ +var filterConfig = helper.FilterConfig{ "deployments_total": {APIFilterable: true, TypeFunc: helper.FilterTypeInt}, "description": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, "is_public": {APIFilterable: true, TypeFunc: helper.FilterTypeBool}, @@ -25,9 +25,9 @@ var dataSourceSchema = map[string]*schema.Schema{ Optional: true, Default: false, }, - "order_by": helper.OrderBySchema(filterConfig), - "order": helper.OrderSchema(), - "filter": helper.FilterSchema(filterConfig), + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), + "filter": filterConfig.FilterSchema(), "stackscripts": { Type: schema.TypeList, Description: "The returned list of StackScripts.", diff --git a/linode/vlan/datasource.go b/linode/vlan/datasource.go index 5bdb1f566..e203ff42c 100644 --- a/linode/vlan/datasource.go +++ b/linode/vlan/datasource.go @@ -1,12 +1,10 @@ package vlan import ( + "context" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/linode/linodego" - "github.com/linode/terraform-provider-linode/linode/helper" - - "context" "time" ) @@ -20,9 +18,9 @@ func DataSource() *schema.Resource { return &schema.Resource{ ReadContext: readDataSource, Schema: map[string]*schema.Schema{ - "order_by": helper.OrderBySchema(filterConfig), - "order": helper.OrderSchema(), - "filter": helper.FilterSchema(filterConfig), + "order_by": filterConfig.OrderBySchema(), + "order": filterConfig.OrderSchema(), + "filter": filterConfig.FilterSchema(), "vlans": { Type: schema.TypeList, Description: "The returned list of VLANs.", @@ -34,7 +32,7 @@ func DataSource() *schema.Resource { } func readDataSource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - results, err := helper.FilterResource(ctx, d, meta, filterConfig, listVLANs, flattenVLAN) + results, err := filterConfig.FilterDataSource(ctx, d, meta, listVLANs, flattenVLAN) if err != nil { return nil } diff --git a/linode/vlan/schema_datasource.go b/linode/vlan/schema_datasource.go index 6f4f7a36b..6617b5da3 100644 --- a/linode/vlan/schema_datasource.go +++ b/linode/vlan/schema_datasource.go @@ -5,7 +5,7 @@ import ( "github.com/linode/terraform-provider-linode/linode/helper" ) -var filterConfig = map[string]helper.FilterAttribute{ +var filterConfig = helper.FilterConfig{ "label": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, "region": {APIFilterable: true, TypeFunc: helper.FilterTypeString}, } From 8b957151ebd7f464a7521870be819ccd3c47ecb0 Mon Sep 17 00:00:00 2001 From: LBGarber Date: Fri, 21 Jan 2022 11:13:26 -0500 Subject: [PATCH 3/4] Fix goimports --- linode/instancetypes/datasource.go | 1 + linode/vlan/datasource.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/linode/instancetypes/datasource.go b/linode/instancetypes/datasource.go index e1660396d..64ebdc01a 100644 --- a/linode/instancetypes/datasource.go +++ b/linode/instancetypes/datasource.go @@ -2,6 +2,7 @@ package instancetypes import ( "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/linode/linodego" diff --git a/linode/vlan/datasource.go b/linode/vlan/datasource.go index e203ff42c..b6a238dc0 100644 --- a/linode/vlan/datasource.go +++ b/linode/vlan/datasource.go @@ -2,10 +2,11 @@ package vlan import ( "context" + "time" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/linode/linodego" - "time" ) func dataSourceVLAN() *schema.Resource { From 4f52ad8b0cc1e9c6ccbca9a219bc5a8d88003265 Mon Sep 17 00:00:00 2001 From: LBGarber Date: Fri, 21 Jan 2022 11:15:32 -0500 Subject: [PATCH 4/4] make fmt --- linode/instancetypes/datasource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linode/instancetypes/datasource.go b/linode/instancetypes/datasource.go index 64ebdc01a..2eb35247f 100644 --- a/linode/instancetypes/datasource.go +++ b/linode/instancetypes/datasource.go @@ -2,7 +2,7 @@ package instancetypes import ( "context" - + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/linode/linodego"