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