From 702e125a244683eba7d58fec0b42c3d016a58478 Mon Sep 17 00:00:00 2001 From: Piotr Truszkowski Date: Tue, 5 Mar 2024 11:44:38 +0100 Subject: [PATCH] added resources for saved filter handling (#521) * added resources for saved filter handling --------- Co-authored-by: Plushnikov, Michail --- docs/data-sources/saved_filter.md | 39 +++ docs/data-sources/saved_filters.md | 57 ++++ docs/resources/saved_filter.md | 93 +++++++ .../spacelift_saved_filter/data-source.tf | 7 + .../spacelift_saved_filters/data-source.tf | 16 ++ .../spacelift_saved_filter/import.sh | 1 + .../spacelift_saved_filter/resource.tf | 54 ++++ spacelift/data_saved_filter.go | 90 ++++++ spacelift/data_saved_filter_test.go | 53 ++++ spacelift/data_saved_filters.go | 115 ++++++++ spacelift/data_saved_filters_test.go | 256 ++++++++++++++++++ spacelift/internal/structs/saved_filter.go | 31 +++ spacelift/provider.go | 3 + spacelift/resource_saved_filter.go | 164 +++++++++++ spacelift/resource_saved_filter_test.go | 78 ++++++ 15 files changed, 1057 insertions(+) create mode 100644 docs/data-sources/saved_filter.md create mode 100644 docs/data-sources/saved_filters.md create mode 100644 docs/resources/saved_filter.md create mode 100644 examples/data-sources/spacelift_saved_filter/data-source.tf create mode 100644 examples/data-sources/spacelift_saved_filters/data-source.tf create mode 100644 examples/resources/spacelift_saved_filter/import.sh create mode 100644 examples/resources/spacelift_saved_filter/resource.tf create mode 100644 spacelift/data_saved_filter.go create mode 100644 spacelift/data_saved_filter_test.go create mode 100644 spacelift/data_saved_filters.go create mode 100644 spacelift/data_saved_filters_test.go create mode 100644 spacelift/internal/structs/saved_filter.go create mode 100644 spacelift/resource_saved_filter.go create mode 100644 spacelift/resource_saved_filter_test.go diff --git a/docs/data-sources/saved_filter.md b/docs/data-sources/saved_filter.md new file mode 100644 index 00000000..0a13fa4d --- /dev/null +++ b/docs/data-sources/saved_filter.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spacelift_saved_filter Data Source - terraform-provider-spacelift" +subcategory: "" +description: |- + spacelift_saved_filter represents a Spacelift saved filter. +--- + +# spacelift_saved_filter (Data Source) + +`spacelift_saved_filter` represents a Spacelift saved filter. + +## Example Usage + +```terraform +data "spacelift_saved_filter" "filter" { + filter_id = spacelift_saved_filter.filter.id +} + +output "filter_data" { + value = data.spacelift_saved_filter.filter.data +} +``` + + +## Schema + +### Required + +- `filter_id` (String) immutable ID (slug) of the filter + +### Read-Only + +- `created_by` (String) Login of the user who created the saved filter +- `data` (String) Data is the JSON representation of the filter data +- `id` (String) Globally unique ID of the saved filter +- `is_public` (Boolean) Toggle whether the filter is public or not +- `name` (String) Name of the filter +- `type` (String) Type describes the type of the filter. It is used to determine which view the filter is for diff --git a/docs/data-sources/saved_filters.md b/docs/data-sources/saved_filters.md new file mode 100644 index 00000000..d32e0421 --- /dev/null +++ b/docs/data-sources/saved_filters.md @@ -0,0 +1,57 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spacelift_saved_filters Data Source - terraform-provider-spacelift" +subcategory: "" +description: |- + spacelift_saved_filters can find all saved filters that have certain type or name +--- + +# spacelift_saved_filters (Data Source) + +`spacelift_saved_filters` can find all saved filters that have certain type or name + +## Example Usage + +```terraform +# For all saved filter +data "spacelift_saved_filters" "all" {} + +# Filters with a matching type +data "spacelift_saved_filters" "stack_filters" { + type = "stacks" +} + +# Filters with a matching name +data "spacelift_saved_filters" "my_filters" { + name = "My best filter" +} + +output "filter_ids" { + value = data.spacelift_saved_filters.stack_filters.filters[*].id +} +``` + + +## Schema + +### Optional + +- `filter_name` (String) filter name to look for +- `filter_type` (String) filter type to look for + +### Read-Only + +- `filters` (List of Object) (see [below for nested schema](#nestedatt--filters)) +- `id` (String) The ID of this resource. + + +### Nested Schema for `filters` + +Read-Only: + +- `created_by` (String) +- `data` (String) +- `id` (String) +- `is_public` (Boolean) +- `name` (String) +- `type` (String) diff --git a/docs/resources/saved_filter.md b/docs/resources/saved_filter.md new file mode 100644 index 00000000..c5ec39fe --- /dev/null +++ b/docs/resources/saved_filter.md @@ -0,0 +1,93 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spacelift_saved_filter Resource - terraform-provider-spacelift" +subcategory: "" +description: |- + spacelift_saved_filter represents a Spacelift filter - a collection of customer-defined criteria that are applied by Spacelift at one of the decision points within the application. +--- + +# spacelift_saved_filter (Resource) + +`spacelift_saved_filter` represents a Spacelift **filter** - a collection of customer-defined criteria that are applied by Spacelift at one of the decision points within the application. + +## Example Usage + +```terraform +resource "spacelift_saved_filter" "my_filter" { + type = "webhooks" + name = "filter for all xyz teams" + is_public = true + data = jsonencode({ + "key" : "activeFilters", + "value" : jsonencode({ + "filters" : [ + [ + "name", + { + "key" : "name", + "filterName" : "name", + "type" : "STRING", + "values" : [ + "team_xyz_*" + ] + } + ] + ], + "sort" : { + "direction" : "ASC", + "option" : "space" + }, + "text" : null, + "order" : [ + { + "name" : "enabled", + "visible" : true + }, + { + "name" : "endpoint", + "visible" : true + }, + { + "name" : "slug", + "visible" : true + }, + { + "name" : "label", + "visible" : true + }, + { + "name" : "name", + "visible" : true + }, + { + "name" : "space", + "visible" : true + } + ] + }) + }) +} +``` + + +## Schema + +### Required + +- `data` (String) Data is the JSON representation of the filter data +- `is_public` (Boolean) Toggle whether the filter is public or not +- `name` (String) Name of the saved filter +- `type` (String) Type describes the type of the filter. It is used to determine which view the filter is for. Possible values are `stacks`, `blueprints`, `contexts`, `webhooks`. + +### Read-Only + +- `created_by` (String) Login of the user who created the saved filter +- `id` (String) Globally unique ID of the saved filter + +## Import + +Import is supported using the following syntax: + +```shell +terraform import spacelift_saved_filter.my_filter $FILTER_ID +``` diff --git a/examples/data-sources/spacelift_saved_filter/data-source.tf b/examples/data-sources/spacelift_saved_filter/data-source.tf new file mode 100644 index 00000000..f1cf0e58 --- /dev/null +++ b/examples/data-sources/spacelift_saved_filter/data-source.tf @@ -0,0 +1,7 @@ +data "spacelift_saved_filter" "filter" { + filter_id = spacelift_saved_filter.filter.id +} + +output "filter_data" { + value = data.spacelift_saved_filter.filter.data +} diff --git a/examples/data-sources/spacelift_saved_filters/data-source.tf b/examples/data-sources/spacelift_saved_filters/data-source.tf new file mode 100644 index 00000000..cc6ee011 --- /dev/null +++ b/examples/data-sources/spacelift_saved_filters/data-source.tf @@ -0,0 +1,16 @@ +# For all saved filter +data "spacelift_saved_filters" "all" {} + +# Filters with a matching type +data "spacelift_saved_filters" "stack_filters" { + type = "stacks" +} + +# Filters with a matching name +data "spacelift_saved_filters" "my_filters" { + name = "My best filter" +} + +output "filter_ids" { + value = data.spacelift_saved_filters.stack_filters.filters[*].id +} diff --git a/examples/resources/spacelift_saved_filter/import.sh b/examples/resources/spacelift_saved_filter/import.sh new file mode 100644 index 00000000..577d102e --- /dev/null +++ b/examples/resources/spacelift_saved_filter/import.sh @@ -0,0 +1 @@ +terraform import spacelift_saved_filter.my_filter $FILTER_ID diff --git a/examples/resources/spacelift_saved_filter/resource.tf b/examples/resources/spacelift_saved_filter/resource.tf new file mode 100644 index 00000000..31a9771e --- /dev/null +++ b/examples/resources/spacelift_saved_filter/resource.tf @@ -0,0 +1,54 @@ +resource "spacelift_saved_filter" "my_filter" { + type = "webhooks" + name = "filter for all xyz teams" + is_public = true + data = jsonencode({ + "key" : "activeFilters", + "value" : jsonencode({ + "filters" : [ + [ + "name", + { + "key" : "name", + "filterName" : "name", + "type" : "STRING", + "values" : [ + "team_xyz_*" + ] + } + ] + ], + "sort" : { + "direction" : "ASC", + "option" : "space" + }, + "text" : null, + "order" : [ + { + "name" : "enabled", + "visible" : true + }, + { + "name" : "endpoint", + "visible" : true + }, + { + "name" : "slug", + "visible" : true + }, + { + "name" : "label", + "visible" : true + }, + { + "name" : "name", + "visible" : true + }, + { + "name" : "space", + "visible" : true + } + ] + }) + }) +} \ No newline at end of file diff --git a/spacelift/data_saved_filter.go b/spacelift/data_saved_filter.go new file mode 100644 index 00000000..4f30548d --- /dev/null +++ b/spacelift/data_saved_filter.go @@ -0,0 +1,90 @@ +package spacelift + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal" +) + +func dataSavedFilter() *schema.Resource { + return &schema.Resource{ + Description: "" + + "`spacelift_saved_filter` represents a Spacelift saved filter.", + + ReadContext: dataFilterRead, + + Schema: map[string]*schema.Schema{ + "filter_id": { + Type: schema.TypeString, + Description: " immutable ID (slug) of the filter", + Required: true, + }, + "id": { + Type: schema.TypeString, + Description: "Globally unique ID of the saved filter", + Computed: true, + }, + "is_public": { + Type: schema.TypeBool, + Description: "Toggle whether the filter is public or not", + Computed: true, + }, + "created_by": { + Type: schema.TypeString, + Description: "Login of the user who created the saved filter", + Computed: true, + }, + "name": { + Type: schema.TypeString, + Description: "Name of the filter", + Computed: true, + }, + "type": { + Type: schema.TypeString, + Description: "Type describes the type of the filter. It is used to determine which view the filter is for", + Computed: true, + }, + "data": { + Type: schema.TypeString, + Description: "Data is the JSON representation of the filter data", + Computed: true, + }, + }, + } +} + +func dataFilterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var query struct { + Filter *struct { + ID string `graphql:"id"` + IsPublic bool `graphql:"isPublic"` + CreatedBy string `graphql:"createdBy"` + Name string `graphql:"name"` + Type string `graphql:"type"` + Data string `graphql:"data"` + } `graphql:"savedFilter(id: $id)"` + } + + id := d.Get("filter_id") + variables := map[string]interface{}{"id": id} + + if err := meta.(*internal.Client).Query(ctx, "savedFilter", &query, variables); err != nil { + return diag.Errorf("could not query for filter: %v", err) + } + + if query.Filter == nil { + return diag.Errorf("could not find filter %s", id) + } + + d.SetId(query.Filter.ID) + d.Set("is_public", query.Filter.IsPublic) + d.Set("name", query.Filter.Name) + d.Set("type", query.Filter.Type) + d.Set("created_by", query.Filter.CreatedBy) + d.Set("data", query.Filter.Data) + + return nil +} diff --git a/spacelift/data_saved_filter_test.go b/spacelift/data_saved_filter_test.go new file mode 100644 index 00000000..d4822e95 --- /dev/null +++ b/spacelift/data_saved_filter_test.go @@ -0,0 +1,53 @@ +package spacelift + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + . "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/testhelpers" +) + +func TestSavedFilterData(t *testing.T) { + t.Run("creates and updates a filter", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + testSteps(t, []resource.TestStep{{ + Config: fmt.Sprintf(` + resource "spacelift_saved_filter" "test" { + name = "My first filter %s" + type = "stacks" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + data "spacelift_saved_filter" "test" { + filter_id = spacelift_saved_filter.test.id + } + `, randomID), + Check: Resource( + "data.spacelift_saved_filter.test", + Attribute("id", IsNotEmpty()), + Attribute("data", Contains("activeFilters")), + Attribute("type", Equals("stacks")), + Attribute("is_public", Equals("true")), + ), + }}) + }) + + t.Run("filter doesn't exist", func(t *testing.T) { + testSteps(t, []resource.TestStep{{ + Config: ` + data "spacelift_saved_filter" "test" { + filter_id = "non-existent" + } + `, + ExpectError: regexp.MustCompile("could not find filter non-existent"), + }}) + }) +} diff --git a/spacelift/data_saved_filters.go b/spacelift/data_saved_filters.go new file mode 100644 index 00000000..44ae28db --- /dev/null +++ b/spacelift/data_saved_filters.go @@ -0,0 +1,115 @@ +package spacelift + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/shurcooL/graphql" + + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs" +) + +func dataSavedFilters() *schema.Resource { + return &schema.Resource{ + Description: "" + + "`spacelift_saved_filters` can find all saved filters that have certain type or name", + + ReadContext: dataFiltersRead, + + Schema: map[string]*schema.Schema{ + "filter_type": { + Type: schema.TypeString, + Description: "filter type to look for", + Optional: true, + }, + "filter_name": { + Type: schema.TypeString, + Description: "filter name to look for", + Optional: true, + }, + "filters": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Globally unique ID of the saved filter", + Computed: true, + }, + "is_public": { + Type: schema.TypeBool, + Description: "Toggle whether the filter is public or not", + Computed: true, + }, + "created_by": { + Type: schema.TypeString, + Description: "Login of the user who created the saved filter", + Computed: true, + }, + "name": { + Type: schema.TypeString, + Description: "Name of the filter", + Computed: true, + }, + "type": { + Type: schema.TypeString, + Description: "Type describes the type of the filter. It is used to determine which view the filter is for", + Computed: true, + }, + "data": { + Type: schema.TypeString, + Description: "Data is the JSON representation of the filter data", + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataFiltersRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var query struct { + Filters []structs.SavedFilter `graphql:"savedFilters(type: $type)"` + } + + // Filtering by type is supported on the server side, but by name, + // we need to do it here on the client side. + typeRaw, typeSpecified := d.GetOk("filter_type") + requestedType := typeRaw.(string) + nameRaw, nameSpecified := d.GetOk("filter_name") + requestedName := nameRaw.(string) + + variables := map[string]interface{}{"type": (*graphql.String)(nil)} + if typeSpecified { + variables["type"] = toString(requestedType) + } + + if err := meta.(*internal.Client).Query(ctx, "savedFilters", &query, variables); err != nil { + return diag.Errorf("could not query for filters: %v", err) + } + + var filters []interface{} + for _, filter := range query.Filters { + if nameSpecified && filter.Name != requestedName { + continue + } + filters = append(filters, map[string]interface{}{ + "id": filter.ID, + "is_public": filter.IsPublic, + "name": filter.Name, + "type": filter.Type, + "created_by": filter.CreatedBy, + "data": filter.Data, + }) + } + + d.SetId(fmt.Sprintf("filters/%s/%s", requestedType, requestedName)) + d.Set("filters", filters) + + return nil +} diff --git a/spacelift/data_saved_filters_test.go b/spacelift/data_saved_filters_test.go new file mode 100644 index 00000000..30338bf6 --- /dev/null +++ b/spacelift/data_saved_filters_test.go @@ -0,0 +1,256 @@ +package spacelift + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + . "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/testhelpers" +) + +// Note: Tests could interfere with each other if run in parallel. +// It's not a problem if we use different types (stacks, contexts, ...). +// But if we use the same type, tests could fail (for example listing saved filters - ). + +func TestSavedFiltersData(t *testing.T) { + t.Run("load all saved filters", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + datasourceName := "data.spacelift_saved_filters.all" + + testSteps(t, []resource.TestStep{{ + Config: ` + resource "spacelift_saved_filter" "test" { + name = "test-` + randomID + `" + type = "contexts" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + data "spacelift_saved_filters" "all" { + depends_on = [spacelift_saved_filter.test] + } + `, + Check: resource.ComposeTestCheckFunc( + Resource(datasourceName, Attribute("id", IsNotEmpty())), + ), + }}) + }) + + t.Run("type specified", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + datasourceName := "data.spacelift_saved_filters.stacks" + + testSteps(t, []resource.TestStep{{ + Config: ` + resource "spacelift_saved_filter" "test1" { + name = "a-` + randomID + `" + type = "stacks" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + resource "spacelift_saved_filter" "test2" { + name = "b-` + randomID + `" + type = "stacks" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + resource "spacelift_saved_filter" "test3" { + name = "c-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + data "spacelift_saved_filters" "stacks" { + filter_type = "stacks" + depends_on = [spacelift_saved_filter.test1,spacelift_saved_filter.test2,spacelift_saved_filter.test3] + } + `, + Check: resource.ComposeTestCheckFunc( + Resource(datasourceName, + Attribute("id", IsNotEmpty()), + Attribute("filters.#", Equals("2")), + Attribute("filters.0.id", IsNotEmpty()), + Attribute("filters.0.type", Equals("stacks")), + Attribute("filters.0.name", Equals("a-"+randomID)), + Attribute("filters.1.id", IsNotEmpty()), + Attribute("filters.1.type", Equals("stacks")), + Attribute("filters.1.name", Equals("b-"+randomID)), + ), + ), + }}) + }) + + t.Run("name specified", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + datasourceName := "data.spacelift_saved_filters.blueprints" + + testSteps(t, []resource.TestStep{{ + Config: ` + resource "spacelift_saved_filter" "test1" { + name = "d-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + resource "spacelift_saved_filter" "test2" { + name = "e-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + resource "spacelift_saved_filter" "test3" { + name = "f-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + data "spacelift_saved_filters" "blueprints" { + filter_name = "d-` + randomID + `" + depends_on = [spacelift_saved_filter.test1,spacelift_saved_filter.test2,spacelift_saved_filter.test3] + } + `, + Check: resource.ComposeTestCheckFunc( + Resource(datasourceName, + Attribute("id", IsNotEmpty()), + Attribute("filters.#", Equals("1")), + Attribute("filters.0.id", IsNotEmpty()), + Attribute("filters.0.type", Equals("blueprints")), + Attribute("filters.0.name", Equals("d-"+randomID)), + ), + ), + }}) + }) + + t.Run("type & name specified", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + datasourceName := "data.spacelift_saved_filters.blueprints" + + testSteps(t, []resource.TestStep{{ + Config: ` + resource "spacelift_saved_filter" "test1" { + name = "g-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + resource "spacelift_saved_filter" "test2" { + name = "h-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + resource "spacelift_saved_filter" "test3" { + name = "i-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + data "spacelift_saved_filters" "blueprints" { + filter_type = "blueprints" + filter_name = "h-` + randomID + `" + depends_on = [spacelift_saved_filter.test1,spacelift_saved_filter.test2,spacelift_saved_filter.test3] + } + `, + Check: resource.ComposeTestCheckFunc( + Resource(datasourceName, + Attribute("id", IsNotEmpty()), + Attribute("filters.#", Equals("1")), + Attribute("filters.0.id", IsNotEmpty()), + Attribute("filters.0.type", Equals("blueprints")), + Attribute("filters.0.name", Equals("h-"+randomID)), + ), + ), + }}) + }) + + t.Run("no results", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + datasourceName := "data.spacelift_saved_filters.blueprints" + + testSteps(t, []resource.TestStep{{ + Config: ` + resource "spacelift_saved_filter" "test1" { + name = "j-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + resource "spacelift_saved_filter" "test2" { + name = "k-` + randomID + `" + type = "blueprints" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + resource "spacelift_saved_filter" "test3" { + name = "l-` + randomID + `" + type = "contexts" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + + data "spacelift_saved_filters" "blueprints" { + filter_type = "blueprints" + filter_name = "l-` + randomID + `" + depends_on = [spacelift_saved_filter.test1,spacelift_saved_filter.test2,spacelift_saved_filter.test3] + } + `, + Check: resource.ComposeTestCheckFunc( + Resource(datasourceName, + Attribute("id", IsNotEmpty()), + Attribute("filters.#", Equals("0")), + ), + ), + }}) + }) +} diff --git a/spacelift/internal/structs/saved_filter.go b/spacelift/internal/structs/saved_filter.go new file mode 100644 index 00000000..ed34f9a8 --- /dev/null +++ b/spacelift/internal/structs/saved_filter.go @@ -0,0 +1,31 @@ +package structs + +import "github.com/shurcooL/graphql" + +// SavedFilterType represents a saved filter type. +type SavedFilterType string + +// SavedFilter represents SavedFilter data relevant to the provider. +type SavedFilter struct { + ID string `graphql:"id"` + Name string `graphql:"name"` + Data string `graphql:"data"` + Type string `graphql:"type"` + IsPublic bool `graphql:"isPublic"` + CreatedBy string `graphql:"createdBy"` +} + +// SavedFilterInput represents the input required to create a saved filter. +type SavedFilterInput struct { + Name graphql.String `json:"name"` + Data graphql.String `json:"data"` + Type graphql.String `json:"type"` + IsPublic graphql.Boolean `json:"isPublic"` +} + +var SavedFilterTypes = []string{ + "stacks", + "blueprints", + "contexts", + "webhooks", +} diff --git a/spacelift/provider.go b/spacelift/provider.go index 984ce79e..8ae4bd27 100644 --- a/spacelift/provider.go +++ b/spacelift/provider.go @@ -71,6 +71,8 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_current_space": dataCurrentSpace(), "spacelift_current_stack": dataCurrentStack(), "spacelift_drift_detection": dataDriftDetection(), + "spacelift_saved_filter": dataSavedFilter(), + "spacelift_saved_filters": dataSavedFilters(), "spacelift_environment_variable": dataEnvironmentVariable(), "spacelift_gcp_service_account": dataGCPServiceAccount(), "spacelift_github_enterprise_integration": dataGithubEnterpriseIntegration(), @@ -127,6 +129,7 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_stack_destructor": resourceStackDestructor(), "spacelift_stack_aws_role": resourceStackAWSRole(), // deprecated "spacelift_stack_gcp_service_account": resourceStackGCPServiceAccount(), // deprecated + "spacelift_saved_filter": resourceSavedFilter(), "spacelift_terraform_provider": resourceTerraformProvider(), "spacelift_user": resourceUser(), "spacelift_vcs_agent_pool": resourceVCSAgentPool(), diff --git a/spacelift/resource_saved_filter.go b/spacelift/resource_saved_filter.go new file mode 100644 index 00000000..69acb946 --- /dev/null +++ b/spacelift/resource_saved_filter.go @@ -0,0 +1,164 @@ +package spacelift + +import ( + "context" + + "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/spacelift-io/terraform-provider-spacelift/spacelift/internal" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations" +) + +func resourceSavedFilter() *schema.Resource { + return &schema.Resource{ + Description: "" + + "`spacelift_saved_filter` represents a Spacelift **filter** - a collection of " + + "customer-defined criteria that are applied by Spacelift at one of the " + + "decision points within the application.", + + CreateContext: resourceSavedFilterCreate, + ReadContext: resourceSavedFilterRead, + UpdateContext: resourceSavedFilterUpdate, + DeleteContext: resourceSavedFilterDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "id": { + Description: "Globally unique ID of the saved filter", + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Description: "Name of the saved filter", + Required: true, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + "data": { + Type: schema.TypeString, + Description: "Data is the JSON representation of the filter data", + Required: true, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + "is_public": { + Type: schema.TypeBool, + Description: "Toggle whether the filter is public or not", + Required: true, + }, + "created_by": { + Type: schema.TypeString, + Description: "Login of the user who created the saved filter", + Computed: true, + }, + "type": { + Type: schema.TypeString, + Description: "Type describes the type of the filter. It is used to determine which view the filter is for. " + + "Possible values are `stacks`, `blueprints`, `contexts`, `webhooks`.", + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice( + structs.SavedFilterTypes, + false, // case-sensitive match + ), + }, + }, + } +} + +func resourceSavedFilterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + CreateFilter structs.SavedFilter `graphql:"savedFilterCreate(input: $input)"` + } + + variables := map[string]interface{}{ + "input": savedFilterInput(d), + } + + if err := meta.(*internal.Client).Mutate(ctx, "savedFilterCreate", &mutation, variables); err != nil { + return diag.Errorf("could not create saved filter %v: %v", toString(d.Get("name")), internal.FromSpaceliftError(err)) + } + + d.SetId(mutation.CreateFilter.ID) + + return resourceSavedFilterRead(ctx, d, meta) +} + +func savedFilterInput(d *schema.ResourceData) structs.SavedFilterInput { + return structs.SavedFilterInput{ + Name: toString(d.Get("name")), + IsPublic: toBool(d.Get("is_public")), + Data: toString(d.Get("data")), + Type: toString(d.Get("type")), + } +} + +func resourceSavedFilterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var query struct { + Filter *structs.SavedFilter `graphql:"savedFilter(id: $id)"` + } + + variables := map[string]interface{}{ + "id": toID(d.Id()), + } + if err := meta.(*internal.Client).Query(ctx, "savedFilter", &query, variables); err != nil { + return diag.Errorf("could not query for saved filter: %v", err) + } + + filter := query.Filter + if filter == nil { + d.SetId("") + return nil + } + + d.Set("name", filter.Name) + d.Set("data", filter.Data) + d.Set("type", filter.Type) + d.Set("is_public", filter.IsPublic) + d.Set("created_by", filter.CreatedBy) + return nil +} + +func resourceSavedFilterUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + UpdateFilter structs.SavedFilter `graphql:"savedFilterUpdate(id: $id, name: $name, data: $data, isPublic: $isPublic)"` + } + + variables := map[string]interface{}{ + "id": toID(d.Id()), + "name": toString(d.Get("name")), + "isPublic": toBool(d.Get("is_public")), + "data": toString(d.Get("data")), + } + + var ret diag.Diagnostics + + if err := meta.(*internal.Client).Mutate(ctx, "savedFilterUpdate", &mutation, variables); err != nil { + ret = diag.Errorf("could not update saved filter: %v", internal.FromSpaceliftError(err)) + } + + return append(ret, resourceSavedFilterRead(ctx, d, meta)...) +} + +func resourceSavedFilterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + DeleteFilter *structs.SavedFilter `graphql:"savedFilterDelete(id: $id)"` + } + + variables := map[string]interface{}{ + "id": toID(d.Id()), + } + + if err := meta.(*internal.Client).Mutate(ctx, "SavedFilterDelete", &mutation, variables); err != nil { + return diag.Errorf("could not delete saved filter: %v", internal.FromSpaceliftError(err)) + } + + d.SetId("") + + return nil +} diff --git a/spacelift/resource_saved_filter_test.go b/spacelift/resource_saved_filter_test.go new file mode 100644 index 00000000..9a3aa386 --- /dev/null +++ b/spacelift/resource_saved_filter_test.go @@ -0,0 +1,78 @@ +package spacelift + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + . "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/testhelpers" +) + +func TestSavedFilterResource(t *testing.T) { + const resourceName = "spacelift_saved_filter.test" + + t.Run("creates and updates a filter", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + config := func(filterType string) string { + return ` + resource "spacelift_saved_filter" "test" { + name = "My first filter ` + randomID + `" + type = "` + filterType + `" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + ` + } + + testSteps(t, []resource.TestStep{ + { + Config: config("stacks"), + Check: Resource( + resourceName, + Attribute("id", IsNotEmpty()), + Attribute("data", Contains("activeFilters")), + Attribute("type", Equals("stacks")), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: config("contexts"), + Check: Resource( + resourceName, + Attribute("type", Equals("contexts")), + ), + }, + }) + }) + + t.Run("unexpected type", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + testSteps(t, []resource.TestStep{ + { + Config: ` + resource "spacelift_saved_filter" "test" { + name = "My first filter ` + randomID + `" + type = "whatever" + is_public = true + data = jsonencode({ + "key": "activeFilters", + "value": jsonencode({}) + }) + } + `, + ExpectError: regexp.MustCompile(`expected type to be one of \["stacks" "blueprints" "contexts" "webhooks"\], got whatever`), + }, + }) + }) +}