Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hcp_vault_cluster resource changes for adding vault plugins #575

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/575.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
Add `vault_plugin` resource as optional subresource for `hcp_vault_cluster`
```
10 changes: 10 additions & 0 deletions docs/resources/vault_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
hashiblaum marked this conversation as resolved.
Show resolved Hide resolved

### Read-Only

Expand Down Expand Up @@ -127,6 +128,15 @@ Optional:
- `delete` (String)
- `update` (String)


<a id="nestedblock--vault_plugin"></a>
### Nested Schema for `vault_plugin`

Required:

- `plugin_name` (String) The name of the plugin - Valid options for plugin name - 'venafi-pki-backend'
himran92 marked this conversation as resolved.
Show resolved Hide resolved
- `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
Expand Down
66 changes: 66 additions & 0 deletions internal/clients/vault_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
123 changes: 122 additions & 1 deletion internal/provider/resource_vault_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
hashiblaum marked this conversation as resolved.
Show resolved Hide resolved
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'",
hashiblaum marked this conversation as resolved.
Show resolved Hide resolved
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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding comment to explain why vault plugin type is validated on both create & update & plugin name is only validated on update

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also have a relative question in another comment on how to do plugin name validation on create :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can definitely update comments to make this more clear

answered question in other thread : )

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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
hashiblaum marked this conversation as resolved.
Show resolved Hide resolved
return diag.FromErr(err)
}
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions internal/provider/resource_vault_cluster_const_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ resource "hcp_vault_cluster" "test" {
major_version_upgrade_config {
upgrade_type = "MANUAL"
}
vault_plugin {
plugin_type = "SECRET"
plugin_name = "venafi-pki-backend"
}
}
`

Expand Down
11 changes: 7 additions & 4 deletions internal/provider/resource_vault_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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(
Expand Down Expand Up @@ -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"
Expand All @@ -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"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed we're only asserting on the vault_plugin field on the second update. Could we add more assertions for the other updates and this one?

Update 1: config 1 (one plugin added)
Update 2: config 2 (one new plugin added. idempotent plugin/add api called twice)
Update 3: config 1 (one plugin removed. )

Like on update 1 that the first plugin is added, update 2 the second plugin is added (I think rn it's only asserting on the venafi SECRET one and not the oracle DATABASE), and update 3 that resource.TestCheckNoResourceAttr on the vault_plugin that was removed (similar to how we do it for metrics/audit logs when they are removed) + the one that wasn't removed is still set.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so that test case isn't included yet because there is only 1 customer managed plugin to work with. I did some hacky things to test 2 of them. Once more plugins are added, we can update the tests.

hafsa had the same confusion, sorry if this wasn't clear in pr description

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh gotcha. Suggestion: Could we maybe have update 3 use a config with no plugins at all and then assert resource.TestCheckNoResourceAttr so then acceptance tests run by anyone will test removal without needing the extra setup you did?

),
}
}
Expand Down
19 changes: 19 additions & 0 deletions internal/provider/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
hashiblaum marked this conversation as resolved.
Show resolved Hide resolved
diagnostics = append(diagnostics, diag.Diagnostic{
Severity: diag.Error,
Summary: msg,
Detail: msg + " (value is case-insensitive).",
AttributePath: path,
})
}

return diagnostics
}