From ac23b84947ea784df76095877a3e1bbbed06e914 Mon Sep 17 00:00:00 2001 From: Martin Englund Date: Mon, 20 May 2024 10:17:06 -0700 Subject: [PATCH] add support for automatically generating query lambda tags --- docs/resources/query_lambda_auto_tag.md | 55 +++++ .../rockset_query_lambda_auto_tag/resource.tf | 15 ++ rockset/provider.go | 51 ++-- rockset/resource_query_lambda_auto_tag.go | 229 ++++++++++++++++++ .../resource_query_lambda_auto_tag_test.go | 87 +++++++ rockset/resource_query_lambda_tag_test.go | 1 - testdata/query_lambda_auto_tag.tf | 16 ++ 7 files changed, 428 insertions(+), 26 deletions(-) create mode 100644 docs/resources/query_lambda_auto_tag.md create mode 100644 examples/resources/rockset_query_lambda_auto_tag/resource.tf create mode 100644 rockset/resource_query_lambda_auto_tag.go create mode 100644 rockset/resource_query_lambda_auto_tag_test.go create mode 100644 testdata/query_lambda_auto_tag.tf diff --git a/docs/resources/query_lambda_auto_tag.md b/docs/resources/query_lambda_auto_tag.md new file mode 100644 index 0000000..7460fbb --- /dev/null +++ b/docs/resources/query_lambda_auto_tag.md @@ -0,0 +1,55 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "rockset_query_lambda_auto_tag Resource - rockset" +subcategory: "" +description: |- + Automatically tags a Query Lambda using a name template, which must contain %s, that is replaced with an ever increasing number. + ~> On resource delete it will remove all old tags that it created. +--- + +# rockset_query_lambda_auto_tag (Resource) + +Automatically tags a Query Lambda using a name template, which must contain %s, that is replaced with an ever increasing number. + +~> On resource delete it will remove all old tags that it created. + +## Example Usage + +```terraform +resource "rockset_query_lambda" "top-movies" { + name = "top_movies" + workspace = "commons" + sql { + query = file("${path.module}/data/top_movies.sql") + } +} + +resource "rockset_query_lambda_auto_tag" "active" { + template = "v%s" + max_tags = 3 + query_lambda = rockset_query_lambda.top-movies.name + workspace = rockset_query_lambda.top-movies.workspace + version = rockset_query_lambda.top-movies.version +} +``` + + +## Schema + +### Required + +- `query_lambda` (String) Unique identifier for the query lambda. +- `template` (String) Template for the tag `name`. Can contain alphanumeric or dash characters. Use `%s` to insert the version. +- `version` (String) Version of the query lambda this tag should point to. +- `workspace` (String) The name of the workspace the query lambda is in. + +### Optional + +- `max_tags` (Number) Maximum number of previous auto-generated tags to keep. + +### Read-Only + +- `id` (String) The ID of this resource. +- `name` (String) Unique identifier for the tag, generated from the `template` and `tag_version`. +- `tag_version` (Number) Auto-incremented version number for the tag `name`. Starts at 1 when the tag is created and is incremented every time the tag is updated. +- `tags` (List of String) List of previous auto-generated tags that are kept in accordance with `max_tags`. diff --git a/examples/resources/rockset_query_lambda_auto_tag/resource.tf b/examples/resources/rockset_query_lambda_auto_tag/resource.tf new file mode 100644 index 0000000..096edc4 --- /dev/null +++ b/examples/resources/rockset_query_lambda_auto_tag/resource.tf @@ -0,0 +1,15 @@ +resource "rockset_query_lambda" "top-movies" { + name = "top_movies" + workspace = "commons" + sql { + query = file("${path.module}/data/top_movies.sql") + } +} + +resource "rockset_query_lambda_auto_tag" "active" { + template = "v%s" + max_tags = 3 + query_lambda = rockset_query_lambda.top-movies.name + workspace = rockset_query_lambda.top-movies.workspace + version = rockset_query_lambda.top-movies.version +} diff --git a/rockset/provider.go b/rockset/provider.go index 4a5e396..538a7f4 100644 --- a/rockset/provider.go +++ b/rockset/provider.go @@ -26,31 +26,32 @@ func Provider() *schema.Provider { schema.DescriptionKind = schema.StringMarkdown return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "rockset_alias": resourceAlias(), - "rockset_api_key": resourceApiKey(), - "rockset_autoscaling_policy": resourceAutoScalingPolicy(), - "rockset_collection": resourceCollection(), - "rockset_collection_mount": resourceCollectionMount(), - "rockset_dynamodb_collection": resourceDynamoDBCollection(), - "rockset_dynamodb_integration": resourceDynamoDBIntegration(), - "rockset_gcs_collection": resourceGCSCollection(), - "rockset_gcs_integration": resourceGCSIntegration(), - "rockset_kafka_collection": resourceKafkaCollection(), - "rockset_kafka_integration": resourceKafkaIntegration(), - "rockset_kinesis_collection": resourceKinesisCollection(), - "rockset_kinesis_integration": resourceKinesisIntegration(), - "rockset_mongodb_collection": resourceMongoDBCollection(), - "rockset_mongodb_integration": resourceMongoDBIntegration(), - "rockset_query_lambda": resourceQueryLambda(), - "rockset_query_lambda_tag": resourceQueryLambdaTag(), - "rockset_role": resourceRole(), - "rockset_s3_collection": resourceS3Collection(), - "rockset_s3_integration": resourceS3Integration(), - "rockset_user": resourceUser(), - "rockset_view": resourceView(), - "rockset_virtual_instance": resourceVirtualInstance(), - "rockset_workspace": resourceWorkspace(), - "rockset_scheduled_lambda": resourceScheduledLambda(), + "rockset_alias": resourceAlias(), + "rockset_api_key": resourceApiKey(), + "rockset_autoscaling_policy": resourceAutoScalingPolicy(), + "rockset_collection": resourceCollection(), + "rockset_collection_mount": resourceCollectionMount(), + "rockset_dynamodb_collection": resourceDynamoDBCollection(), + "rockset_dynamodb_integration": resourceDynamoDBIntegration(), + "rockset_gcs_collection": resourceGCSCollection(), + "rockset_gcs_integration": resourceGCSIntegration(), + "rockset_kafka_collection": resourceKafkaCollection(), + "rockset_kafka_integration": resourceKafkaIntegration(), + "rockset_kinesis_collection": resourceKinesisCollection(), + "rockset_kinesis_integration": resourceKinesisIntegration(), + "rockset_mongodb_collection": resourceMongoDBCollection(), + "rockset_mongodb_integration": resourceMongoDBIntegration(), + "rockset_query_lambda": resourceQueryLambda(), + "rockset_query_lambda_tag": resourceQueryLambdaTag(), + "rockset_query_lambda_auto_tag": resourceQueryLambdaAutoTag(), + "rockset_role": resourceRole(), + "rockset_s3_collection": resourceS3Collection(), + "rockset_s3_integration": resourceS3Integration(), + "rockset_user": resourceUser(), + "rockset_view": resourceView(), + "rockset_virtual_instance": resourceVirtualInstance(), + "rockset_workspace": resourceWorkspace(), + "rockset_scheduled_lambda": resourceScheduledLambda(), }, DataSourcesMap: map[string]*schema.Resource{ "rockset_account": dataSourceRocksetAccount(), diff --git a/rockset/resource_query_lambda_auto_tag.go b/rockset/resource_query_lambda_auto_tag.go new file mode 100644 index 0000000..81412df --- /dev/null +++ b/rockset/resource_query_lambda_auto_tag.go @@ -0,0 +1,229 @@ +package rockset + +import ( + "context" + "errors" + "regexp" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "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/rockset/rockset-go-client" + rockerr "github.com/rockset/rockset-go-client/errors" +) + +func resourceQueryLambdaAutoTag() *schema.Resource { + percentS := regexp.MustCompile("%s") + + return &schema.Resource{ + Description: `Automatically tags a Query Lambda using a name template, which must contain %s, that is replaced with an ever increasing number. + +~> On resource delete it will remove all old tags that it created.`, + + CreateContext: resourceQueryLambdaAutoTagCreate, + ReadContext: resourceQueryLambdaAutoTagRead, + DeleteContext: resourceQueryLambdaAutoTagDelete, + UpdateContext: resourceQueryLambdaTagUpdate, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Description: "Unique identifier for the tag, generated from the `template` and `tag_version`.", + Type: schema.TypeString, + Computed: true, + }, + "template": { + Description: "Template for the tag `name`. Can contain alphanumeric or dash characters. Use `%s` to insert the version.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch(percentS, "must contain %s"), + }, + "tag_version": { + Description: "Auto-incremented version number for the tag `name`. Starts at 1 when the tag is created and is incremented every time the tag is updated.", + Type: schema.TypeInt, + Computed: true, + }, + "max_tags": { + Description: "Maximum number of previous auto-generated tags to keep.", + Type: schema.TypeInt, + Optional: true, + Default: 5, + ValidateFunc: validation.IntBetween(1, 20), + }, + "workspace": { + Description: "The name of the workspace the query lambda is in.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "query_lambda": { + Description: "Unique identifier for the query lambda.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: rocksetNameValidator, + }, + "version": { + Description: "Version of the query lambda this tag should point to.", + Type: schema.TypeString, + Required: true, + ValidateFunc: rocksetNameValidator, + }, + "tags": { + Description: "List of previous auto-generated tags that are kept in accordance with `max_tags`.", + Type: schema.TypeList, + Computed: true, + // this is used to keep track of the tags that were created by this resource, so they can be deleted on destroy + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceQueryLambdaAutoTagCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + rc := meta.(*rockset.RockClient) + var diags diag.Diagnostics + + workspace := d.Get("workspace").(string) + template := d.Get("template").(string) + tagName := strings.ReplaceAll(template, "%s", "1") + version := d.Get("version").(string) + queryLambdaName := d.Get("query_lambda").(string) + + d.SetId(toQueryLambdaTagID(workspace, queryLambdaName, template)) + _ = d.Set("tag_version", 1) + _ = d.Set("name", tagName) + _ = d.Set("tags", []string{}) // the current tag is not included in the list of old tags + + _, err := rc.CreateQueryLambdaTag(ctx, workspace, queryLambdaName, version, tagName) + if err != nil { + return DiagFromErr(err) + } + tflog.Info(ctx, "Created query lambda auto tag", map[string]interface{}{ + "workspace": workspace, + "name": queryLambdaName, + "tag": tagName, + "version": version, + }) + + return diags +} + +func resourceQueryLambdaTagUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + rc := meta.(*rockset.RockClient) + var diags diag.Diagnostics + + workspace, queryLambdaName, template := fromQueryLambdaTagID(d.Id()) + tagVersion := d.Get("tag_version").(int) + oldTagName := strings.ReplaceAll(template, "%s", strconv.Itoa(tagVersion)) + tagVersion++ + tagName := strings.ReplaceAll(template, "%s", strconv.Itoa(tagVersion)) + version := d.Get("version").(string) + + maxTags := d.Get("max_tags").(int) + tags := toStringArray(d.Get("tags").([]interface{})) + + var err error + if len(tags) >= maxTags { + // remove the oldest tag so we make room for the new one + oldest := tags[0] + if err = rc.DeleteQueryLambdaTag(ctx, workspace, queryLambdaName, oldest); err != nil { + return diag.Errorf("failed to delete oldest tag %s.%s %s: %v", workspace, queryLambdaName, oldest, err) + } + tflog.Debug(ctx, "Deleted oldest query lambda auto tag", map[string]interface{}{ + "workspace": workspace, + "name": queryLambdaName, + "tag": oldest, + }) + tags = tags[1:] + } + + if _, err = rc.CreateQueryLambdaTag(ctx, workspace, queryLambdaName, version, tagName); err != nil { + return DiagFromErr(err) + } + _ = d.Set("tag_version", tagVersion) + _ = d.Set("name", tagName) + tags = append(tags, oldTagName) + _ = d.Set("tags", tags) + + tflog.Info(ctx, "Updated query lambda auto tag", map[string]interface{}{ + "workspace": workspace, + "name": queryLambdaName, + "tag": tagName, + "version": version, + "tags": tags, + }) + + return diags +} + +func resourceQueryLambdaAutoTagRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + rc := meta.(*rockset.RockClient) + var diags diag.Diagnostics + + workspace, queryLambdaName, template := fromQueryLambdaTagID(d.Id()) + tagVersion := d.Get("tag_version").(int) + tagName := strings.ReplaceAll(template, "%s", strconv.Itoa(tagVersion)) + + queryLambdaTag, err := rc.GetQueryLambdaVersionByTag(ctx, workspace, queryLambdaName, tagName) + if err != nil { + return checkForNotFoundError(d, err) + } + + err = parseQueryLambdaTag(&queryLambdaTag, d) + if err != nil { + return DiagFromErr(err) + } + + return diags +} + +func resourceQueryLambdaAutoTagDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + rc := meta.(*rockset.RockClient) + var diags diag.Diagnostics + + workspace, queryLambdaName, template := fromQueryLambdaTagID(d.Id()) + tagVersion := d.Get("tag_version").(int) + tagName := strings.ReplaceAll(template, "%s", strconv.Itoa(tagVersion)) + + err := rc.DeleteQueryLambdaTag(ctx, workspace, queryLambdaName, tagName) + if err != nil { + return diag.Errorf("failed to delete query lambda auto tag %s.%s:%s: %v", workspace, queryLambdaName, tagName, err) + } + tflog.Info(ctx, "Created query lambda auto tag", map[string]interface{}{ + "workspace": workspace, + "name": queryLambdaName, + "tag": tagName, + "version": tagVersion, + }) + + tags := toStringArray(d.Get("tags").([]interface{})) + for _, tag := range tags { + if err = rc.DeleteQueryLambdaTag(ctx, workspace, queryLambdaName, tag); err != nil { + var re rockerr.Error + if errors.As(err, &re) && re.IsNotFoundError() { + // we ignore not found errors, as the tag is already gone and we want it gone + tflog.Error(ctx, "Failed to delete old tag", map[string]interface{}{ + "workspace": workspace, + "name": queryLambdaName, + "tag": tag, + "error": err, + }) + continue + } + + return diag.Errorf("failed to delete old tag %s: %v", tag, err) + } + } + + return diags +} diff --git a/rockset/resource_query_lambda_auto_tag_test.go b/rockset/resource_query_lambda_auto_tag_test.go new file mode 100644 index 0000000..17d736a --- /dev/null +++ b/rockset/resource_query_lambda_auto_tag_test.go @@ -0,0 +1,87 @@ +package rockset + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccQueryLambdaAutoTag_Basic(t *testing.T) { + tagName := randomName("tag_%s") + v1Tag := strings.ReplaceAll(tagName, "%s", "1") + v2Tag := strings.ReplaceAll(tagName, "%s", "2") + v3Tag := strings.ReplaceAll(tagName, "%s", "3") + v4Tag := strings.ReplaceAll(tagName, "%s", "4") + + v1 := Values{ + Name: randomName("ql"), + Tag: tagName, // used for template + Alias: "commons._events", + Workspace: randomName("ws"), + Description: description(), + SQL: "SELECT 1", + } + v2 := v1 + v2.SQL = "SELECT 2" + v3 := v1 + v3.SQL = "SELECT 3" + v4 := v1 + v4.SQL = "SELECT 4" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckRocksetQueryLambdaTagDestroy, + Steps: []resource.TestStep{ + { + Config: getHCLTemplate("query_lambda_auto_tag.tf", v1), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("rockset_query_lambda.test", "name", v1.Name), + resource.TestCheckResourceAttr("rockset_query_lambda.test", "description", v1.Description), + resource.TestCheckResourceAttrSet("rockset_query_lambda.test", "version"), + resource.TestCheckResourceAttrSet("rockset_query_lambda.test", "state"), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "name", v1Tag), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "tag_version", "1"), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "workspace", "acc"), + resource.TestCheckResourceAttrSet("rockset_query_lambda_auto_tag.test", "version"), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "tags.#", "0"), + ), + }, + { + Config: getHCLTemplate("query_lambda_auto_tag.tf", v2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("rockset_query_lambda.test", "name", v2.Name), + resource.TestCheckResourceAttr("rockset_query_lambda.test", "description", v2.Description), + resource.TestCheckResourceAttrSet("rockset_query_lambda.test", "version"), + resource.TestCheckResourceAttrSet("rockset_query_lambda.test", "state"), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "name", v2Tag), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "tag_version", "2"), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "workspace", "acc"), + resource.TestCheckResourceAttrSet("rockset_query_lambda_auto_tag.test", "version"), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "tags.#", "1"), + ), + }, + { + Config: getHCLTemplate("query_lambda_auto_tag.tf", v3), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("rockset_query_lambda.test", "name", v3.Name), + resource.TestCheckResourceAttr("rockset_query_lambda.test", "description", v3.Description), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "name", v3Tag), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "tag_version", "3"), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "tags.#", "2"), + ), + }, + { + Config: getHCLTemplate("query_lambda_auto_tag.tf", v4), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("rockset_query_lambda.test", "name", v4.Name), + resource.TestCheckResourceAttr("rockset_query_lambda.test", "description", v4.Description), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "name", v4Tag), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "tag_version", "4"), + resource.TestCheckResourceAttr("rockset_query_lambda_auto_tag.test", "tags.#", "2"), + ), + }, + }, + }) +} diff --git a/rockset/resource_query_lambda_tag_test.go b/rockset/resource_query_lambda_tag_test.go index 10c705a..f478342 100644 --- a/rockset/resource_query_lambda_tag_test.go +++ b/rockset/resource_query_lambda_tag_test.go @@ -16,7 +16,6 @@ func TestAccQueryLambdaTag_Basic(t *testing.T) { v1 := Values{ Name: randomName("ql"), Tag: randomName("tag"), - Alias: "commons._events", Workspace: randomName("ws"), Description: description(), SQL: "SELECT 1", diff --git a/testdata/query_lambda_auto_tag.tf b/testdata/query_lambda_auto_tag.tf new file mode 100644 index 0000000..3986d7b --- /dev/null +++ b/testdata/query_lambda_auto_tag.tf @@ -0,0 +1,16 @@ +resource rockset_query_lambda test { + workspace = "acc" + name = "{{ .Name }}" + description = "{{ .Description }}" + sql { + query = "{{ .SQL }}" + } +} + +resource rockset_query_lambda_auto_tag test { + template = "{{ .Tag }}" + max_tags = 2 + workspace = rockset_query_lambda.test.workspace + query_lambda = rockset_query_lambda.test.name + version = rockset_query_lambda.test.version +}