From ed94c4be708829a4045ddd8f2ccf8ce19dc48b88 Mon Sep 17 00:00:00 2001 From: Michael Blaum Date: Tue, 1 Aug 2023 14:41:11 -0400 Subject: [PATCH 1/7] hcp_vault_cluster resource changes for adding vault plugins --- internal/clients/vault_cluster.go | 66 ++++++++++ internal/provider/resource_vault_cluster.go | 123 +++++++++++++++++- .../resource_vault_cluster_const_test.go | 12 ++ .../provider/resource_vault_cluster_test.go | 11 +- internal/provider/validators.go | 19 +++ 5 files changed, 226 insertions(+), 5 deletions(-) diff --git a/internal/clients/vault_cluster.go b/internal/clients/vault_cluster.go index ea8c5444d..5a64d5ea7 100644 --- a/internal/clients/vault_cluster.go +++ b/internal/clients/vault_cluster.go @@ -273,3 +273,69 @@ func DeleteVaultPathsFilter(ctx context.Context, client *Client, loc *sharedmode return deleteResp.Payload, nil } + +// AddPlugin will make a call to the Vault service to add a plugin to a Vault cluster +func AddPlugin(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, clusterID string, + request *vaultmodels.HashicorpCloudVault20201125AddPluginRequest) (vaultmodels.HashicorpCloudVault20201125AddPluginResponse, error) { + + region := &sharedmodels.HashicorpCloudLocationRegion{} + if loc.Region != nil { + region = loc.Region + } + locInternal := &vaultmodels.HashicorpCloudInternalLocationLocation{ + OrganizationID: loc.OrganizationID, + ProjectID: loc.ProjectID, + Region: &vaultmodels.HashicorpCloudInternalLocationRegion{ + Provider: region.Provider, + Region: region.Region, + }, + } + request.Location = locInternal + request.ClusterID = clusterID + addPluginParams := vault_service.NewAddPluginParams() + addPluginParams.Context = ctx + addPluginParams.ClusterID = clusterID + addPluginParams.LocationProjectID = loc.ProjectID + addPluginParams.LocationOrganizationID = loc.OrganizationID + addPluginParams.Body = request + + addPluginResp, err := client.Vault.AddPlugin(addPluginParams, nil) + if err != nil { + return nil, err + } + + return addPluginResp.Payload, nil +} + +// DeletePlugin will make a call to the Vault service to remove a plugin to a Vault cluster +func DeletePlugin(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, clusterID string, + request *vaultmodels.HashicorpCloudVault20201125DeletePluginRequest) (vaultmodels.HashicorpCloudVault20201125DeletePluginResponse, error) { + + region := &sharedmodels.HashicorpCloudLocationRegion{} + if loc.Region != nil { + region = loc.Region + } + locInternal := &vaultmodels.HashicorpCloudInternalLocationLocation{ + OrganizationID: loc.OrganizationID, + ProjectID: loc.ProjectID, + Region: &vaultmodels.HashicorpCloudInternalLocationRegion{ + Provider: region.Provider, + Region: region.Region, + }, + } + request.Location = locInternal + request.ClusterID = clusterID + delPluginPluginParams := vault_service.NewDeletePluginParams() + delPluginPluginParams.Context = ctx + delPluginPluginParams.ClusterID = clusterID + delPluginPluginParams.LocationProjectID = loc.ProjectID + delPluginPluginParams.LocationOrganizationID = loc.OrganizationID + delPluginPluginParams.Body = request + + delPluginResp, err := client.Vault.DeletePlugin(delPluginPluginParams, nil) + if err != nil { + return nil, err + } + + return delPluginResp.Payload, nil +} diff --git a/internal/provider/resource_vault_cluster.go b/internal/provider/resource_vault_cluster.go index 610aeb9fe..6b49b5271 100644 --- a/internal/provider/resource_vault_cluster.go +++ b/internal/provider/resource_vault_cluster.go @@ -276,6 +276,33 @@ If a project is not configured in the HCP Provider config block, the oldest proj }, }, }, + "vault_plugin": { + Description: "The external plugins that are to be installed on the vault cluster", + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "plugin_name": { + Description: "The name of the plugin - Valid options for plugin name - 'venafi-pki-backend'", + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: func(_, old, new string, _ *schema.ResourceData) bool { + return strings.EqualFold(old, new) + }, + }, + "plugin_type": { + Description: "The type of the plugin - Valid options for plugin type - 'SECRET', 'AUTH', 'DATABASE'", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateVaultPluginType, + DiffSuppressFunc: func(_, old, new string, _ *schema.ResourceData) bool { + return strings.EqualFold(old, new) + }, + }, + }, + }, + }, "vault_public_endpoint_url": { Description: "The public URL for the Vault cluster. This will be empty if `public_endpoint` is `false`.", Type: schema.TypeString, @@ -334,6 +361,10 @@ func resourceVaultClusterCreate(ctx context.Context, d *schema.ResourceData, met if diagErr != nil { return diagErr } + pluginConfig, diagErr := getPluginConfig(d) + if diagErr != nil { + return diagErr + } // Use the hvn to get provider and region. hvn, err := clients.GetHvnByID(ctx, client, loc, hvnID) @@ -506,7 +537,6 @@ func resourceVaultClusterCreate(ctx context.Context, d *schema.ResourceData, met // If we pass the major version upgrade configuration we need to update it after the creation of the cluster, // since the cluster is created by default to automatic upgrade if mvuConfig != nil { - _, err := clients.UpdateVaultMajorVersionUpgradeConfig(ctx, client, clusterLocationShared, payload.ClusterID, mvuConfig) if err != nil { return diag.Errorf("error updating Vault cluster major version upgrade config (%s): %v", payload.ClusterID, err) @@ -519,6 +549,14 @@ func resourceVaultClusterCreate(ctx context.Context, d *schema.ResourceData, met } } + // add plugins to cluster after cluster creation + for _, plugin := range pluginConfig { + _, err := clients.AddPlugin(ctx, client, clusterLocationShared, payload.ClusterID, plugin) + if err != nil { + return diag.Errorf("error adding plugin (%s) to Vault cluster (%s): %v", plugin.PluginName, payload.ClusterID, err) + } + } + if err := setVaultClusterResourceData(d, cluster); err != nil { return diag.FromErr(err) } @@ -610,6 +648,11 @@ func resourceVaultClusterUpdate(ctx context.Context, d *schema.ResourceData, met return diagErr } + newPluginConfig, diagErr := getPluginConfig(d) + if diagErr != nil { + return diagErr + } + if d.HasChange("tier") || d.HasChange("metrics_config") || d.HasChange("audit_log_config") { diagErr := updateVaultClusterConfig(ctx, client, d, cluster, clusterID) if diagErr != nil { @@ -680,6 +723,48 @@ func resourceVaultClusterUpdate(ctx context.Context, d *schema.ResourceData, met return diag.Errorf("unable to retrieve Vault cluster (%s): %v", clusterID, err) } + // on update, delete plugins that were removed from plugin config. Add all plugins in new config + if d.HasChange("vault_plugin") { + old, _ := d.GetChange("vault_plugin") + + oldPlugins := old.([]interface{}) + + for _, oldP := range oldPlugins { + config, ok := oldP.(map[string]interface{}) + if !ok { + return diag.Errorf("could not parse old plugin config: %v", err) + } + + pluginName := config["plugin_name"].(string) + pluginType := config["plugin_type"].(string) + + // if plugin in old config is not found in the new config, delete the plugin + found := false + for _, plugin := range newPluginConfig { + if strings.EqualFold(pluginName, plugin.PluginName) && strings.EqualFold(pluginType, plugin.PluginType) { + found = true + } + } + + if !found { + req := &vaultmodels.HashicorpCloudVault20201125DeletePluginRequest{PluginName: pluginName, PluginType: pluginType} + _, err := clients.DeletePlugin(ctx, client, clusterLocationShared, clusterID, req) + if err != nil { + return diag.Errorf("error deleting plugin (%s) on Vault cluster (%s): %v", pluginName, clusterID, err) + } + } + + } + + // add all plugins in new plugin config + for _, plugin := range newPluginConfig { + _, err := clients.AddPlugin(ctx, client, clusterLocationShared, clusterID, plugin) + if err != nil { + return diag.Errorf("error adding plugin (%s) to Vault cluster (%s): %v", plugin.PluginName, clusterID, err) + } + } + } + if err := setVaultClusterResourceData(d, cluster); err != nil { return diag.FromErr(err) } @@ -1150,6 +1235,42 @@ func flattenMajorVersionUpgradeConfig(config *vaultmodels.HashicorpCloudVault202 return []interface{}{configMap} } +func getPluginConfig(d *schema.ResourceData) ([]*vaultmodels.HashicorpCloudVault20201125AddPluginRequest, diag.Diagnostics) { + if !d.HasChange("vault_plugin") { + return nil, nil + } + configParam, ok := d.GetOk("vault_plugin") + if !ok { + return nil, nil + } + + configIfaceArr, ok := configParam.([]interface{}) + if !ok || len(configIfaceArr) == 0 { + return nil, nil + } + + if !ok || len(configIfaceArr) == 0 { + return nil, nil + } + + var pluginConfigs []*vaultmodels.HashicorpCloudVault20201125AddPluginRequest + + for _, plugin := range configIfaceArr { + config, ok := plugin.(map[string]interface{}) + if !ok { + return nil, nil + } + pluginName := config["plugin_name"].(string) + pluginType := config["plugin_type"].(string) + pluginConfigs = append(pluginConfigs, &vaultmodels.HashicorpCloudVault20201125AddPluginRequest{ + PluginName: pluginName, + PluginType: pluginType, + }) + + } + return pluginConfigs, nil +} + func resourceVaultClusterImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { // with multi-projects, import arguments must become dynamic: // use explicit project ID with terraform import: diff --git a/internal/provider/resource_vault_cluster_const_test.go b/internal/provider/resource_vault_cluster_const_test.go index b6dae1070..e870e8ba1 100644 --- a/internal/provider/resource_vault_cluster_const_test.go +++ b/internal/provider/resource_vault_cluster_const_test.go @@ -48,6 +48,14 @@ resource "hcp_vault_cluster" "test" { major_version_upgrade_config { upgrade_type = "MANUAL" } + vault_plugin { + plugin_type = "SECRET" + plugin_name = "venafi-pki-backend" + } + vault_plugin { + plugin_type = "DATABASE" + plugin_name = "vault-plugin-database-oracle" + } } ` @@ -64,6 +72,10 @@ resource "hcp_vault_cluster" "test" { maintenance_window_day = "WEDNESDAY" maintenance_window_time = "WINDOW_12AM_4AM" } + vault_plugin { + plugin_type = "SECRET" + plugin_name = "venafi-pki-backend" + } } ` diff --git a/internal/provider/resource_vault_cluster_test.go b/internal/provider/resource_vault_cluster_test.go index d80ffd8b8..0d97cb8ec 100644 --- a/internal/provider/resource_vault_cluster_test.go +++ b/internal/provider/resource_vault_cluster_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" ) @@ -156,7 +157,7 @@ func testAccCheckVaultClusterDestroy(s *terraform.State) error { func awsTestSteps(t *testing.T, inp inputT) []resource.TestStep { in := &inp return []resource.TestStep{ - createClusteAndTestAdminTokenGeneration(t, in), + createClusterAndTestAdminTokenGeneration(t, in), importResourcesInTFState(t, in), tfApply(t, in), testTFDataSources(t, in), @@ -169,7 +170,7 @@ func awsTestSteps(t *testing.T, inp inputT) []resource.TestStep { func azureTestSteps(t *testing.T, inp inputT) []resource.TestStep { in := &inp return []resource.TestStep{ - createClusteAndTestAdminTokenGeneration(t, in), + createClusterAndTestAdminTokenGeneration(t, in), importResourcesInTFState(t, in), tfApply(t, in), testTFDataSources(t, in), @@ -178,7 +179,7 @@ func azureTestSteps(t *testing.T, inp inputT) []resource.TestStep { } // This step tests Vault cluster and admin token resource creation. -func createClusteAndTestAdminTokenGeneration(t *testing.T, in *inputT) resource.TestStep { +func createClusterAndTestAdminTokenGeneration(t *testing.T, in *inputT) resource.TestStep { return resource.TestStep{ Config: testConfig(in.tf), Check: resource.ComposeTestCheckFunc( @@ -289,7 +290,7 @@ func updateClusterTier(t *testing.T, in *inputT) resource.TestStep { } } -// This step verifies the successful update of "public_endpoint", "audit_log", "metrics" and MVU config +// This step verifies the successful update of "public_endpoint", "audit_log", "metrics", MVU config, and plugin config func updateVaultPublicEndpointObservabilityDataAndMVU(t *testing.T, in *inputT) resource.TestStep { newIn := *in newIn.PublicEndpoint = "true" @@ -307,6 +308,8 @@ func updateVaultPublicEndpointObservabilityDataAndMVU(t *testing.T, in *inputT) resource.TestCheckResourceAttrSet(in.VaultClusterResourceName, "audit_log_config.0.datadog_api_key"), resource.TestCheckResourceAttr(in.VaultClusterResourceName, "audit_log_config.0.datadog_region", "us1"), resource.TestCheckResourceAttr(in.VaultClusterResourceName, "major_version_upgrade_config.0.upgrade_type", "MANUAL"), + resource.TestCheckResourceAttr(in.VaultClusterResourceName, "vault_plugin.0.plugin_name", "venafi-pki-backend"), + resource.TestCheckResourceAttr(in.VaultClusterResourceName, "vault_plugin.0.plugin_type", "SECRET"), ), } } diff --git a/internal/provider/validators.go b/internal/provider/validators.go index cf05e8041..cd2b39fea 100644 --- a/internal/provider/validators.go +++ b/internal/provider/validators.go @@ -441,3 +441,22 @@ func validateBoundaryPassword(v interface{}, path cty.Path) diag.Diagnostics { return diagnostics } + +func validateVaultPluginType(v interface{}, path cty.Path) diag.Diagnostics { + var diagnostics diag.Diagnostics + + err := vaultmodels.HashicorpCloudVault20201125PluginType(strings.ToUpper(v.(string))).Validate(strfmt.Default) + if err != nil { + enumList := regexp.MustCompile(`\[.*\]`).FindString(err.Error()) + expectedEnumList := strings.ToLower(enumList) + msg := fmt.Sprintf("expected '%v' to be one of: %v", v, expectedEnumList) + diagnostics = append(diagnostics, diag.Diagnostic{ + Severity: diag.Error, + Summary: msg, + Detail: msg + " (value is case-insensitive).", + AttributePath: path, + }) + } + + return diagnostics +} From 8416d13bd7c424ecdf976eaa91ba1b254002da6e Mon Sep 17 00:00:00 2001 From: Michael Blaum Date: Mon, 7 Aug 2023 15:16:24 -0400 Subject: [PATCH 2/7] remove test case for multiple plugins --- internal/provider/resource_vault_cluster_const_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/provider/resource_vault_cluster_const_test.go b/internal/provider/resource_vault_cluster_const_test.go index e870e8ba1..dcc4d9136 100644 --- a/internal/provider/resource_vault_cluster_const_test.go +++ b/internal/provider/resource_vault_cluster_const_test.go @@ -52,10 +52,6 @@ resource "hcp_vault_cluster" "test" { plugin_type = "SECRET" plugin_name = "venafi-pki-backend" } - vault_plugin { - plugin_type = "DATABASE" - plugin_name = "vault-plugin-database-oracle" - } } ` @@ -72,10 +68,6 @@ resource "hcp_vault_cluster" "test" { maintenance_window_day = "WEDNESDAY" maintenance_window_time = "WINDOW_12AM_4AM" } - vault_plugin { - plugin_type = "SECRET" - plugin_name = "venafi-pki-backend" - } } ` From 629843b8a763ba5a685257f3f0f65e04f16b4141 Mon Sep 17 00:00:00 2001 From: Michael Blaum Date: Mon, 7 Aug 2023 15:27:12 -0400 Subject: [PATCH 3/7] changelog entry --- .changelog/575.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/575.txt diff --git a/.changelog/575.txt b/.changelog/575.txt new file mode 100644 index 000000000..dc306ee4b --- /dev/null +++ b/.changelog/575.txt @@ -0,0 +1,3 @@ +```release-note:improvement +Add `vault_plugin` resource as optional subresource for `hcp_vault_cluster` +``` From ced527ee222232d1195ea0e32cdcffd875b4beea Mon Sep 17 00:00:00 2001 From: Michael Blaum Date: Mon, 7 Aug 2023 15:40:59 -0400 Subject: [PATCH 4/7] go generate --- docs/resources/vault_cluster.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/resources/vault_cluster.md b/docs/resources/vault_cluster.md index 18ca644ee..3ba665b13 100644 --- a/docs/resources/vault_cluster.md +++ b/docs/resources/vault_cluster.md @@ -61,6 +61,7 @@ If a project is not configured in the HCP Provider config block, the oldest proj - `public_endpoint` (Boolean) Denotes that the cluster has a public endpoint. Defaults to false. - `tier` (String) Tier of the HCP Vault cluster. Valid options for tiers - `dev`, `starter_small`, `standard_small`, `standard_medium`, `standard_large`, `plus_small`, `plus_medium`, `plus_large`. See [pricing information](https://www.hashicorp.com/products/vault/pricing). Changing a cluster's size or tier is only available to admins. See [Scale a cluster](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/guides/vault-scaling). - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `vault_plugin` (Block List) The external plugins that are to be installed on the vault cluster (see [below for nested schema](#nestedblock--vault_plugin)) ### Read-Only @@ -127,6 +128,15 @@ Optional: - `delete` (String) - `update` (String) + + +### Nested Schema for `vault_plugin` + +Required: + +- `plugin_name` (String) The name of the plugin - Valid options for plugin name - 'venafi-pki-backend' +- `plugin_type` (String) The type of the plugin - Valid options for plugin type - 'SECRET', 'AUTH', 'DATABASE' + -> **Note:** When establishing performance replication links between clusters in different HVNs, an HVN peering connection is required. This can be defined explicitly using an [`hcp_hvn_peering_connection`](hvn_peering_connection.md), or HCP will create the connection automatically (peering connections can be imported after creation using [terraform import](https://www.terraform.io/cli/import)). Note HVN peering [CIDR block requirements](https://cloud.hashicorp.com/docs/hcp/network/routes#cidr-block-requirements). ## Import From 325a75dbcb6a84ce19423576533e7412f27fa807 Mon Sep 17 00:00:00 2001 From: Michael Blaum Date: Thu, 10 Aug 2023 18:28:33 -0400 Subject: [PATCH 5/7] set plugins for data source & do validation on plugin names for terraform updates --- docs/data-sources/vault_cluster.md | 10 +++ docs/resources/vault_cluster.md | 2 +- internal/clients/vault_cluster.go | 39 ++++++++--- .../provider/data_source_vault_cluster.go | 28 +++++++- internal/provider/resource_vault_cluster.go | 68 ++++++++++++++++--- internal/provider/validators.go | 16 +++++ 6 files changed, 142 insertions(+), 21 deletions(-) diff --git a/docs/data-sources/vault_cluster.md b/docs/data-sources/vault_cluster.md index 03e16b1cf..478458924 100644 --- a/docs/data-sources/vault_cluster.md +++ b/docs/data-sources/vault_cluster.md @@ -31,6 +31,7 @@ data "hcp_vault_cluster" "example" { If not specified, the project specified in the HCP Provider config block will be used, if configured. If a project is not configured in the HCP Provider config block, the oldest project in the organization will be used. - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `vault_plugin` (Block List) The external plugins to install on the vault cluster (see [below for nested schema](#nestedblock--vault_plugin)) ### Read-Only @@ -63,6 +64,15 @@ Optional: - `default` (String) + +### Nested Schema for `vault_plugin` + +Required: + +- `plugin_name` (String) The name of the plugin +- `plugin_type` (String) The type of the plugin + + ### Nested Schema for `audit_log_config` diff --git a/docs/resources/vault_cluster.md b/docs/resources/vault_cluster.md index 3ba665b13..b3f0d8cd4 100644 --- a/docs/resources/vault_cluster.md +++ b/docs/resources/vault_cluster.md @@ -61,7 +61,7 @@ If a project is not configured in the HCP Provider config block, the oldest proj - `public_endpoint` (Boolean) Denotes that the cluster has a public endpoint. Defaults to false. - `tier` (String) Tier of the HCP Vault cluster. Valid options for tiers - `dev`, `starter_small`, `standard_small`, `standard_medium`, `standard_large`, `plus_small`, `plus_medium`, `plus_large`. See [pricing information](https://www.hashicorp.com/products/vault/pricing). Changing a cluster's size or tier is only available to admins. See [Scale a cluster](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/guides/vault-scaling). - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) -- `vault_plugin` (Block List) The external plugins that are to be installed on the vault cluster (see [below for nested schema](#nestedblock--vault_plugin)) +- `vault_plugin` (Block List) The external plugins to install on the vault cluster (see [below for nested schema](#nestedblock--vault_plugin)) ### Read-Only diff --git a/internal/clients/vault_cluster.go b/internal/clients/vault_cluster.go index 5a64d5ea7..7080ddbc1 100644 --- a/internal/clients/vault_cluster.go +++ b/internal/clients/vault_cluster.go @@ -325,17 +325,40 @@ func DeletePlugin(ctx context.Context, client *Client, loc *sharedmodels.Hashico } request.Location = locInternal request.ClusterID = clusterID - delPluginPluginParams := vault_service.NewDeletePluginParams() - delPluginPluginParams.Context = ctx - delPluginPluginParams.ClusterID = clusterID - delPluginPluginParams.LocationProjectID = loc.ProjectID - delPluginPluginParams.LocationOrganizationID = loc.OrganizationID - delPluginPluginParams.Body = request - - delPluginResp, err := client.Vault.DeletePlugin(delPluginPluginParams, nil) + delPluginParams := vault_service.NewDeletePluginParams() + delPluginParams.Context = ctx + delPluginParams.ClusterID = clusterID + delPluginParams.LocationProjectID = loc.ProjectID + delPluginParams.LocationOrganizationID = loc.OrganizationID + delPluginParams.Body = request + + delPluginResp, err := client.Vault.DeletePlugin(delPluginParams, nil) if err != nil { return nil, err } return delPluginResp.Payload, nil } + +// ListPlugins will make a call to the Vault service plugin status api to get names of valid plugins +func ListPlugins(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, clusterID string) (*vaultmodels.HashicorpCloudVault20201125PluginRegistrationStatusResponse, error) { + region := &sharedmodels.HashicorpCloudLocationRegion{} + if loc.Region != nil { + region = loc.Region + } + + listPluginsParams := vault_service.NewPluginRegistrationStatusParams() + listPluginsParams.Context = ctx + listPluginsParams.ClusterID = clusterID + listPluginsParams.LocationProjectID = loc.ProjectID + listPluginsParams.LocationOrganizationID = loc.OrganizationID + listPluginsParams.LocationRegionProvider = ®ion.Provider + listPluginsParams.LocationRegionRegion = ®ion.Region + + listPluginsResp, err := client.Vault.PluginRegistrationStatus(listPluginsParams, nil) + if err != nil { + return nil, err + } + + return listPluginsResp.Payload, nil +} diff --git a/internal/provider/data_source_vault_cluster.go b/internal/provider/data_source_vault_cluster.go index 768a22b46..ae911fbb9 100644 --- a/internal/provider/data_source_vault_cluster.go +++ b/internal/provider/data_source_vault_cluster.go @@ -208,6 +208,26 @@ If a project is not configured in the HCP Provider config block, the oldest proj }, }, }, + "vault_plugin": { + Description: "The external plugins to install on the vault cluster", + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "plugin_name": { + Description: "The name of the plugin", + Type: schema.TypeString, + Required: true, + }, + "plugin_type": { + Description: "The type of the plugin", + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, }, } } @@ -242,8 +262,14 @@ func dataSourceVaultClusterRead(ctx context.Context, d *schema.ResourceData, met d.SetId(url) + plugins, err := clients.ListPlugins(ctx, client, loc, clusterID) + if err != nil { + log.Printf("[ERROR] Vault cluster (%s) failed to list plugins", clusterID) + return diag.FromErr(err) + } + // Cluster found, update resource data. - if err := setVaultClusterResourceData(d, cluster); err != nil { + if err := setVaultClusterResourceData(d, cluster, plugins.Plugins); err != nil { return diag.FromErr(err) } diff --git a/internal/provider/resource_vault_cluster.go b/internal/provider/resource_vault_cluster.go index 6b49b5271..a243a503d 100644 --- a/internal/provider/resource_vault_cluster.go +++ b/internal/provider/resource_vault_cluster.go @@ -277,7 +277,7 @@ If a project is not configured in the HCP Provider config block, the oldest proj }, }, "vault_plugin": { - Description: "The external plugins that are to be installed on the vault cluster", + Description: "The external plugins to install on the vault cluster", Type: schema.TypeList, Optional: true, Computed: true, @@ -333,7 +333,6 @@ If a project is not configured in the HCP Provider config block, the oldest proj } func resourceVaultClusterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*clients.Client) clusterID := d.Get("cluster_id").(string) @@ -361,7 +360,8 @@ func resourceVaultClusterCreate(ctx context.Context, d *schema.ResourceData, met if diagErr != nil { return diagErr } - pluginConfig, diagErr := getPluginConfig(d) + + pluginConfig, diagErr := getPluginConfig(d, nil) if diagErr != nil { return diagErr } @@ -557,7 +557,13 @@ func resourceVaultClusterCreate(ctx context.Context, d *schema.ResourceData, met } } - if err := setVaultClusterResourceData(d, cluster); err != nil { + plugins, err := clients.ListPlugins(ctx, client, loc, clusterID) + if err != nil { + log.Printf("[ERROR] Vault cluster (%s) failed to list plugins", clusterID) + return diag.FromErr(err) + } + + if err := setVaultClusterResourceData(d, cluster, plugins.Plugins); err != nil { return diag.FromErr(err) } @@ -595,8 +601,14 @@ func resourceVaultClusterRead(ctx context.Context, d *schema.ResourceData, meta return nil } + plugins, err := clients.ListPlugins(ctx, client, loc, clusterID) + if err != nil { + log.Printf("[ERROR] Vault cluster (%s) failed to list plugins", clusterID) + return diag.FromErr(err) + } + // Cluster found, update resource data. - if err := setVaultClusterResourceData(d, cluster); err != nil { + if err := setVaultClusterResourceData(d, cluster, plugins.Plugins); err != nil { return diag.FromErr(err) } @@ -648,7 +660,14 @@ func resourceVaultClusterUpdate(ctx context.Context, d *schema.ResourceData, met return diagErr } - newPluginConfig, diagErr := getPluginConfig(d) + // get plugins for plugin-name validation in getPluginConfig + plugins, err := clients.ListPlugins(ctx, client, loc, clusterID) + if err != nil { + log.Printf("[ERROR] Vault cluster (%s) failed to list plugins", clusterID) + return diag.FromErr(err) + } + + newPluginConfig, diagErr := getPluginConfig(d, plugins.Plugins) if diagErr != nil { return diagErr } @@ -765,7 +784,13 @@ func resourceVaultClusterUpdate(ctx context.Context, d *schema.ResourceData, met } } - if err := setVaultClusterResourceData(d, cluster); err != nil { + plugins, err = clients.ListPlugins(ctx, client, loc, clusterID) + if err != nil { + log.Printf("[ERROR] Vault cluster (%s) failed to list plugins", clusterID) + return diag.FromErr(err) + } + + if err := setVaultClusterResourceData(d, cluster, plugins.Plugins); err != nil { return diag.FromErr(err) } @@ -892,8 +917,7 @@ func getClusterTier(d *schema.ResourceData) *string { } // setVaultClusterResourceData sets the KV pairs of the Vault cluster resource schema. -func setVaultClusterResourceData(d *schema.ResourceData, cluster *vaultmodels.HashicorpCloudVault20201125Cluster) error { - +func setVaultClusterResourceData(d *schema.ResourceData, cluster *vaultmodels.HashicorpCloudVault20201125Cluster, plugins []*vaultmodels.HashicorpCloudVault20201125PluginRegistrationStatus) error { if err := d.Set("cluster_id", cluster.ID); err != nil { return err } @@ -1022,6 +1046,20 @@ func setVaultClusterResourceData(d *schema.ResourceData, cluster *vaultmodels.Ha } } + var pluginConfig []map[string]any + for _, plugin := range plugins { + if plugin.IsRegistered { + pluginMap := map[string]any{} + pluginMap["plugin_name"] = plugin.PluginName + pluginMap["plugin_type"] = plugin.PluginType + pluginConfig = append(pluginConfig, pluginMap) + } + if err = d.Set("vault_plugin", pluginConfig); err != nil { + return err + } + + } + return nil } @@ -1235,7 +1273,7 @@ func flattenMajorVersionUpgradeConfig(config *vaultmodels.HashicorpCloudVault202 return []interface{}{configMap} } -func getPluginConfig(d *schema.ResourceData) ([]*vaultmodels.HashicorpCloudVault20201125AddPluginRequest, diag.Diagnostics) { +func getPluginConfig(d *schema.ResourceData, plugins []*vaultmodels.HashicorpCloudVault20201125PluginRegistrationStatus) ([]*vaultmodels.HashicorpCloudVault20201125AddPluginRequest, diag.Diagnostics) { if !d.HasChange("vault_plugin") { return nil, nil } @@ -1262,12 +1300,20 @@ func getPluginConfig(d *schema.ResourceData) ([]*vaultmodels.HashicorpCloudVault } pluginName := config["plugin_name"].(string) pluginType := config["plugin_type"].(string) + + if plugins != nil { + err := validateVaultPluginName(pluginName, pluginType, plugins) + if err != nil { + return nil, err + } + } + pluginConfigs = append(pluginConfigs, &vaultmodels.HashicorpCloudVault20201125AddPluginRequest{ PluginName: pluginName, PluginType: pluginType, }) - } + return pluginConfigs, nil } diff --git a/internal/provider/validators.go b/internal/provider/validators.go index cd2b39fea..a0eac0a59 100644 --- a/internal/provider/validators.go +++ b/internal/provider/validators.go @@ -460,3 +460,19 @@ func validateVaultPluginType(v interface{}, path cty.Path) diag.Diagnostics { return diagnostics } + +func validateVaultPluginName(pluginName string, pluginType string, plugins []*vaultmodels.HashicorpCloudVault20201125PluginRegistrationStatus) diag.Diagnostics { + var found bool + for _, plugin := range plugins { + if strings.EqualFold(pluginName, plugin.PluginName) && strings.EqualFold(pluginType, string(*plugin.PluginType)) { + found = true + break + } + } + + if !found { + return diag.Errorf(fmt.Sprintf("plugin of plugin name: %s and plugin type: %s is not supported for installation by HCP Vault", pluginName, pluginType)) + } + + return nil +} From 0ca8f12f87682bfe07a25dab7c0e2333f6597337 Mon Sep 17 00:00:00 2001 From: Michael Blaum Date: Fri, 11 Aug 2023 10:14:10 -0400 Subject: [PATCH 6/7] remove invalid option from enum list --- internal/provider/validators.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/provider/validators.go b/internal/provider/validators.go index a0eac0a59..bc60c4b70 100644 --- a/internal/provider/validators.go +++ b/internal/provider/validators.go @@ -449,6 +449,12 @@ func validateVaultPluginType(v interface{}, path cty.Path) diag.Diagnostics { if err != nil { enumList := regexp.MustCompile(`\[.*\]`).FindString(err.Error()) expectedEnumList := strings.ToLower(enumList) + // Remove invalid option from allowed list in error message + expectedEnumList = strings.ReplaceAll( + expectedEnumList, + strings.ToLower(string(vaultmodels.HashicorpCloudVault20201125PluginTypePLUGINTYPEINVALID)), + "", + ) msg := fmt.Sprintf("expected '%v' to be one of: %v", v, expectedEnumList) diagnostics = append(diagnostics, diag.Diagnostic{ Severity: diag.Error, From 5e4dcb8ae276022d20c1d492d039a531050e7c2f Mon Sep 17 00:00:00 2001 From: Michael Blaum Date: Mon, 14 Aug 2023 11:52:57 -0400 Subject: [PATCH 7/7] update comments & tests --- internal/clients/vault_cluster.go | 2 +- internal/provider/resource_vault_cluster.go | 6 +++++- internal/provider/resource_vault_cluster_test.go | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/clients/vault_cluster.go b/internal/clients/vault_cluster.go index 7080ddbc1..f8654d272 100644 --- a/internal/clients/vault_cluster.go +++ b/internal/clients/vault_cluster.go @@ -340,7 +340,7 @@ func DeletePlugin(ctx context.Context, client *Client, loc *sharedmodels.Hashico return delPluginResp.Payload, nil } -// ListPlugins will make a call to the Vault service plugin status api to get names of valid plugins +// ListPlugins will make a call to the Vault service plugin status api to get all available plugins for the cluster. func ListPlugins(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, clusterID string) (*vaultmodels.HashicorpCloudVault20201125PluginRegistrationStatusResponse, error) { region := &sharedmodels.HashicorpCloudLocationRegion{} if loc.Region != nil { diff --git a/internal/provider/resource_vault_cluster.go b/internal/provider/resource_vault_cluster.go index a243a503d..3ef09a69c 100644 --- a/internal/provider/resource_vault_cluster.go +++ b/internal/provider/resource_vault_cluster.go @@ -276,6 +276,10 @@ If a project is not configured in the HCP Provider config block, the oldest proj }, }, }, + // vault_plugin is a terraform resource used to specify a plugin for installation. + // + // plugin_name is only validated on updates because the PluginStatus API to list the valid available plugin names requires a created cluster. + // plugin_type is validated on create & update because there is a static list for valid plugint types. "vault_plugin": { Description: "The external plugins to install on the vault cluster", Type: schema.TypeList, @@ -660,7 +664,7 @@ func resourceVaultClusterUpdate(ctx context.Context, d *schema.ResourceData, met return diagErr } - // get plugins for plugin-name validation in getPluginConfig + // get list of plugins for plugin-name validation in getPluginConfig plugins, err := clients.ListPlugins(ctx, client, loc, clusterID) if err != nil { log.Printf("[ERROR] Vault cluster (%s) failed to list plugins", clusterID) diff --git a/internal/provider/resource_vault_cluster_test.go b/internal/provider/resource_vault_cluster_test.go index 0d97cb8ec..4e0826f57 100644 --- a/internal/provider/resource_vault_cluster_test.go +++ b/internal/provider/resource_vault_cluster_test.go @@ -286,6 +286,8 @@ func updateClusterTier(t *testing.T, in *inputT) resource.TestStep { resource.TestCheckResourceAttr(vaultClusterResourceName, "major_version_upgrade_config.0.upgrade_type", "SCHEDULED"), resource.TestCheckResourceAttr(vaultClusterResourceName, "major_version_upgrade_config.0.maintenance_window_day", "WEDNESDAY"), resource.TestCheckResourceAttr(vaultClusterResourceName, "major_version_upgrade_config.0.maintenance_window_time", "WINDOW_12AM_4AM"), + resource.TestCheckNoResourceAttr(in.VaultClusterResourceName, "vault_plugin.0"), + resource.TestCheckNoResourceAttr(in.VaultClusterResourceName, "vault_plugin.0"), ), } } @@ -333,6 +335,8 @@ func updateTierPublicEndpointAndRemoveObservabilityData(t *testing.T, in *inputT resource.TestCheckResourceAttr(in.VaultClusterResourceName, "major_version_upgrade_config.0.upgrade_type", "SCHEDULED"), resource.TestCheckResourceAttr(in.VaultClusterResourceName, "major_version_upgrade_config.0.maintenance_window_day", "WEDNESDAY"), resource.TestCheckResourceAttr(in.VaultClusterResourceName, "major_version_upgrade_config.0.maintenance_window_time", "WINDOW_12AM_4AM"), + resource.TestCheckNoResourceAttr(in.VaultClusterResourceName, "vault_plugin.0"), + resource.TestCheckNoResourceAttr(in.VaultClusterResourceName, "vault_plugin.0"), ), } }