diff --git a/README.md b/README.md index d3b957527..be627f94c 100644 --- a/README.md +++ b/README.md @@ -69,4 +69,10 @@ resource "aws_vpc_peering_connection_accepter" "peer" { vpc_peering_connection_id = hcp_aws_network_peering.example.provider_peering_id auto_accept = true } + +// Create a Vault cluster within the HVN. +resource "hcp_vault_cluster" "example" { + cluster_id = "vault-cluster" + hvn_id = hcp_hvn.example_hvn.hvn_id +} ``` diff --git a/docs/data-sources/vault_cluster.md b/docs/data-sources/vault_cluster.md new file mode 100644 index 000000000..2d8cb5b91 --- /dev/null +++ b/docs/data-sources/vault_cluster.md @@ -0,0 +1,54 @@ +--- +page_title: "hcp_vault_cluster Data Source - terraform-provider-hcp" +subcategory: "" +description: |- + The cluster data source provides information about an existing HCP Vault cluster. +--- + +# Data Source `hcp_vault_cluster` + +The cluster data source provides information about an existing HCP Vault cluster. + +## Example Usage + +```terraform +data "hcp_vault_cluster" "example" { + cluster_id = var.cluster_id +} +``` + +## Schema + +### Required + +- **cluster_id** (String) The ID of the HCP Vault cluster. + +### Optional + +- **id** (String) The ID of this resource. +- **timeouts** (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-only + +- **cloud_provider** (String) The provider where the HCP Vault cluster is located. +- **created_at** (String) The time that the Vault cluster was created. +- **hvn_id** (String) The ID of the HVN this HCP Vault cluster is associated to. +- **min_vault_version** (String) The minimum Vault version to use when creating the cluster. If not specified, it is defaulted to the version that is currently recommended by HCP. +- **namespace** (String) The name of the customer namespace this HCP Vault cluster is located in. +- **organization_id** (String) The ID of the organization this HCP Vault cluster is located in. +- **project_id** (String) The ID of the project this HCP Vault cluster is located in. +- **public_endpoint** (Boolean) Denotes that the cluster has a public endpoint. Defaults to false. +- **region** (String) The region where the HCP Vault cluster is located. +- **tier** (String) The tier that the HCP Vault cluster will be provisioned as. Only 'development' is available at this time. +- **vault_private_endpoint_url** (String) The private URL for the Vault cluster. +- **vault_public_endpoint_url** (String) The public URL for the Vault cluster. This will be empty if `public_endpoint` is `false`. +- **vault_version** (String) The Vault version of the cluster. + + +### Nested Schema for `timeouts` + +Optional: + +- **default** (String) + + diff --git a/docs/guides/vault-admin-token.md b/docs/guides/vault-admin-token.md new file mode 100644 index 000000000..98a15342d --- /dev/null +++ b/docs/guides/vault-admin-token.md @@ -0,0 +1,23 @@ +--- +subcategory: "" +page_title: "Create a Vault cluster and admin token - HCP Provider" +description: |- + An example of creating a Vault cluster and admin token. +--- + +# Create a new Vault cluster and an admin token + +Once you have an HVN, HCP Vault enables you to quickly deploy a Vault Enterprise cluster in AWS across a variety of environments while offloading the operations burden to the SRE experts at HashiCorp. +The cluster's admin token grants its bearer administrator access to the Vault cluster. This admin token is valid for six hours. On subsequent reads after creation, +the resource will check if the admin token is close to expiration or expired and automatically refresh as needed. + +```terraform +resource "hcp_vault_cluster" "example_vault_cluster" { + hvn_id = hcp_hvn.example_hvn.hvn_id + cluster_id = "hcp-tf-example-vault-cluster" +} + +resource "hcp_vault_cluster_admin_token" "example_vault_admin_token" { + cluster_id = hcp_vault_cluster.example_vault_cluster.cluster_id +} +``` diff --git a/docs/index.md b/docs/index.md index fcd8e80ba..347c93968 100644 --- a/docs/index.md +++ b/docs/index.md @@ -79,6 +79,12 @@ resource "hcp_consul_cluster" "example_secondary" { tier = "development" primary_link = hcp_consul_cluster.example.self_link } + +// Create a Vault cluster in the same region and cloud provider as the HVN +resource "hcp_vault_cluster" "example" { + cluster_id = "hcp-tf-example-vault-cluster" + hvn_id = hcp_hvn.example_hvn.hvn_id +} ``` ## Schema diff --git a/docs/resources/vault_cluster.md b/docs/resources/vault_cluster.md new file mode 100644 index 000000000..79da5ef43 --- /dev/null +++ b/docs/resources/vault_cluster.md @@ -0,0 +1,71 @@ +--- +page_title: "hcp_vault_cluster Resource - terraform-provider-hcp" +subcategory: "" +description: |- + The Vault cluster resource allows you to manage an HCP Vault cluster. +--- + +# Resource `hcp_vault_cluster` + +The Vault cluster resource allows you to manage an HCP Vault cluster. + +## Example Usage + +```terraform +resource "hcp_hvn" "example" { + hvn_id = "hvn" + cloud_provider = "aws" + region = "us-west-2" + cidr_block = "172.25.16.0/20" +} + +resource "hcp_vault_cluster" "example" { + cluster_id = "vault-cluster" + hvn_id = hcp_hvn.example.hvn_id +} +``` + +## Schema + +### Required + +- **cluster_id** (String) The ID of the HCP Vault cluster. +- **hvn_id** (String) The ID of the HVN this HCP Vault cluster is associated to. + +### Optional + +- **id** (String) The ID of this resource. +- **min_vault_version** (String) The minimum Vault version to use when creating the cluster. If not specified, it is defaulted to the version that is currently recommended by HCP. +- **public_endpoint** (Boolean) Denotes that the cluster has a public endpoint. Defaults to false. +- **timeouts** (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-only + +- **cloud_provider** (String) The provider where the HCP Vault cluster is located. +- **created_at** (String) The time that the Vault cluster was created. +- **namespace** (String) The name of the customer namespace this HCP Vault cluster is located in. +- **organization_id** (String) The ID of the organization this HCP Vault cluster is located in. +- **project_id** (String) The ID of the project this HCP Vault cluster is located in. +- **region** (String) The region where the HCP Vault cluster is located. +- **tier** (String) The tier that the HCP Vault cluster will be provisioned as. Only 'development' is available at this time. +- **vault_private_endpoint_url** (String) The private URL for the Vault cluster. +- **vault_public_endpoint_url** (String) The public URL for the Vault cluster. This will be empty if `public_endpoint` is `false`. +- **vault_version** (String) The Vault version of the cluster. + + +### Nested Schema for `timeouts` + +Optional: + +- **create** (String) +- **default** (String) +- **delete** (String) + +## Import + +Import is supported using the following syntax: + +```shell +# The import ID is {cluster_id} +terraform import hcp_vault_cluster.example vault-cluster +``` diff --git a/docs/resources/vault_cluster_admin_token.md b/docs/resources/vault_cluster_admin_token.md new file mode 100644 index 000000000..db907af54 --- /dev/null +++ b/docs/resources/vault_cluster_admin_token.md @@ -0,0 +1,56 @@ +--- +page_title: "hcp_vault_cluster_admin_token Resource - terraform-provider-hcp" +subcategory: "" +description: |- + The Vault cluster admin token resource generates an admin-level token for the HCP Vault cluster. +--- + +# Resource `hcp_vault_cluster_admin_token` + +~> **Important Security Notice** The admin token generated by this resource will +be stored *unencrypted* in your Terraform state file. **Use of this resource +for production deployments is *not* recommended**. Instead, generate +an admin token outside of Terraform and distribute it securely +to the system where Terraform will be run. + +The Vault cluster admin token resource generates an admin-level token for the HCP Vault cluster. + +This resource saves a single admin token per Vault cluster and auto-refreshes the token when it is about to expire. +Destroying this resource *does not* invalidate the admin token. + +~> **Known Issue** An admin token may be generated during a `terraform plan` if the current token is expiring. +Since the Plan phase does not save any state, the Apply phase saves a different generated token, and the token generated during Plan ends up orphaned. +It will expire in six hours. + +## Example Usage + +```terraform +resource "hcp_vault_cluster_admin_token" "example" { + cluster_id = "test-vault-cluster" +} +``` + +## Schema + +### Required + +- **cluster_id** (String) The ID of the HCP Vault cluster. + +### Optional + +- **id** (String) The ID of this resource. +- **timeouts** (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-only + +- **created_at** (String) The time that the admin token was created. +- **token** (String, Sensitive) The admin token of this HCP Vault cluster. + + +### Nested Schema for `timeouts` + +Optional: + +- **create** (String) +- **delete** (String) +- **read** (String) \ No newline at end of file diff --git a/examples/data-sources/hcp_vault_cluster/data-source.tf b/examples/data-sources/hcp_vault_cluster/data-source.tf new file mode 100644 index 000000000..1b723f3b8 --- /dev/null +++ b/examples/data-sources/hcp_vault_cluster/data-source.tf @@ -0,0 +1,3 @@ +data "hcp_vault_cluster" "example" { + cluster_id = var.cluster_id +} \ No newline at end of file diff --git a/examples/guides/vault_cluster_admin_token/main.tf b/examples/guides/vault_cluster_admin_token/main.tf new file mode 100644 index 000000000..79128826c --- /dev/null +++ b/examples/guides/vault_cluster_admin_token/main.tf @@ -0,0 +1,8 @@ +resource "hcp_vault_cluster" "example_vault_cluster" { + hvn_id = hcp_hvn.example_hvn.hvn_id + cluster_id = "hcp-tf-example-vault-cluster" +} + +resource "hcp_vault_cluster_admin_token" "example_vault_admin_token" { + cluster_id = hcp_vault_cluster.example_vault_cluster.cluster_id +} \ No newline at end of file diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 2863a8486..55ef87521 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -60,4 +60,10 @@ resource "hcp_consul_cluster" "example_secondary" { cluster_id = "hcp-tf-example-consul-cluster-secondary" tier = "development" primary_link = hcp_consul_cluster.example.self_link +} + +// Create a Vault cluster in the same region and cloud provider as the HVN +resource "hcp_vault_cluster" "example" { + cluster_id = "hcp-tf-example-vault-cluster" + hvn_id = hcp_hvn.example_hvn.hvn_id } \ No newline at end of file diff --git a/examples/resources/hcp_vault_cluster/import.sh b/examples/resources/hcp_vault_cluster/import.sh new file mode 100644 index 000000000..6dc62f3a9 --- /dev/null +++ b/examples/resources/hcp_vault_cluster/import.sh @@ -0,0 +1,2 @@ +# The import ID is {cluster_id} +terraform import hcp_vault_cluster.example vault-cluster \ No newline at end of file diff --git a/examples/resources/hcp_vault_cluster/resource.tf b/examples/resources/hcp_vault_cluster/resource.tf new file mode 100644 index 000000000..77de77e83 --- /dev/null +++ b/examples/resources/hcp_vault_cluster/resource.tf @@ -0,0 +1,11 @@ +resource "hcp_hvn" "example" { + hvn_id = "hvn" + cloud_provider = "aws" + region = "us-west-2" + cidr_block = "172.25.16.0/20" +} + +resource "hcp_vault_cluster" "example" { + cluster_id = "vault-cluster" + hvn_id = hcp_hvn.example.hvn_id +} \ No newline at end of file diff --git a/examples/resources/hcp_vault_cluster_admin_token/resource.tf b/examples/resources/hcp_vault_cluster_admin_token/resource.tf new file mode 100644 index 000000000..a420f8b5f --- /dev/null +++ b/examples/resources/hcp_vault_cluster_admin_token/resource.tf @@ -0,0 +1,3 @@ +resource "hcp_vault_cluster_admin_token" "example" { + cluster_id = "test-vault-cluster" +} diff --git a/internal/clients/client.go b/internal/clients/client.go index 623274c8a..3520f6379 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -10,6 +10,8 @@ import ( cloud_resource_manager "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client" "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/organization_service" "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/project_service" + cloud_vault "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/preview/2020-11-25/client" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/preview/2020-11-25/client/vault_service" sdk "github.com/hashicorp/hcp-sdk-go/httpclient" ) @@ -22,6 +24,7 @@ type Client struct { Project project_service.ClientService Organization organization_service.ClientService Consul consul_service.ClientService + Vault vault_service.ClientService } // ClientConfig specifies configuration for the client that interacts with HCP @@ -65,6 +68,7 @@ func NewClient(config ClientConfig) (*Client, error) { Project: cloud_resource_manager.New(httpClient, nil).ProjectService, Organization: cloud_resource_manager.New(httpClient, nil).OrganizationService, Consul: cloud_consul.New(httpClient, nil).ConsulService, + Vault: cloud_vault.New(httpClient, nil).VaultService, } return client, nil diff --git a/internal/clients/vault_cluster.go b/internal/clients/vault_cluster.go new file mode 100644 index 000000000..630855157 --- /dev/null +++ b/internal/clients/vault_cluster.go @@ -0,0 +1,87 @@ +package clients + +import ( + "context" + + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/preview/2020-11-25/client/vault_service" + vaultmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/preview/2020-11-25/models" +) + +// GetVaultClusterByID gets an Vault cluster by its ID. +func GetVaultClusterByID(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, + vaultClusterID string) (*vaultmodels.HashicorpCloudVault20201125Cluster, error) { + + getParams := vault_service.NewGetParams() + getParams.Context = ctx + getParams.ClusterID = vaultClusterID + getParams.LocationOrganizationID = loc.OrganizationID + getParams.LocationProjectID = loc.ProjectID + + getResp, err := client.Vault.Get(getParams, nil) + if err != nil { + return nil, err + } + + return getResp.Payload.Cluster, nil +} + +// CreateVaultCluster will make a call to the Consul service to initiate the create Consul +// cluster workflow. +func CreateVaultCluster(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, + vaultCluster *vaultmodels.HashicorpCloudVault20201125InputCluster) (*vaultmodels.HashicorpCloudVault20201125CreateResponse, error) { + + p := vault_service.NewCreateParams() + p.Context = ctx + p.Body = &vaultmodels.HashicorpCloudVault20201125CreateRequest{Cluster: vaultCluster} + + p.ClusterLocationOrganizationID = loc.OrganizationID + p.ClusterLocationProjectID = loc.ProjectID + + resp, err := client.Vault.Create(p, nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +// DeleteVaultCluster will make a call to the Vault service to initiate the delete Vault +// cluster workflow. +func DeleteVaultCluster(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, + clusterID string) (*vaultmodels.HashicorpCloudVault20201125DeleteResponse, error) { + + p := vault_service.NewDeleteParams() + p.Context = ctx + p.ClusterID = clusterID + p.LocationOrganizationID = loc.OrganizationID + p.LocationProjectID = loc.ProjectID + + deleteResp, err := client.Vault.Delete(p, nil) + if err != nil { + return nil, err + } + + return deleteResp.Payload, nil +} + +// CreateVaultClusterAdminToken will make a call to the Vault service to generate an admin token for the Vault cluster +// that expires after 6 hours. +func CreateVaultClusterAdminToken(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, + vaultClusterID string) (*vaultmodels.HashicorpCloudVault20201125GetAdminTokenResponse, error) { + + p := vault_service.NewGetAdminTokenParams() + p.Context = ctx + p.ClusterID = vaultClusterID + p.LocationOrganizationID = loc.OrganizationID + p.LocationProjectID = loc.ProjectID + p.LocationRegionProvider = &loc.Region.Provider + p.LocationRegionRegion = &loc.Region.Region + + resp, err := client.Vault.GetAdminToken(p, nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} diff --git a/internal/consul/version.go b/internal/consul/version.go index a6f9f3f91..a1b077892 100644 --- a/internal/consul/version.go +++ b/internal/consul/version.go @@ -34,11 +34,6 @@ func IsValidVersion(version string, versions []*consulmodels.HashicorpCloudConsu return false } -// NormalizeVersion ensures the version starts with a 'v' -func NormalizeVersion(version string) string { - return "v" + strings.TrimPrefix(version, "v") -} - // VersionsToString converts a slice of version pointers to a string of their comma delimited values. func VersionsToString(versions []*consulmodels.HashicorpCloudConsul20210204Version) string { var recommendedVersion string diff --git a/internal/consul/version_test.go b/internal/consul/version_test.go index 202f89354..2d9b568f7 100644 --- a/internal/consul/version_test.go +++ b/internal/consul/version_test.go @@ -117,31 +117,6 @@ func Test_IsValidVersion(t *testing.T) { } } -func Test_NormalizeVersion(t *testing.T) { - tcs := map[string]struct { - expected string - input string - }{ - "with a prefixed v": { - input: "v1.9.0", - expected: "v1.9.0", - }, - "without a prefixed v": { - input: "1.9.0", - expected: "v1.9.0", - }, - } - - for n, tc := range tcs { - t.Run(n, func(t *testing.T) { - r := require.New(t) - - result := NormalizeVersion(tc.input) - r.Equal(tc.expected, result) - }) - } -} - func Test_VersionsToString(t *testing.T) { tcs := map[string]struct { expected string diff --git a/internal/input/input.go b/internal/input/input.go new file mode 100644 index 000000000..b193a3fbf --- /dev/null +++ b/internal/input/input.go @@ -0,0 +1,10 @@ +package input + +import ( + "strings" +) + +// NormalizeVersion ensures the version starts with a 'v' +func NormalizeVersion(version string) string { + return "v" + strings.TrimPrefix(version, "v") +} diff --git a/internal/input/input_test.go b/internal/input/input_test.go new file mode 100644 index 000000000..5f0958b4f --- /dev/null +++ b/internal/input/input_test.go @@ -0,0 +1,32 @@ +package input + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_NormalizeVersion(t *testing.T) { + tcs := map[string]struct { + expected string + input string + }{ + "with a prefixed v": { + input: "v1.9.0", + expected: "v1.9.0", + }, + "without a prefixed v": { + input: "1.9.0", + expected: "v1.9.0", + }, + } + + for n, tc := range tcs { + t.Run(n, func(t *testing.T) { + r := require.New(t) + + result := NormalizeVersion(tc.input) + r.Equal(tc.expected, result) + }) + } +} diff --git a/internal/provider/data_source_vault_cluster.go b/internal/provider/data_source_vault_cluster.go new file mode 100644 index 000000000..bb3ed1e13 --- /dev/null +++ b/internal/provider/data_source_vault_cluster.go @@ -0,0 +1,130 @@ +package provider + +import ( + "context" + "log" + + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +func dataSourceVaultCluster() *schema.Resource { + return &schema.Resource{ + Description: "The cluster data source provides information about an existing HCP Vault cluster.", + ReadContext: dataSourceVaultClusterRead, + Timeouts: &schema.ResourceTimeout{ + Default: &defaultClusterTimeoutDuration, + }, + Schema: map[string]*schema.Schema{ + // Required inputs + "cluster_id": { + Description: "The ID of the HCP Vault cluster.", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateSlugID, + }, + // computed outputs + "hvn_id": { + Description: "The ID of the HVN this HCP Vault cluster is associated to.", + Type: schema.TypeString, + Computed: true, + }, + "public_endpoint": { + Description: "Denotes that the cluster has a public endpoint. Defaults to false.", + Type: schema.TypeBool, + Computed: true, + }, + "min_vault_version": { + Description: "The minimum Vault version to use when creating the cluster. If not specified, it is defaulted to the version that is currently recommended by HCP.", + Type: schema.TypeString, + Computed: true, + }, + "tier": { + Description: "The tier that the HCP Vault cluster will be provisioned as. Only 'development' is available at this time.", + Type: schema.TypeString, + Computed: true, + }, + "organization_id": { + Description: "The ID of the organization this HCP Vault cluster is located in.", + Type: schema.TypeString, + Computed: true, + }, + "project_id": { + Description: "The ID of the project this HCP Vault cluster is located in.", + Type: schema.TypeString, + Computed: true, + }, + "cloud_provider": { + Description: "The provider where the HCP Vault cluster is located.", + Type: schema.TypeString, + Computed: true, + }, + "region": { + Description: "The region where the HCP Vault cluster is located.", + Type: schema.TypeString, + Computed: true, + }, + "namespace": { + Description: "The name of the customer namespace this HCP Vault cluster is located in.", + Type: schema.TypeString, + Computed: true, + }, + "vault_version": { + Description: "The Vault version of the cluster.", + Type: schema.TypeString, + Computed: true, + }, + "vault_public_endpoint_url": { + Description: "The public URL for the Vault cluster. This will be empty if `public_endpoint` is `false`.", + Type: schema.TypeString, + Computed: true, + }, + "vault_private_endpoint_url": { + Description: "The private URL for the Vault cluster.", + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Description: "The time that the Vault cluster was created.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceVaultClusterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + clusterID := d.Get("cluster_id").(string) + client := meta.(*clients.Client) + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: client.Config.ProjectID, + } + + log.Printf("[INFO] Reading Vault cluster (%s) [project_id=%s, organization_id=%s]", clusterID, loc.ProjectID, loc.OrganizationID) + + cluster, err := clients.GetVaultClusterByID(ctx, client, loc, clusterID) + if err != nil { + return diag.FromErr(err) + } + + // build the id for this Vault cluster + link := newLink(loc, VaultClusterResourceType, clusterID) + url, err := linkURL(link) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(url) + + // Cluster found, update resource data. + if err := setVaultClusterResourceData(d, cluster); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/internal/provider/link.go b/internal/provider/link.go index 78bd0aaa6..84bd7d605 100644 --- a/internal/provider/link.go +++ b/internal/provider/link.go @@ -41,6 +41,9 @@ const ( // ConsulClusterAgentKubernetesSecretDataSourceType is the data source // type of a Consul cluster agent Kubernetes secret ConsulClusterAgentKubernetesSecretDataSourceType = ConsulClusterResourceType + ".agent-kubernetes-secret" + + // VaultClusterResourceType is the resource type of a Vault cluster + VaultClusterResourceType = "hashicorp.vault.cluster" ) // newLink constructs a new Link from the passed arguments. ID should be the diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 60d908df7..103de5d15 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -25,6 +25,7 @@ func New() func() *schema.Provider { "hcp_consul_cluster": dataSourceConsulCluster(), "hcp_consul_versions": dataSourceConsulVersions(), "hcp_hvn": dataSourceHvn(), + "hcp_vault_cluster": dataSourceVaultCluster(), }, ResourcesMap: map[string]*schema.Resource{ "hcp_aws_network_peering": resourceAwsNetworkPeering(), @@ -33,6 +34,8 @@ func New() func() *schema.Provider { "hcp_consul_cluster_root_token": resourceConsulClusterRootToken(), "hcp_consul_snapshot": resourceConsulSnapshot(), "hcp_hvn": resourceHvn(), + "hcp_vault_cluster": resourceVaultCluster(), + "hcp_vault_cluster_admin_token": resourceVaultClusterAdminToken(), }, Schema: map[string]*schema.Schema{ "client_id": { diff --git a/internal/provider/resource_consul_cluster.go b/internal/provider/resource_consul_cluster.go index ab1a39ef5..2cb8abe51 100644 --- a/internal/provider/resource_consul_cluster.go +++ b/internal/provider/resource_consul_cluster.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-provider-hcp/internal/clients" "github.com/hashicorp/terraform-provider-hcp/internal/consul" + "github.com/hashicorp/terraform-provider-hcp/internal/input" ) // defaultClusterTimeoutDuration is the amount of time that can elapse @@ -93,7 +94,7 @@ func resourceConsulCluster() *schema.Resource { DiffSuppressFunc: func(_, old, new string, _ *schema.ResourceData) bool { // Suppress diff is normalized versions match OR min_consul_version is removed from the resource // since min_consul_version is required in order to upgrade the cluster to a new Consul version. - return consul.NormalizeVersion(old) == consul.NormalizeVersion(new) || new == "" + return input.NormalizeVersion(old) == input.NormalizeVersion(new) || new == "" }, }, "datacenter": { @@ -257,7 +258,7 @@ func resourceConsulClusterCreate(ctx context.Context, d *schema.ResourceData, me consulVersion := consul.RecommendedVersion(availableConsulVersions) v, ok := d.GetOk("min_consul_version") if ok { - consulVersion = consul.NormalizeVersion(v.(string)) + consulVersion = input.NormalizeVersion(v.(string)) } // check if version is valid and available @@ -561,7 +562,7 @@ func resourceConsulClusterUpdate(ctx context.Context, d *schema.ResourceData, me if !ok { return diag.Errorf("min_consul_version is required in order to upgrade the cluster") } - newConsulVersion := consul.NormalizeVersion(v.(string)) + newConsulVersion := input.NormalizeVersion(v.(string)) // Check that there are any valid upgrade versions if upgradeVersions == nil { diff --git a/internal/provider/resource_vault_cluster.go b/internal/provider/resource_vault_cluster.go new file mode 100644 index 000000000..f1d79566e --- /dev/null +++ b/internal/provider/resource_vault_cluster.go @@ -0,0 +1,373 @@ +package provider + +import ( + "context" + "log" + "time" + + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + vaultmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/preview/2020-11-25/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/input" +) + +// defaultClusterTimeout is the amount of time that can elapse +// before a cluster read operation should timeout. +var defaultVaultClusterTimeout = time.Minute * 5 + +// createTimeout is the amount of time that can elapse +// before a cluster create operation should timeout. +var createVaultClusterTimeout = time.Minute * 35 + +// deleteTimeout is the amount of time that can elapse +// before a cluster delete operation should timeout. +var deleteVaultClusterTimeout = time.Minute * 25 + +func resourceVaultCluster() *schema.Resource { + return &schema.Resource{ + Description: "The Vault cluster resource allows you to manage an HCP Vault cluster.", + CreateContext: resourceVaultClusterCreate, + ReadContext: resourceVaultClusterRead, + DeleteContext: resourceVaultClusterDelete, + Timeouts: &schema.ResourceTimeout{ + Create: &createVaultClusterTimeout, + Delete: &deleteVaultClusterTimeout, + Default: &defaultVaultClusterTimeout, + }, + Importer: &schema.ResourceImporter{ + StateContext: resourceVaultClusterImport, + }, + Schema: map[string]*schema.Schema{ + // Required inputs + "cluster_id": { + Description: "The ID of the HCP Vault cluster.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateSlugID, + }, + "hvn_id": { + Description: "The ID of the HVN this HCP Vault cluster is associated to.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateSlugID, + }, + // optional fields + "public_endpoint": { + Description: "Denotes that the cluster has a public endpoint. Defaults to false.", + Type: schema.TypeBool, + Default: false, + Optional: true, + ForceNew: true, + }, + "min_vault_version": { + Description: "The minimum Vault version to use when creating the cluster. If not specified, it is defaulted to the version that is currently recommended by HCP.", + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateSemVer, + ForceNew: true, + }, + // computed outputs + // TODO: once more tiers are supported and can be changed by users, make this a required input. + "tier": { + Description: "The tier that the HCP Vault cluster will be provisioned as. Only 'development' is available at this time.", + Type: schema.TypeString, + Computed: true, + }, + "organization_id": { + Description: "The ID of the organization this HCP Vault cluster is located in.", + Type: schema.TypeString, + Computed: true, + }, + "project_id": { + Description: "The ID of the project this HCP Vault cluster is located in.", + Type: schema.TypeString, + Computed: true, + }, + "cloud_provider": { + Description: "The provider where the HCP Vault cluster is located.", + Type: schema.TypeString, + Computed: true, + }, + "region": { + Description: "The region where the HCP Vault cluster is located.", + Type: schema.TypeString, + Computed: true, + }, + "namespace": { + Description: "The name of the customer namespace this HCP Vault cluster is located in.", + Type: schema.TypeString, + Computed: true, + }, + "vault_version": { + Description: "The Vault version of the cluster.", + Type: schema.TypeString, + Computed: true, + }, + "vault_public_endpoint_url": { + Description: "The public URL for the Vault cluster. This will be empty if `public_endpoint` is `false`.", + Type: schema.TypeString, + Computed: true, + }, + "vault_private_endpoint_url": { + Description: "The private URL for the Vault cluster.", + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Description: "The time that the Vault cluster was created.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceVaultClusterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + clusterID := d.Get("cluster_id").(string) + hvnID := d.Get("hvn_id").(string) + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: client.Config.ProjectID, + } + + // Use the hvn to get provider and region. + hvn, err := clients.GetHvnByID(ctx, client, loc, hvnID) + if err != nil { + return diag.Errorf("unable to find existing HVN (%s): %v", hvnID, err) + } + loc.Region = &sharedmodels.HashicorpCloudLocationRegion{ + Provider: hvn.Location.Region.Provider, + Region: hvn.Location.Region.Region, + } + + // Check for an existing Vault cluster. + _, err = clients.GetVaultClusterByID(ctx, client, loc, clusterID) + if err != nil { + if !clients.IsResponseCodeNotFound(err) { + return diag.Errorf("unable to check for presence of an existing Vault cluster (%s): %v", clusterID, err) + } + + // A 404 indicates a Vault cluster was not found. + log.Printf("[INFO] Vault cluster (%s) not found, proceeding with create", clusterID) + } else { + return diag.Errorf("a Vault cluster with cluster_id=%q in project_id=%q already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documentation for hcp_vault_cluster for more information.", clusterID, loc.ProjectID) + } + + // If no min_vault_version is set, an empty version is passed and the backend will set a default version. + var vaultVersion string + v, ok := d.GetOk("min_vault_version") + if ok { + vaultVersion = input.NormalizeVersion(v.(string)) + } + + publicEndpoint := d.Get("public_endpoint").(bool) + + // TODO: Tier is hard-coded for now, but eventually will be required input on the resource. + tier := vaultmodels.HashicorpCloudVault20201125TierDEV + + log.Printf("[INFO] Creating Vault cluster (%s)", clusterID) + + vaultCuster := &vaultmodels.HashicorpCloudVault20201125InputCluster{ + Config: &vaultmodels.HashicorpCloudVault20201125InputClusterConfig{ + VaultConfig: &vaultmodels.HashicorpCloudVault20201125VaultConfig{ + InitialVersion: vaultVersion, + }, + Tier: tier, + NetworkConfig: &vaultmodels.HashicorpCloudVault20201125InputNetworkConfig{ + NetworkID: hvn.ID, + PublicIpsEnabled: publicEndpoint, + }, + }, + ID: clusterID, + Location: loc, + } + + payload, err := clients.CreateVaultCluster(ctx, client, loc, vaultCuster) + if err != nil { + return diag.Errorf("unable to create Vault cluster (%s): %v", clusterID, err) + } + + link := newLink(loc, VaultClusterResourceType, clusterID) + url, err := linkURL(link) + if err != nil { + return diag.FromErr(err) + } + d.SetId(url) + + // Wait for the Vault cluster to be created. + if err := clients.WaitForOperation(ctx, client, "create Vault cluster", loc, payload.Operation.ID); err != nil { + return diag.Errorf("unable to create Vault cluster (%s): %v", payload.ClusterID, err) + } + + log.Printf("[INFO] Created Vault cluster (%s)", payload.ClusterID) + + // Get the created Vault cluster. + cluster, err := clients.GetVaultClusterByID(ctx, client, loc, payload.ClusterID) + if err != nil { + return diag.Errorf("unable to retrieve Vault cluster (%s): %v", payload.ClusterID, err) + } + + if err := setVaultClusterResourceData(d, cluster); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceVaultClusterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + link, err := buildLinkFromURL(d.Id(), VaultClusterResourceType, client.Config.OrganizationID) + if err != nil { + return diag.FromErr(err) + } + + clusterID := link.ID + loc := link.Location + + log.Printf("[INFO] Reading Vault cluster (%s) [project_id=%s, organization_id=%s]", clusterID, loc.ProjectID, loc.OrganizationID) + + cluster, err := clients.GetVaultClusterByID(ctx, client, loc, clusterID) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + log.Printf("[WARN] Vault cluster (%s) not found, removing from state", clusterID) + d.SetId("") + return nil + } + + return diag.Errorf("unable to fetch Vault cluster (%s): %v", clusterID, err) + } + + // The Vault cluster failed to provision properly so we want to let the user know and + // remove it from state. + if cluster.State == vaultmodels.HashicorpCloudVault20201125ClusterStateFAILED { + log.Printf("[WARN] Vault cluster (%s) failed to provision, removing from state", clusterID) + d.SetId("") + return nil + } + + // Cluster found, update resource data. + if err := setVaultClusterResourceData(d, cluster); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceVaultClusterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + link, err := buildLinkFromURL(d.Id(), VaultClusterResourceType, client.Config.OrganizationID) + if err != nil { + return diag.FromErr(err) + } + + clusterID := link.ID + loc := link.Location + + log.Printf("[INFO] Deleting Vault cluster (%s)", clusterID) + + deleteResp, err := clients.DeleteVaultCluster(ctx, client, loc, clusterID) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + log.Printf("[WARN] Vault cluster (%s) not found, so no action was taken", clusterID) + return nil + } + + return diag.Errorf("unable to delete Vault cluster (%s): %v", clusterID, err) + } + + // Wait for the delete cluster operation. + if err := clients.WaitForOperation(ctx, client, "delete Vault cluster", loc, deleteResp.Operation.ID); err != nil { + return diag.Errorf("unable to delete Vault cluster (%s): %v", clusterID, err) + } + + return nil +} + +// setVaultClusterResourceData sets the KV pairs of the Vault cluster resource schema. +func setVaultClusterResourceData(d *schema.ResourceData, cluster *vaultmodels.HashicorpCloudVault20201125Cluster) error { + + if err := d.Set("cluster_id", cluster.ID); err != nil { + return err + } + + if err := d.Set("hvn_id", cluster.Config.NetworkConfig.NetworkID); err != nil { + return err + } + + if err := d.Set("organization_id", cluster.Location.OrganizationID); err != nil { + return err + } + + if err := d.Set("project_id", cluster.Location.ProjectID); err != nil { + return err + } + + if err := d.Set("cloud_provider", cluster.Location.Region.Provider); err != nil { + return err + } + + if err := d.Set("region", cluster.Location.Region.Region); err != nil { + return err + } + + if err := d.Set("tier", cluster.Config.Tier); err != nil { + return err + } + + if err := d.Set("vault_version", cluster.CurrentVersion); err != nil { + return err + } + + if err := d.Set("namespace", cluster.Config.VaultConfig.Namespace); err != nil { + return err + } + + publicEndpoint := cluster.Config.NetworkConfig.PublicIpsEnabled + if err := d.Set("public_endpoint", publicEndpoint); err != nil { + return err + } + + if publicEndpoint { + if err := d.Set("vault_public_endpoint_url", cluster.DNSNames.Public); err != nil { + return err + } + } + + if err := d.Set("vault_private_endpoint_url", cluster.DNSNames.Private); err != nil { + return err + } + + if err := d.Set("created_at", cluster.CreatedAt.String()); err != nil { + return err + } + + return nil +} + +func resourceVaultClusterImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + client := meta.(*clients.Client) + + clusterID := d.Id() + loc := &sharedmodels.HashicorpCloudLocationLocation{ + ProjectID: client.Config.ProjectID, + } + + link := newLink(loc, VaultClusterResourceType, clusterID) + url, err := linkURL(link) + if err != nil { + return nil, err + } + + d.SetId(url) + + return []*schema.ResourceData{d}, nil +} diff --git a/internal/provider/resource_vault_cluster_admin_token.go b/internal/provider/resource_vault_cluster_admin_token.go new file mode 100644 index 000000000..e28d46e39 --- /dev/null +++ b/internal/provider/resource_vault_cluster_admin_token.go @@ -0,0 +1,214 @@ +package provider + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +// defaultVaultAdminTokenTimeout is the amount of time that can elapse +// before an admin token operation should timeout. +var defaultVaultAdminTokenTimeout = time.Minute * 5 + +// adminTokenExpiry is the length of the time in seconds before a generated admin token expires. +var adminTokenExpiry = time.Second * 3600 * 6 + +func resourceVaultClusterAdminToken() *schema.Resource { + return &schema.Resource{ + Description: "The Vault cluster admin token resource generates an admin-level token for the HCP Vault cluster.", + CreateContext: resourceVaultClusterAdminTokenCreate, + ReadContext: resourceVaultClusterAdminTokenRead, + DeleteContext: resourceVaultClusterAdminTokenDelete, + Timeouts: &schema.ResourceTimeout{ + Create: &defaultVaultAdminTokenTimeout, + Read: &defaultVaultAdminTokenTimeout, + Delete: &defaultVaultAdminTokenTimeout, + }, + Schema: map[string]*schema.Schema{ + // Required inputs + "cluster_id": { + Description: "The ID of the HCP Vault cluster.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateSlugID, + }, + // computed outputs + "created_at": { + Description: "The time that the admin token was created.", + Type: schema.TypeString, + Computed: true, + }, + "token": { + Description: "The admin token of this HCP Vault cluster.", + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} + +// resourceVaultClusterAdminTokenCreate generates a new admin token for the Vault cluster. +func resourceVaultClusterAdminTokenCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + clusterID := d.Get("cluster_id").(string) + + loc := &models.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: client.Config.ProjectID, + } + + log.Printf("[INFO] reading Vault cluster (%s) [project_id=%s, organization_id=%s]", clusterID, loc.ProjectID, loc.OrganizationID) + cluster, err := clients.GetVaultClusterByID(ctx, client, loc, clusterID) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + return diag.Errorf("unable to create admin token; Vault cluster (%s) not found", + clusterID, + ) + } + + return diag.Errorf("unable to check for presence of an existing Vault cluster (%s): %v", + clusterID, + err, + ) + } + + loc.Region = &models.HashicorpCloudLocationRegion{ + Provider: cluster.Location.Region.Provider, + Region: cluster.Location.Region.Region, + } + + tokenResp, err := clients.CreateVaultClusterAdminToken(ctx, client, loc, clusterID) + if err != nil { + return diag.Errorf("error creating HCP Vault cluster admin token (cluster_id %q) (project_id %q): %+v", + clusterID, + client.Config.ProjectID, + err, + ) + } + + err = d.Set("token", tokenResp.Token) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("created_at", time.Now().Format(time.RFC3339)) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("/project/%s/%s/%s/token", + loc.ProjectID, + VaultClusterResourceType, + clusterID)) + + return nil +} + +// resourceVaultClusterAdminTokenRead cannot read the admin token from the API as it is not persisted in +// any way that it can be fetched. Instead this operation first verifies the existence of the associated Vault cluster +// and then refreshes the token if it is close to expiring or expired. +func resourceVaultClusterAdminTokenRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + clusterID := d.Get("cluster_id").(string) + organizationID := client.Config.OrganizationID + projectID := client.Config.ProjectID + + loc := &models.HashicorpCloudLocationLocation{ + OrganizationID: organizationID, + ProjectID: projectID, + } + + log.Printf("[INFO] reading Vault cluster (%s) [project_id=%s, organization_id=%s]", clusterID, loc.ProjectID, loc.OrganizationID) + + cluster, err := clients.GetVaultClusterByID(ctx, client, loc, clusterID) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + // No cluster exists, so this admin token should be removed from state. + log.Printf("[WARN] no HCP Vault cluster found with (cluster_id %q) (project_id %q); removing admin token.", + clusterID, + projectID, + ) + d.SetId("") + return nil + } + + return diag.Errorf("unable to check for presence of an existing Vault cluster (cluster_id %q) (project_id %q): %v", + clusterID, + projectID, + err, + ) + } + + loc.Region = &models.HashicorpCloudLocationRegion{ + Provider: cluster.Location.Region.Provider, + Region: cluster.Location.Region.Region, + } + + // If the token already exists, this block verifies if it is close to expiration and should be refreshed. + createdAt := d.Get("created_at").(string) + if createdAt != "" { + log.Printf("[INFO] existing admin token found for Vault cluster (%s) [project_id=%s, organization_id=%s]", + clusterID, + loc.ProjectID, + loc.OrganizationID, + ) + + // The refresh window starts five minutes before the 6h expiry. + expiry := adminTokenExpiry - (time.Second * 60 * 5) + + t, err := time.Parse(time.RFC3339, createdAt) + if err != nil { + return diag.Errorf("error verifying HCP Vault cluster admin token (cluster_id %q) (project_id %q): %+v", + clusterID, + client.Config.ProjectID, + err, + ) + } + + // If the token is less than five minutes from the 6h expiry, it's time to regenerate. + if time.Now().Unix() > t.Add(expiry).Unix() { + log.Printf("[INFO] refreshing admin token for Vault cluster (%s) [project_id=%s, organization_id=%s]", + clusterID, + loc.ProjectID, + loc.OrganizationID, + ) + + tokenResp, err := clients.CreateVaultClusterAdminToken(ctx, client, loc, clusterID) + if err != nil { + return diag.Errorf("error creating HCP Vault cluster admin token (cluster_id %q) (project_id %q): %+v", + clusterID, + client.Config.ProjectID, + err, + ) + } + + err = d.Set("token", tokenResp.Token) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("created_at", time.Now().Format(time.RFC3339)) + if err != nil { + return diag.FromErr(err) + } + } + } + + return nil +} + +// resourceVaultClusterAdminTokenDelete will remove the token from state but there is currently no way to invalidate an existing token. +func resourceVaultClusterAdminTokenDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + d.SetId("") + return nil +} diff --git a/internal/provider/resource_vault_cluster_admin_token_test.go b/internal/provider/resource_vault_cluster_admin_token_test.go new file mode 100644 index 000000000..125344289 --- /dev/null +++ b/internal/provider/resource_vault_cluster_admin_token_test.go @@ -0,0 +1,46 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +var ( + testAccVaultClusterAdminTokenConfig = fmt.Sprintf(` +resource "hcp_hvn" "test" { + hvn_id = "test-hvn" + cloud_provider = "aws" + region = "us-west-2" +} + +resource "hcp_vault_cluster" "test" { + cluster_id = "test-vault-cluster" + hvn_id = hcp_hvn.test.hvn_id +} + +resource "hcp_vault_cluster_admin_token" "test" { + cluster_id = hcp_vault_cluster.test.cluster_id +} +`) +) + +func TestAccVaultClusterAdminToken(t *testing.T) { + resourceName := "hcp_vault_cluster_admin_token.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testConfig(testAccVaultClusterAdminTokenConfig), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "cluster_id", "test-vault-cluster"), + resource.TestCheckResourceAttrSet(resourceName, "token"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + ), + }, + }, + }) +} diff --git a/internal/provider/resource_vault_cluster_test.go b/internal/provider/resource_vault_cluster_test.go new file mode 100644 index 000000000..30d81f1c5 --- /dev/null +++ b/internal/provider/resource_vault_cluster_test.go @@ -0,0 +1,149 @@ +package provider + +import ( + "context" + "fmt" + "testing" + + "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" +) + +var ( + testAccVaultClusterConfig = fmt.Sprintf(` +resource "hcp_hvn" "test" { + hvn_id = "test-hvn" + cloud_provider = "aws" + region = "us-west-2" +} + +resource "hcp_vault_cluster" "test" { + cluster_id = "test-vault-cluster" + hvn_id = hcp_hvn.test.hvn_id +} +`) +) + +func TestAccVaultCluster(t *testing.T) { + resourceName := "hcp_vault_cluster.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckVaultClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testConfig(testAccVaultClusterConfig), + Check: resource.ComposeTestCheckFunc( + testAccCheckVaultClusterExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "cluster_id", "test-vault-cluster"), + resource.TestCheckResourceAttr(resourceName, "hvn_id", "test-hvn"), + resource.TestCheckResourceAttr(resourceName, "tier", "DEV"), + resource.TestCheckResourceAttr(resourceName, "cloud_provider", "aws"), + resource.TestCheckResourceAttr(resourceName, "region", "us-west-2"), + resource.TestCheckResourceAttr(resourceName, "public_endpoint", "false"), + resource.TestCheckResourceAttr(resourceName, "namespace", "admin"), + resource.TestCheckResourceAttrSet(resourceName, "vault_version"), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "project_id"), + resource.TestCheckNoResourceAttr(resourceName, "vault_public_endpoint_url"), + resource.TestCheckResourceAttrSet(resourceName, "vault_private_endpoint_url"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + ), + }, + // This step simulates an import of the resource. + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("not found: %s", resourceName) + } + + return rs.Primary.Attributes["cluster_id"], nil + }, + ImportStateVerify: true, + }, + // This step is a subsequent terraform apply that verifies that no state is modified. + { + Config: testConfig(testAccVaultClusterConfig), + Check: resource.ComposeTestCheckFunc( + testAccCheckVaultClusterExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "cluster_id", "test-vault-cluster"), + resource.TestCheckResourceAttr(resourceName, "hvn_id", "test-hvn"), + resource.TestCheckResourceAttr(resourceName, "tier", "DEV"), + resource.TestCheckResourceAttr(resourceName, "cloud_provider", "aws"), + resource.TestCheckResourceAttr(resourceName, "region", "us-west-2"), + resource.TestCheckResourceAttr(resourceName, "public_endpoint", "false"), + resource.TestCheckResourceAttr(resourceName, "namespace", "admin"), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "project_id"), + resource.TestCheckResourceAttrSet(resourceName, "vault_version"), + resource.TestCheckNoResourceAttr(resourceName, "vault_public_endpoint_url"), + resource.TestCheckResourceAttrSet(resourceName, "vault_private_endpoint_url"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + ), + }, + }, + }) +} + +func testAccCheckVaultClusterExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("no ID is set") + } + + client := testAccProvider.Meta().(*clients.Client) + + link, err := buildLinkFromURL(id, VaultClusterResourceType, client.Config.OrganizationID) + if err != nil { + return fmt.Errorf("unable to build link for %q: %v", id, err) + } + + clusterID := link.ID + loc := link.Location + + if _, err := clients.GetVaultClusterByID(context.Background(), client, loc, clusterID); err != nil { + return fmt.Errorf("unable to read Vault cluster %q: %v", id, err) + } + + return nil + } +} + +func testAccCheckVaultClusterDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*clients.Client) + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "hcp_vault_cluster": + id := rs.Primary.ID + + link, err := buildLinkFromURL(id, VaultClusterResourceType, client.Config.OrganizationID) + if err != nil { + return fmt.Errorf("unable to build link for %q: %v", id, err) + } + + clusterID := link.ID + loc := link.Location + + _, err = clients.GetVaultClusterByID(context.Background(), client, loc, clusterID) + if err == nil || !clients.IsResponseCodeNotFound(err) { + return fmt.Errorf("didn't get a 404 when reading destroyed Vault cluster %q: %v", id, err) + } + + default: + continue + } + } + return nil +} diff --git a/templates/guides/vault-admin-token.md.tmpl b/templates/guides/vault-admin-token.md.tmpl new file mode 100644 index 000000000..2033ffae8 --- /dev/null +++ b/templates/guides/vault-admin-token.md.tmpl @@ -0,0 +1,14 @@ +--- +subcategory: "" +page_title: "Create a Vault cluster and admin token - HCP Provider" +description: |- + An example of creating a Vault cluster and admin token. +--- + +# Create a new Vault cluster and an admin token + +Once you have an HVN, HCP Vault enables you to quickly deploy a Vault Enterprise cluster in AWS across a variety of environments while offloading the operations burden to the SRE experts at HashiCorp. +The cluster's admin token grants its bearer administrator access to the Vault cluster. This admin token is valid for six hours. On subsequent reads after creation, +the resource will check if the admin token is close to expiration or expired and automatically refresh as needed. + +{{ tffile "examples/guides/vault_cluster_admin_token/main.tf" }} diff --git a/templates/resources/vault_cluster_admin_token.md.tmpl b/templates/resources/vault_cluster_admin_token.md.tmpl new file mode 100644 index 000000000..f3bb23cd5 --- /dev/null +++ b/templates/resources/vault_cluster_admin_token.md.tmpl @@ -0,0 +1,29 @@ +--- +page_title: "{{.Type}} {{.Name}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} `{{.Type}}` + +~> **Important Security Notice** The admin token generated by this resource will +be stored *unencrypted* in your Terraform state file. **Use of this resource +for production deployments is *not* recommended**. Instead, generate +an admin token outside of Terraform and distribute it securely +to the system where Terraform will be run. + +{{ .Description | trimspace }} + +This resource saves a single admin token per Vault cluster and auto-refreshes the token when it is about to expire. +Destroying this resource *does not* invalidate the admin token. + +~> **Known Issue** An admin token may be generated during a `terraform plan` if the current token is expiring. +Since the Plan phase does not save any state, the Apply phase saves a different generated token, and the token generated during Plan ends up orphaned. +It will expire in six hours. + +## Example Usage + +{{ tffile "examples/resources/hcp_vault_cluster_admin_token/resource.tf" }} + +{{ .SchemaMarkdown | trimspace }} \ No newline at end of file