From 131e4889de4e99f759b573732956b84ccbdb7642 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Tue, 28 May 2024 16:29:11 -0700 Subject: [PATCH] add new WIF fields with test --- internal/consts/consts.go | 19 +- internal/provider/meta.go | 17 +- vault/resource_gcp_secret_backend.go | 256 ++++++++++++++-------- vault/resource_gcp_secret_backend_test.go | 25 +++ 4 files changed, 216 insertions(+), 101 deletions(-) diff --git a/internal/consts/consts.go b/internal/consts/consts.go index a1d78dd221..c70ee147cc 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -485,14 +485,17 @@ const ( /* Vault version constants */ - VaultVersion190 = "1.9.0" - VaultVersion110 = "1.10.0" - VaultVersion111 = "1.11.0" - VaultVersion112 = "1.12.0" - VaultVersion113 = "1.13.0" - VaultVersion114 = "1.14.0" - VaultVersion115 = "1.15.0" - VaultVersion116 = "1.16.0" + VaultVersion190 = "1.9.0" + VaultVersion110 = "1.10.0" + VaultVersion111 = "1.11.0" + VaultVersion112 = "1.12.0" + VaultVersion113 = "1.13.0" + VaultVersion114 = "1.14.0" + VaultVersion115 = "1.15.0" + VaultVersion116 = "1.16.0" + VaultVersion116Ent = "1.16.0+ent" + VaultVersion117 = "1.17.0" + VaultVersion117Ent = "1.17.0+ent" /* Vault auth methods diff --git a/internal/provider/meta.go b/internal/provider/meta.go index 4f741f8c69..3fb885ec5c 100644 --- a/internal/provider/meta.go +++ b/internal/provider/meta.go @@ -35,13 +35,16 @@ const ( var ( MaxHTTPRetriesCCC int - VaultVersion110 = version.Must(version.NewSemver(consts.VaultVersion110)) - VaultVersion111 = version.Must(version.NewSemver(consts.VaultVersion111)) - VaultVersion112 = version.Must(version.NewSemver(consts.VaultVersion112)) - VaultVersion113 = version.Must(version.NewSemver(consts.VaultVersion113)) - VaultVersion114 = version.Must(version.NewSemver(consts.VaultVersion114)) - VaultVersion115 = version.Must(version.NewSemver(consts.VaultVersion115)) - VaultVersion116 = version.Must(version.NewSemver(consts.VaultVersion116)) + VaultVersion110 = version.Must(version.NewSemver(consts.VaultVersion110)) + VaultVersion111 = version.Must(version.NewSemver(consts.VaultVersion111)) + VaultVersion112 = version.Must(version.NewSemver(consts.VaultVersion112)) + VaultVersion113 = version.Must(version.NewSemver(consts.VaultVersion113)) + VaultVersion114 = version.Must(version.NewSemver(consts.VaultVersion114)) + VaultVersion115 = version.Must(version.NewSemver(consts.VaultVersion115)) + VaultVersion116 = version.Must(version.NewSemver(consts.VaultVersion116)) + VaultVersion116Ent = version.Must(version.NewSemver(consts.VaultVersion116Ent)) + VaultVersion117 = version.Must(version.NewSemver(consts.VaultVersion117)) + VaultVersion117Ent = version.Must(version.NewSemver(consts.VaultVersion117Ent)) TokenTTLMinRecommended = time.Minute * 15 ) diff --git a/vault/resource_gcp_secret_backend.go b/vault/resource_gcp_secret_backend.go index 948e43b0cc..381993e65a 100644 --- a/vault/resource_gcp_secret_backend.go +++ b/vault/resource_gcp_secret_backend.go @@ -10,6 +10,7 @@ import ( "log" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" @@ -21,18 +22,17 @@ import ( func gcpSecretBackendResource(name string) *schema.Resource { return provider.MustAddMountMigrationSchema(&schema.Resource{ - Create: gcpSecretBackendCreate, - Read: provider.ReadWrapper(gcpSecretBackendRead), - Update: gcpSecretBackendUpdate, - Delete: gcpSecretBackendDelete, - Exists: gcpSecretBackendExists, + CreateContext: gcpSecretBackendCreate, + ReadContext: provider.ReadContextWrapper(gcpSecretBackendRead), + UpdateContext: gcpSecretBackendUpdate, + DeleteContext: gcpSecretBackendDelete, CustomizeDiff: getMountCustomizeDiffFunc(consts.FieldPath), Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + StateContext: schema.ImportStatePassthroughContext, }, Schema: map[string]*schema.Schema{ - "path": { + consts.FieldPath: { Type: schema.TypeString, Optional: true, Default: consts.MountTypeGCP, @@ -48,7 +48,7 @@ func gcpSecretBackendResource(name string) *schema.Resource { return old+"/" == new || new+"/" == old }, }, - "credentials": { + consts.FieldCredentials: { Type: schema.TypeString, Optional: true, Description: "JSON-encoded credentials to use to connect to GCP", @@ -60,24 +60,24 @@ func gcpSecretBackendResource(name string) *schema.Resource { StateFunc: NormalizeDataJSONFunc(name), ValidateFunc: ValidateDataJSONFunc(name), }, - "description": { + consts.FieldDescription: { Type: schema.TypeString, Optional: true, Description: "Human-friendly description of the mount for the backend.", }, - "default_lease_ttl_seconds": { + consts.FieldDefaultLeaseTTL: { Type: schema.TypeInt, Optional: true, Default: "", Description: "Default lease duration for secrets in seconds", }, - "max_lease_ttl_seconds": { + consts.FieldMaxLeaseTTL: { Type: schema.TypeInt, Optional: true, Default: "", Description: "Maximum possible lease duration for secrets in seconds", }, - "local": { + consts.FieldLocal: { Type: schema.TypeBool, Required: false, Optional: true, @@ -85,70 +85,118 @@ func gcpSecretBackendResource(name string) *schema.Resource { ForceNew: true, Description: "Local mount flag that can be explicitly set to true to enforce local mount in HA environment", }, + consts.FieldIdentityTokenKey: { + Type: schema.TypeString, + Optional: true, + Description: "The key to use for signing identity tokens.", + }, + consts.FieldIdentityTokenAudience: { + Type: schema.TypeString, + Optional: true, + Description: "The audience claim value for plugin identity tokens.", + }, + consts.FieldIdentityTokenTTL: { + Type: schema.TypeInt, + Optional: true, + Description: "The TTL of generated tokens.", + }, + consts.FieldServiceAccountEmail: { + Type: schema.TypeString, + Optional: true, + Description: "Service Account to impersonate for plugin workload identity federation.", + }, + consts.FieldAccessor: { + Type: schema.TypeString, + Computed: true, + Description: "Accessor of the created GCP mount.", + }, }, }, false) } -func gcpSecretBackendCreate(d *schema.ResourceData, meta interface{}) error { +func gcpSecretBackendCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client, e := provider.GetClient(d, meta) if e != nil { - return e + return diag.FromErr(e) } - path := d.Get("path").(string) - description := d.Get("description").(string) - defaultTTL := d.Get("default_lease_ttl_seconds").(int) - maxTTL := d.Get("max_lease_ttl_seconds").(int) - credentials := d.Get("credentials").(string) - local := d.Get("local").(bool) + path := d.Get(consts.FieldPath).(string) + description := d.Get(consts.FieldDescription).(string) + defaultTTL := d.Get(consts.FieldDefaultLeaseTTL).(int) + maxTTL := d.Get(consts.FieldMaxLeaseTTL).(int) + local := d.Get(consts.FieldLocal).(bool) + identityTokenKey := d.Get(consts.FieldIdentityTokenKey).(string) configPath := gcpSecretBackendConfigPath(path) d.Partial(true) log.Printf("[DEBUG] Mounting GCP backend at %q", path) + useAPIVer117Ent := provider.IsAPISupported(meta, provider.VaultVersion117Ent) + + mountConfig := api.MountConfigInput{ + DefaultLeaseTTL: fmt.Sprintf("%ds", defaultTTL), + MaxLeaseTTL: fmt.Sprintf("%ds", maxTTL), + } + + // ID Token Key is only used in GCP mounts for 1.17+ + if useAPIVer117Ent { + mountConfig.IdentityTokenKey = identityTokenKey + } + err := client.Sys().Mount(path, &api.MountInput{ Type: consts.MountTypeGCP, Description: description, - Config: api.MountConfigInput{ - DefaultLeaseTTL: fmt.Sprintf("%ds", defaultTTL), - MaxLeaseTTL: fmt.Sprintf("%ds", maxTTL), - }, - Local: local, + Config: mountConfig, + Local: local, }) if err != nil { - return fmt.Errorf("error mounting to %q: %s", path, err) + return diag.Errorf("error mounting to %q: %s", path, err) } log.Printf("[DEBUG] Mounted GCP backend at %q", path) d.SetId(path) log.Printf("[DEBUG] Writing GCP configuration to %q", configPath) - if credentials != "" { - data := map[string]interface{}{ - "credentials": credentials, - } - if _, err := client.Logical().Write(configPath, data); err != nil { - return fmt.Errorf("error writing GCP configuration for %q: %s", path, err) + + data := map[string]interface{}{} + fields := []string{ + consts.FieldCredentials, + } + + if useAPIVer117Ent { + fields = append(fields, + consts.FieldIdentityTokenAudience, + consts.FieldIdentityTokenTTL, + consts.FieldServiceAccountEmail, + ) + } + + for _, k := range fields { + if v, ok := d.GetOk(k); ok { + data[k] = v } - } else { - log.Printf("[DEBUG] No credentials configured") } + + if _, err := client.Logical().WriteWithContext(ctx, configPath, data); err != nil { + return diag.Errorf("error writing GCP configuration for %q: %s", path, err) + } + log.Printf("[DEBUG] Wrote GCP configuration to %q", configPath) d.Partial(false) - return gcpSecretBackendRead(d, meta) + return gcpSecretBackendRead(ctx, d, meta) } -func gcpSecretBackendRead(d *schema.ResourceData, meta interface{}) error { +func gcpSecretBackendRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client, e := provider.GetClient(d, meta) if e != nil { - return e + return diag.FromErr(e) } path := d.Id() log.Printf("[DEBUG] Reading GCP backend mount %q from Vault", path) - mount, err := mountutil.GetMount(context.Background(), client, path) + mount, err := mountutil.GetMount(ctx, client, path) if errors.Is(err, mountutil.ErrMountNotFound) { log.Printf("[WARN] Mount %q not found, removing from state.", path) d.SetId("") @@ -156,24 +204,59 @@ func gcpSecretBackendRead(d *schema.ResourceData, meta interface{}) error { } if err != nil { - return err + return diag.FromErr(err) } log.Printf("[DEBUG] Read GCP backend mount %q from Vault", path) - d.Set("path", path) - d.Set("description", mount.Description) - d.Set("default_lease_ttl_seconds", mount.Config.DefaultLeaseTTL) - d.Set("max_lease_ttl_seconds", mount.Config.MaxLeaseTTL) - d.Set("local", mount.Local) + if err := d.Set(consts.FieldPath, path); err != nil { + return diag.FromErr(err) + } + if err := d.Set(consts.FieldDescription, mount.Description); err != nil { + return diag.FromErr(err) + } + if err := d.Set(consts.FieldDefaultLeaseTTL, mount.Config.DefaultLeaseTTL); err != nil { + return diag.FromErr(err) + } + if err := d.Set(consts.FieldMaxLeaseTTL, mount.Config.MaxLeaseTTL); err != nil { + return diag.FromErr(err) + } + if err := d.Set(consts.FieldLocal, mount.Local); err != nil { + return diag.FromErr(err) + } + + if err := d.Set(consts.FieldAccessor, mount.Accessor); err != nil { + return diag.FromErr(err) + } + + // read and set config if needed + if provider.IsAPISupported(meta, provider.VaultVersion117Ent) { + resp, err := client.Logical().Read(gcpSecretBackendConfigPath(path)) + if err != nil { + return diag.FromErr(err) + } + fields := []string{ + consts.FieldIdentityTokenAudience, + consts.FieldIdentityTokenTTL, + consts.FieldServiceAccountEmail, + } + + for _, k := range fields { + if v, ok := resp.Data[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.FromErr(err) + } + } + } + } return nil } -func gcpSecretBackendUpdate(d *schema.ResourceData, meta interface{}) error { +func gcpSecretBackendUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client, e := provider.GetClient(d, meta) if e != nil { - return e + return diag.FromErr(e) } path := d.Id() @@ -181,74 +264,75 @@ func gcpSecretBackendUpdate(d *schema.ResourceData, meta interface{}) error { path, err := util.Remount(d, client, consts.FieldPath, false) if err != nil { - return err + return diag.FromErr(err) } - if d.HasChange("default_lease_ttl_seconds") || d.HasChange("max_lease_ttl_seconds") { + useAPIVer117Ent := provider.IsAPISupported(meta, provider.VaultVersion117Ent) + + if d.HasChange(consts.FieldDefaultLeaseTTL) || d.HasChange(consts.FieldMaxLeaseTTL) || d.HasChange(consts.FieldIdentityTokenKey) { config := api.MountConfigInput{ - DefaultLeaseTTL: fmt.Sprintf("%ds", d.Get("default_lease_ttl_seconds")), - MaxLeaseTTL: fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds")), + DefaultLeaseTTL: fmt.Sprintf("%ds", d.Get(consts.FieldDefaultLeaseTTL)), + MaxLeaseTTL: fmt.Sprintf("%ds", d.Get(consts.FieldMaxLeaseTTL)), + } + + if useAPIVer117Ent { + config.IdentityTokenKey = d.Get(consts.FieldIdentityTokenKey).(string) } - log.Printf("[DEBUG] Updating lease TTLs for %q", path) - err := client.Sys().TuneMount(path, config) + + log.Printf("[DEBUG] Updating mount config for %q", path) + err := client.Sys().TuneMountWithContext(ctx, path, config) if err != nil { - return fmt.Errorf("error updating mount TTLs for %q: %s", path, err) + return diag.Errorf("error updating mount config for %q: %s", path, err) } - log.Printf("[DEBUG] Updated lease TTLs for %q", path) + log.Printf("[DEBUG] Updated mount config for %q", path) } - if d.HasChange("credentials") { - data := map[string]interface{}{ - "credentials": d.Get("credentials"), + data := make(map[string]interface{}) + + if d.HasChange(consts.FieldCredentials) { + data[consts.FieldCredentials] = d.Get(consts.FieldCredentials) + } + if useAPIVer117Ent { + if d.HasChange(consts.FieldIdentityTokenAudience) { + data[consts.FieldIdentityTokenAudience] = d.Get(consts.FieldIdentityTokenAudience) + } + + if d.HasChange(consts.FieldIdentityTokenTTL) { + data[consts.FieldIdentityTokenTTL] = d.Get(consts.FieldIdentityTokenTTL) } - configPath := gcpSecretBackendConfigPath(path) - if _, err := client.Logical().Write(configPath, data); err != nil { - return fmt.Errorf("error writing GCP credentials for %q: %s", path, err) + + if d.HasChange(consts.FieldServiceAccountEmail) { + data[consts.FieldServiceAccountEmail] = d.Get(consts.FieldServiceAccountEmail) } - log.Printf("[DEBUG] Updated credentials for %q", path) } + configPath := gcpSecretBackendConfigPath(path) + if _, err := client.Logical().WriteWithContext(ctx, configPath, data); err != nil { + return diag.Errorf("error writing GCP credentials for %q: %s", path, err) + } + log.Printf("[DEBUG] Updated credentials for %q", path) + d.Partial(false) - return gcpSecretBackendRead(d, meta) + return gcpSecretBackendRead(ctx, d, meta) } -func gcpSecretBackendDelete(d *schema.ResourceData, meta interface{}) error { +func gcpSecretBackendDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client, e := provider.GetClient(d, meta) if e != nil { - return e + return diag.FromErr(e) } path := d.Id() log.Printf("[DEBUG] Unmounting GCP backend %q", path) - err := client.Sys().Unmount(path) + err := client.Sys().UnmountWithContext(ctx, path) if err != nil { - return fmt.Errorf("error unmounting GCP backend from %q: %s", path, err) + return diag.Errorf("error unmounting GCP backend from %q: %s", path, err) } log.Printf("[DEBUG] Unmounted GCP backend %q", path) return nil } -func gcpSecretBackendExists(d *schema.ResourceData, meta interface{}) (bool, error) { - client, e := provider.GetClient(d, meta) - if e != nil { - return false, e - } - - path := d.Id() - log.Printf("[DEBUG] Checking if GCP backend exists at %q", path) - _, err := mountutil.GetMount(context.Background(), client, path) - if errors.Is(err, mountutil.ErrMountNotFound) { - return false, nil - } - - if err != nil { - return true, err - } - - return true, nil -} - func gcpSecretBackendConfigPath(backend string) string { return strings.Trim(backend, "/") + "/config" } diff --git a/vault/resource_gcp_secret_backend_test.go b/vault/resource_gcp_secret_backend_test.go index f46097037a..4f3e68c398 100644 --- a/vault/resource_gcp_secret_backend_test.go +++ b/vault/resource_gcp_secret_backend_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" "github.com/hashicorp/terraform-provider-vault/testutil" ) @@ -47,6 +48,20 @@ func TestGCPSecretBackend(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "local", "true"), ), }, + { + SkipFunc: func() (bool, error) { + meta := testProvider.Meta().(*provider.ProviderMeta) + return !meta.IsAPISupported(provider.VaultVersion117Ent), nil + }, + Config: testGCPSecretBackend_WIFConfig(path), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "path", path), + resource.TestCheckResourceAttr(resourceName, consts.FieldIdentityTokenAudience, "test"), + resource.TestCheckResourceAttr(resourceName, consts.FieldIdentityTokenTTL, "30"), + resource.TestCheckResourceAttr(resourceName, consts.FieldServiceAccountEmail, "test"), + ), + }, + testutil.GetImportTestStep(resourceName, false, nil, consts.FieldDisableRemount), }, }) } @@ -104,6 +119,16 @@ EOF }`, path) } +func testGCPSecretBackend_WIFConfig(path string) string { + return fmt.Sprintf(` +resource "vault_gcp_secret_backend" "test" { + path = "%s" + service_account_email = "test" + identity_token_audience = "test" + identity_token_ttl = 30 +}`, path) +} + func testGCPSecretBackend_updateConfig(path string) string { return fmt.Sprintf(` resource "vault_gcp_secret_backend" "test" {