From 55aef93f60310aaee2b922a9bd82b03da4b21416 Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Fri, 18 Jun 2021 23:26:00 +0000 Subject: [PATCH] Add TTL fields to Secret Manager (#4821) Co-authored-by: upodroid Signed-off-by: Modular Magician --- .changelog/4821.txt | 3 + google-beta/resource_secret_manager_secret.go | 235 ++++++++++++++++++ ...ce_secret_manager_secret_generated_test.go | 2 +- .../resource_secret_manager_secret_test.go | 17 +- .../r/secret_manager_secret.html.markdown | 39 +++ 5 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 .changelog/4821.txt diff --git a/.changelog/4821.txt b/.changelog/4821.txt new file mode 100644 index 0000000000..614608f758 --- /dev/null +++ b/.changelog/4821.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +secretmanager: added `ttl`, `expire_time`, `topics` and `rotation` fields to `google_secret_manager_secret` +``` diff --git a/google-beta/resource_secret_manager_secret.go b/google-beta/resource_secret_manager_secret.go index 3d37e29e52..3cb1587c33 100644 --- a/google-beta/resource_secret_manager_secret.go +++ b/google-beta/resource_secret_manager_secret.go @@ -107,6 +107,13 @@ after the Secret has been created.`, ForceNew: true, Description: `This must be unique within the project.`, }, + "expire_time": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Timestamp in UTC when the Secret is scheduled to expire. This is always provided on output, regardless of what was sent on input. +A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z".`, + }, "labels": { Type: schema.TypeMap, Optional: true, @@ -124,6 +131,54 @@ An object containing a list of "key": value pairs. Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }.`, Elem: &schema.Schema{Type: schema.TypeString}, }, + "rotation": { + Type: schema.TypeList, + Optional: true, + Description: `The rotation time and period for a Secret. At 'next_rotation_time', Secret Manager will send a Pub/Sub notification to the topics configured on the Secret. 'topics' must be set to configure rotation.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "next_rotation_time": { + Type: schema.TypeString, + Optional: true, + Description: `Timestamp in UTC at which the Secret is scheduled to rotate. +A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z".`, + RequiredWith: []string{"rotation.0.rotation_period"}, + }, + "rotation_period": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The Duration between rotation notifications. Must be in seconds and at least 3600s (1h) and at most 3153600000s (100 years). +If rotationPeriod is set, 'next_rotation_time' must be set. 'next_rotation_time' will be advanced by this period when the service automatically sends rotation notifications.`, + }, + }, + }, + RequiredWith: []string{"topics"}, + }, + "topics": { + Type: schema.TypeList, + Optional: true, + Description: `A list of up to 10 Pub/Sub topics to which messages are published when control plane operations are called on the secret or its versions.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: `The resource name of the Pub/Sub topic that will be published to, in the following format: projects/*/topics/*. +For publication to succeed, the Secret Manager Service Agent service account must have pubsub.publisher permissions on the topic.`, + }, + }, + }, + RequiredWith: []string{"rotation"}, + }, + "ttl": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The TTL for the Secret. +A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s".`, + }, "create_time": { Type: schema.TypeString, Computed: true, @@ -166,6 +221,30 @@ func resourceSecretManagerSecretCreate(d *schema.ResourceData, meta interface{}) } else if v, ok := d.GetOkExists("replication"); !isEmptyValue(reflect.ValueOf(replicationProp)) && (ok || !reflect.DeepEqual(v, replicationProp)) { obj["replication"] = replicationProp } + topicsProp, err := expandSecretManagerSecretTopics(d.Get("topics"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("topics"); !isEmptyValue(reflect.ValueOf(topicsProp)) && (ok || !reflect.DeepEqual(v, topicsProp)) { + obj["topics"] = topicsProp + } + expireTimeProp, err := expandSecretManagerSecretExpireTime(d.Get("expire_time"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("expire_time"); !isEmptyValue(reflect.ValueOf(expireTimeProp)) && (ok || !reflect.DeepEqual(v, expireTimeProp)) { + obj["expireTime"] = expireTimeProp + } + ttlProp, err := expandSecretManagerSecretTtl(d.Get("ttl"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("ttl"); !isEmptyValue(reflect.ValueOf(ttlProp)) && (ok || !reflect.DeepEqual(v, ttlProp)) { + obj["ttl"] = ttlProp + } + rotationProp, err := expandSecretManagerSecretRotation(d.Get("rotation"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("rotation"); !isEmptyValue(reflect.ValueOf(rotationProp)) && (ok || !reflect.DeepEqual(v, rotationProp)) { + obj["rotation"] = rotationProp + } url, err := replaceVars(d, config, "{{SecretManagerBasePath}}projects/{{project}}/secrets?secretId={{secret_id}}") if err != nil { @@ -252,6 +331,15 @@ func resourceSecretManagerSecretRead(d *schema.ResourceData, meta interface{}) e if err := d.Set("replication", flattenSecretManagerSecretReplication(res["replication"], d, config)); err != nil { return fmt.Errorf("Error reading Secret: %s", err) } + if err := d.Set("topics", flattenSecretManagerSecretTopics(res["topics"], d, config)); err != nil { + return fmt.Errorf("Error reading Secret: %s", err) + } + if err := d.Set("expire_time", flattenSecretManagerSecretExpireTime(res["expireTime"], d, config)); err != nil { + return fmt.Errorf("Error reading Secret: %s", err) + } + if err := d.Set("rotation", flattenSecretManagerSecretRotation(res["rotation"], d, config)); err != nil { + return fmt.Errorf("Error reading Secret: %s", err) + } return nil } @@ -278,6 +366,24 @@ func resourceSecretManagerSecretUpdate(d *schema.ResourceData, meta interface{}) } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, labelsProp)) { obj["labels"] = labelsProp } + topicsProp, err := expandSecretManagerSecretTopics(d.Get("topics"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("topics"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, topicsProp)) { + obj["topics"] = topicsProp + } + expireTimeProp, err := expandSecretManagerSecretExpireTime(d.Get("expire_time"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("expire_time"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, expireTimeProp)) { + obj["expireTime"] = expireTimeProp + } + rotationProp, err := expandSecretManagerSecretRotation(d.Get("rotation"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("rotation"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, rotationProp)) { + obj["rotation"] = rotationProp + } url, err := replaceVars(d, config, "{{SecretManagerBasePath}}projects/{{project}}/secrets/{{secret_id}}") if err != nil { @@ -290,6 +396,18 @@ func resourceSecretManagerSecretUpdate(d *schema.ResourceData, meta interface{}) if d.HasChange("labels") { updateMask = append(updateMask, "labels") } + + if d.HasChange("topics") { + updateMask = append(updateMask, "topics") + } + + if d.HasChange("expire_time") { + updateMask = append(updateMask, "expireTime") + } + + if d.HasChange("rotation") { + updateMask = append(updateMask, "rotation") + } // updateMask is a URL parameter but not present in the schema, so replaceVars // won't set it url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) @@ -454,6 +572,55 @@ func flattenSecretManagerSecretReplicationUserManagedReplicasCustomerManagedEncr return v } +func flattenSecretManagerSecretTopics(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed = append(transformed, map[string]interface{}{ + "name": flattenSecretManagerSecretTopicsName(original["name"], d, config), + }) + } + return transformed +} +func flattenSecretManagerSecretTopicsName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenSecretManagerSecretExpireTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenSecretManagerSecretRotation(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["next_rotation_time"] = + flattenSecretManagerSecretRotationNextRotationTime(original["nextRotationTime"], d, config) + transformed["rotation_period"] = + flattenSecretManagerSecretRotationRotationPeriod(original["rotationPeriod"], d, config) + return []interface{}{transformed} +} +func flattenSecretManagerSecretRotationNextRotationTime(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenSecretManagerSecretRotationRotationPeriod(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + func expandSecretManagerSecretLabels(v interface{}, d TerraformResourceData, config *Config) (map[string]string, error) { if v == nil { return map[string]string{}, nil @@ -573,3 +740,71 @@ func expandSecretManagerSecretReplicationUserManagedReplicasCustomerManagedEncry func expandSecretManagerSecretReplicationUserManagedReplicasCustomerManagedEncryptionKmsKeyName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { return v, nil } + +func expandSecretManagerSecretTopics(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedName, err := expandSecretManagerSecretTopicsName(original["name"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedName); val.IsValid() && !isEmptyValue(val) { + transformed["name"] = transformedName + } + + req = append(req, transformed) + } + return req, nil +} + +func expandSecretManagerSecretTopicsName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandSecretManagerSecretExpireTime(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandSecretManagerSecretTtl(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandSecretManagerSecretRotation(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedNextRotationTime, err := expandSecretManagerSecretRotationNextRotationTime(original["next_rotation_time"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedNextRotationTime); val.IsValid() && !isEmptyValue(val) { + transformed["nextRotationTime"] = transformedNextRotationTime + } + + transformedRotationPeriod, err := expandSecretManagerSecretRotationRotationPeriod(original["rotation_period"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedRotationPeriod); val.IsValid() && !isEmptyValue(val) { + transformed["rotationPeriod"] = transformedRotationPeriod + } + + return transformed, nil +} + +func expandSecretManagerSecretRotationNextRotationTime(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandSecretManagerSecretRotationRotationPeriod(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} diff --git a/google-beta/resource_secret_manager_secret_generated_test.go b/google-beta/resource_secret_manager_secret_generated_test.go index f751d4d03b..e62da9cf9e 100644 --- a/google-beta/resource_secret_manager_secret_generated_test.go +++ b/google-beta/resource_secret_manager_secret_generated_test.go @@ -42,7 +42,7 @@ func TestAccSecretManagerSecret_secretConfigBasicExample(t *testing.T) { ResourceName: "google_secret_manager_secret.secret-basic", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"secret_id"}, + ImportStateVerifyIgnore: []string{"ttl", "secret_id"}, }, }, }) diff --git a/google-beta/resource_secret_manager_secret_test.go b/google-beta/resource_secret_manager_secret_test.go index f62f50dfa9..86e29970e8 100644 --- a/google-beta/resource_secret_manager_secret_test.go +++ b/google-beta/resource_secret_manager_secret_test.go @@ -22,9 +22,10 @@ func TestAccSecretManagerSecret_import(t *testing.T) { Config: testAccSecretManagerSecret_basic(context), }, { - ResourceName: "google_secret_manager_secret.secret-basic", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_secret_manager_secret.secret-basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ttl"}, }, }, }) @@ -50,9 +51,10 @@ func TestAccSecretManagerSecret_cmek(t *testing.T) { Config: testAccSecretMangerSecret_cmek(context1), }, { - ResourceName: "google_secret_manager_secret.secret-basic", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_secret_manager_secret.secret-basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ttl"}, }, }, }) @@ -77,6 +79,9 @@ resource "google_secret_manager_secret" "secret-basic" { } } } + + ttl = "3600s" + } `, context) } diff --git a/website/docs/r/secret_manager_secret.html.markdown b/website/docs/r/secret_manager_secret.html.markdown index e157e4da4c..5dae91b840 100644 --- a/website/docs/r/secret_manager_secret.html.markdown +++ b/website/docs/r/secret_manager_secret.html.markdown @@ -126,10 +126,49 @@ The `customer_managed_encryption` block supports: An object containing a list of "key": value pairs. Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }. +* `topics` - + (Optional) + A list of up to 10 Pub/Sub topics to which messages are published when control plane operations are called on the secret or its versions. + Structure is documented below. + +* `expire_time` - + (Optional) + Timestamp in UTC when the Secret is scheduled to expire. This is always provided on output, regardless of what was sent on input. + A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z". + +* `ttl` - + (Optional) + The TTL for the Secret. + A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". + +* `rotation` - + (Optional) + The rotation time and period for a Secret. At `next_rotation_time`, Secret Manager will send a Pub/Sub notification to the topics configured on the Secret. `topics` must be set to configure rotation. + Structure is documented below. + * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. +The `topics` block supports: + +* `name` - + (Required) + The resource name of the Pub/Sub topic that will be published to, in the following format: projects/*/topics/*. + For publication to succeed, the Secret Manager Service Agent service account must have pubsub.publisher permissions on the topic. + +The `rotation` block supports: + +* `next_rotation_time` - + (Optional) + Timestamp in UTC at which the Secret is scheduled to rotate. + A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z". + +* `rotation_period` - + (Optional) + The Duration between rotation notifications. Must be in seconds and at least 3600s (1h) and at most 3153600000s (100 years). + If rotationPeriod is set, `next_rotation_time` must be set. `next_rotation_time` will be advanced by this period when the service automatically sends rotation notifications. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are exported: