From 77d12ab911a082e29145d6fa2050615bce2fbe26 Mon Sep 17 00:00:00 2001 From: HashiCorp Cloud Services <> Date: Wed, 13 Sep 2023 04:28:01 +0000 Subject: [PATCH] Sync with public Provider --- contributing/writing-tests.md | 4 + docs/data-sources/vault_cluster.md | 17 + docs/data-sources/vault_plugin.md | 40 +++ docs/resources/vault_cluster.md | 23 ++ docs/resources/vault_plugin.md | 40 +++ go.mod | 24 +- go.sum | 48 +-- internal/clients/vault_cluster.go | 61 ++++ .../provider/data_source_vault_cluster.go | 85 +++++ internal/provider/data_source_vault_plugin.go | 104 ++++++ internal/provider/provider.go | 2 + internal/provider/resource_vault_cluster.go | 249 ++++++++++++-- .../resource_vault_cluster_config_test.go | 96 +++--- internal/provider/resource_vault_plugin.go | 312 ++++++++++++++++++ .../provider/resource_vault_plugin_test.go | 188 +++++++++++ internal/provider/validators.go | 29 +- internal/provider/validators_test.go | 6 +- 17 files changed, 1213 insertions(+), 115 deletions(-) create mode 100644 docs/data-sources/vault_plugin.md create mode 100644 docs/resources/vault_plugin.md create mode 100644 internal/provider/data_source_vault_plugin.go create mode 100644 internal/provider/resource_vault_plugin.go create mode 100644 internal/provider/resource_vault_plugin_test.go diff --git a/contributing/writing-tests.md b/contributing/writing-tests.md index 65a99fab5..0b92a855e 100644 --- a/contributing/writing-tests.md +++ b/contributing/writing-tests.md @@ -67,6 +67,10 @@ write the regular expression. For advanced developers, the acceptance testing framework accepts some additional environment variables that can be used to control Terraform CLI binary selection, logging, and other behaviors. See the [Extending Terraform documentation](https://www.terraform.io/docs/extend/testing/acceptance-tests/index.html#environment-variables) for more information. +```sh +export TF_LOG=... +``` + ## Writing an Acceptance Test Terraform has a framework for writing acceptance tests which minimizes the diff --git a/docs/data-sources/vault_cluster.md b/docs/data-sources/vault_cluster.md index 546b2f292..4abe23da2 100644 --- a/docs/data-sources/vault_cluster.md +++ b/docs/data-sources/vault_cluster.md @@ -70,7 +70,16 @@ Optional: Read-Only: +- `cloudwatch_access_key_id` (String) CloudWatch access key ID for streaming audit logs +- `cloudwatch_group_name` (String) CloudWatch group name of the target log stream for audit logs +- `cloudwatch_region` (String) CloudWatch region for streaming audit logs +- `cloudwatch_secret_access_key` (String) CloudWatch secret access key for streaming audit logs +- `cloudwatch_stream_name` (String) CloudWatch stream name for the target log stream for audit logs - `datadog_region` (String) Datadog region for streaming audit logs +- `elasticsearch_dataset` (String) ElasticSearch dataset for streaming audit logs +- `elasticsearch_endpoint` (String) ElasticSearch endpoint for streaming audit logs +- `elasticsearch_password` (String) ElasticSearch password for streaming audit logs +- `elasticsearch_user` (String) ElasticSearch user for streaming audit logs - `grafana_endpoint` (String) Grafana endpoint for streaming audit logs - `grafana_user` (String) Grafana user for streaming audit logs - `splunk_hecendpoint` (String) Splunk endpoint for streaming audit logs @@ -91,7 +100,15 @@ Read-Only: Read-Only: +- `cloudwatch_access_key_id` (String) CloudWatch access key ID for streaming metrics +- `cloudwatch_namespace` (String) CloudWatch namespace for streaming metrics +- `cloudwatch_region` (String) CloudWatch region for streaming metrics +- `cloudwatch_secret_access_key` (String) CloudWatch secret access key for streaming metrics - `datadog_region` (String) Datadog region for streaming metrics +- `elasticsearch_dataset` (String) ElasticSearch dataset for streaming metrics +- `elasticsearch_endpoint` (String) ElasticSearch endpoint for streaming metrics +- `elasticsearch_password` (String) ElasticSearch password for streaming metrics +- `elasticsearch_user` (String) ElasticSearch user for streaming metrics - `grafana_endpoint` (String) Grafana endpoint for streaming metrics - `grafana_user` (String) Grafana user for streaming metrics - `splunk_hecendpoint` (String) Splunk endpoint for streaming metrics diff --git a/docs/data-sources/vault_plugin.md b/docs/data-sources/vault_plugin.md new file mode 100644 index 000000000..c3fa49cfc --- /dev/null +++ b/docs/data-sources/vault_plugin.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "hcp_vault_plugin Data Source - terraform-provider-hcp" +subcategory: "" +description: |- + The Vault plugin data source provides information about an existing HCP Vault plugin +--- + +# hcp_vault_plugin (Data Source) + +The Vault plugin data source provides information about an existing HCP Vault plugin + + + + +## Schema + +### Required + +- `cluster_id` (String) The ID of the HCP Vault cluster. +- `plugin_name` (String) The name of the plugin - Valid options for plugin name - 'venafi-pki-backend' +- `plugin_type` (String) The type of the plugin - Valid options for plugin type - 'SECRET', 'AUTH', 'DATABASE' + +### Optional + +- `project_id` (String) The ID of the HCP project where the HCP Vault cluster is located. +If not specified, the project specified in the HCP Provider config block will be used, if configured. +If a project is not configured in the HCP Provider config block, the oldest project in the organization will be used. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `timeouts` + +Optional: + +- `default` (String) diff --git a/docs/resources/vault_cluster.md b/docs/resources/vault_cluster.md index 382d2a41c..ef0650b46 100644 --- a/docs/resources/vault_cluster.md +++ b/docs/resources/vault_cluster.md @@ -83,14 +83,26 @@ If a project is not configured in the HCP Provider config block, the oldest proj Optional: +- `cloudwatch_access_key_id` (String) CloudWatch access key ID for streaming audit logs +- `cloudwatch_region` (String) CloudWatch region for streaming audit logs +- `cloudwatch_secret_access_key` (String, Sensitive) CloudWatch secret access key for streaming audit logs - `datadog_api_key` (String, Sensitive) Datadog api key for streaming audit logs - `datadog_region` (String) Datadog region for streaming audit logs +- `elasticsearch_endpoint` (String) ElasticSearch endpoint for streaming audit logs +- `elasticsearch_password` (String, Sensitive) ElasticSearch password for streaming audit logs +- `elasticsearch_user` (String) ElasticSearch user for streaming audit logs - `grafana_endpoint` (String) Grafana endpoint for streaming audit logs - `grafana_password` (String, Sensitive) Grafana password for streaming audit logs - `grafana_user` (String) Grafana user for streaming audit logs - `splunk_hecendpoint` (String) Splunk endpoint for streaming audit logs - `splunk_token` (String, Sensitive) Splunk token for streaming audit logs +Read-Only: + +- `cloudwatch_group_name` (String) CloudWatch group name of the target log stream for audit logs +- `cloudwatch_stream_name` (String) CloudWatch stream name for the target log stream for audit logs +- `elasticsearch_dataset` (String) ElasticSearch dataset for streaming audit logs + ### Nested Schema for `major_version_upgrade_config` @@ -110,14 +122,25 @@ Optional: Optional: +- `cloudwatch_access_key_id` (String) CloudWatch access key ID for streaming metrics +- `cloudwatch_region` (String) CloudWatch region for streaming metrics +- `cloudwatch_secret_access_key` (String, Sensitive) CloudWatch secret access key for streaming metrics - `datadog_api_key` (String, Sensitive) Datadog api key for streaming metrics - `datadog_region` (String) Datadog region for streaming metrics +- `elasticsearch_endpoint` (String) ElasticSearch endpoint for streaming metrics +- `elasticsearch_password` (String, Sensitive) ElasticSearch password for streaming metrics +- `elasticsearch_user` (String) ElasticSearch user for streaming metrics - `grafana_endpoint` (String) Grafana endpoint for streaming metrics - `grafana_password` (String, Sensitive) Grafana password for streaming metrics - `grafana_user` (String) Grafana user for streaming metrics - `splunk_hecendpoint` (String) Splunk endpoint for streaming metrics - `splunk_token` (String, Sensitive) Splunk token for streaming metrics +Read-Only: + +- `cloudwatch_namespace` (String) CloudWatch namespace for streaming metrics +- `elasticsearch_dataset` (String) ElasticSearch dataset for streaming metrics + ### Nested Schema for `timeouts` diff --git a/docs/resources/vault_plugin.md b/docs/resources/vault_plugin.md new file mode 100644 index 000000000..e68136db2 --- /dev/null +++ b/docs/resources/vault_plugin.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "hcp_vault_plugin Resource - terraform-provider-hcp" +subcategory: "" +description: |- + The Vault plugin resource allows you to manage an HCP Vault plugin. +--- + +# hcp_vault_plugin (Resource) + +The Vault plugin resource allows you to manage an HCP Vault plugin. + + + + +## Schema + +### Required + +- `cluster_id` (String) The ID of the HCP Vault cluster. +- `plugin_name` (String) The name of the plugin - Valid options for plugin name - 'venafi-pki-backend' +- `plugin_type` (String) The type of the plugin - Valid options for plugin type - 'SECRET', 'AUTH', 'DATABASE' + +### Optional + +- `project_id` (String) The ID of the HCP project where the HCP Vault cluster is located. +If not specified, the project specified in the HCP Provider config block will be used, if configured. +If a project is not configured in the HCP Provider config block, the oldest project in the organization will be used. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `timeouts` + +Optional: + +- `default` (String) diff --git a/go.mod b/go.mod index 1b1d6a1be..56fec6ce9 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,16 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/go-openapi/runtime v0.26.0 github.com/go-openapi/strfmt v0.21.7 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.6.0 - github.com/hashicorp/hcp-sdk-go v0.59.0 + github.com/hashicorp/hcp-sdk-go v0.61.0 github.com/hashicorp/terraform-plugin-docs v0.16.0 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.27.0 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0 github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df - google.golang.org/grpc v1.57.0 + google.golang.org/grpc v1.58.0 ) require ( @@ -54,7 +54,7 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.18.1 // indirect github.com/hashicorp/terraform-json v0.17.1 // indirect - github.com/hashicorp/terraform-plugin-go v0.16.0 // indirect + github.com/hashicorp/terraform-plugin-go v0.18.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.1 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect @@ -89,15 +89,15 @@ require ( go.mongodb.org/mongo-driver v1.11.7 // indirect go.opentelemetry.io/otel v1.14.0 // indirect go.opentelemetry.io/otel/trace v1.14.0 // indirect - golang.org/x/crypto v0.10.0 // indirect + golang.org/x/crypto v0.12.0 // indirect golang.org/x/mod v0.11.0 // indirect - golang.org/x/net v0.11.0 // indirect - golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 85c8e84a1..51bf9e8e8 100644 --- a/go.sum +++ b/go.sum @@ -122,8 +122,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -150,8 +150,8 @@ github.com/hashicorp/hc-install v0.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEse github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI= github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= -github.com/hashicorp/hcp-sdk-go v0.59.0 h1:gFlr5YRKuF4S6E6tgsjBkdsQphO5PQjvKL8vOPKj/pc= -github.com/hashicorp/hcp-sdk-go v0.59.0/go.mod h1:xP7wmWAmdMxs/7+ovH3jZn+MCDhHRj50Rn+m7JIY3Ck= +github.com/hashicorp/hcp-sdk-go v0.61.0 h1:x4hJ8SlLI5WCE8Uzcu4q5jfdOEz/hFxfUkhAdoFdzSg= +github.com/hashicorp/hcp-sdk-go v0.61.0/go.mod h1:xP7wmWAmdMxs/7+ovH3jZn+MCDhHRj50Rn+m7JIY3Ck= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4= @@ -160,12 +160,12 @@ github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQH github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFccGyBZn52KtMNsS12dI= github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA= -github.com/hashicorp/terraform-plugin-go v0.16.0 h1:DSOQ0rz5FUiVO4NUzMs8ln9gsPgHMTsfns7Nk+6gPuE= -github.com/hashicorp/terraform-plugin-go v0.16.0/go.mod h1:4sn8bFuDbt+2+Yztt35IbOrvZc0zyEi87gJzsTgCES8= +github.com/hashicorp/terraform-plugin-go v0.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE= +github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.27.0 h1:I8efBnjuDrgPjNF1MEypHy48VgcTIUY4X6rOFunrR3Y= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.27.0/go.mod h1:cUEP4ly/nxlHy5HzD6YRrHydtlheGvGRJDhiWqqVik4= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0 h1:gY4SG34ANc6ZSeWEKC9hDTChY0ZiN+Myon17fSA0Xgc= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.28.0/go.mod h1:deXEw/iJXtJxNV9d1c/OVJrvL7Zh0a++v7rzokW6wVY= github.com/hashicorp/terraform-registry-address v0.2.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM= github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -323,8 +323,8 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= @@ -335,10 +335,10 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -363,8 +363,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -372,8 +372,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -383,14 +383,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= -google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o= +google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/clients/vault_cluster.go b/internal/clients/vault_cluster.go index 927b9f003..1a6b8c4ff 100644 --- a/internal/clients/vault_cluster.go +++ b/internal/clients/vault_cluster.go @@ -288,3 +288,64 @@ func DeleteVaultPathsFilter(ctx context.Context, client *Client, loc *sharedmode return deleteResp.Payload, nil } + +// AddPlugin will make a call to the Vault service to add a plugin to a Vault cluster +func AddPlugin(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, clusterID string, + request *vaultmodels.HashicorpCloudVault20201125AddPluginRequest) (vaultmodels.HashicorpCloudVault20201125AddPluginResponse, error) { + + addPluginParams := vault_service.NewAddPluginParams() + addPluginParams.Context = ctx + addPluginParams.ClusterID = clusterID + addPluginParams.LocationProjectID = loc.ProjectID + addPluginParams.LocationOrganizationID = loc.OrganizationID + addPluginParams.Body = request + + addPluginResp, err := client.Vault.AddPlugin(addPluginParams, nil) + if err != nil { + return nil, err + } + + return addPluginResp.Payload, nil +} + +// DeletePlugin will make a call to the Vault service to remove a plugin to a Vault cluster +func DeletePlugin(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, clusterID string, + request *vaultmodels.HashicorpCloudVault20201125DeletePluginRequest) (vaultmodels.HashicorpCloudVault20201125DeletePluginResponse, error) { + + delPluginParams := vault_service.NewDeletePluginParams() + delPluginParams.Context = ctx + delPluginParams.ClusterID = clusterID + delPluginParams.LocationProjectID = loc.ProjectID + delPluginParams.LocationOrganizationID = loc.OrganizationID + delPluginParams.Body = request + + delPluginResp, err := client.Vault.DeletePlugin(delPluginParams, nil) + if err != nil { + return nil, err + } + + return delPluginResp.Payload, nil +} + +// ListPlugins will make a call to the Vault service plugin status api to get all available plugins for the cluster. +func ListPlugins(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, clusterID string) (*vaultmodels.HashicorpCloudVault20201125PluginRegistrationStatusResponse, error) { + region := &sharedmodels.HashicorpCloudLocationRegion{} + if loc.Region != nil { + region = loc.Region + } + + listPluginsParams := vault_service.NewPluginRegistrationStatusParams() + listPluginsParams.Context = ctx + listPluginsParams.ClusterID = clusterID + listPluginsParams.LocationProjectID = loc.ProjectID + listPluginsParams.LocationOrganizationID = loc.OrganizationID + listPluginsParams.LocationRegionProvider = ®ion.Provider + listPluginsParams.LocationRegionRegion = ®ion.Region + + listPluginsResp, err := client.Vault.PluginRegistrationStatus(listPluginsParams, nil) + if err != nil { + return nil, err + } + + return listPluginsResp.Payload, nil +} diff --git a/internal/provider/data_source_vault_cluster.go b/internal/provider/data_source_vault_cluster.go index 8203a908e..4897f6d33 100644 --- a/internal/provider/data_source_vault_cluster.go +++ b/internal/provider/data_source_vault_cluster.go @@ -162,6 +162,46 @@ If a project is not configured in the HCP Provider config block, the oldest proj Type: schema.TypeString, Computed: true, }, + "cloudwatch_access_key_id": { + Description: "CloudWatch access key ID for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_secret_access_key": { + Description: "CloudWatch secret access key for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_region": { + Description: "CloudWatch region for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_namespace": { + Description: "CloudWatch namespace for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_endpoint": { + Description: "ElasticSearch endpoint for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_dataset": { + Description: "ElasticSearch dataset for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_user": { + Description: "ElasticSearch user for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_password": { + Description: "ElasticSearch password for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, }, }, }, @@ -192,6 +232,51 @@ If a project is not configured in the HCP Provider config block, the oldest proj Type: schema.TypeString, Computed: true, }, + "cloudwatch_access_key_id": { + Description: "CloudWatch access key ID for streaming audit logs", + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_secret_access_key": { + Description: "CloudWatch secret access key for streaming audit logs", + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_region": { + Description: "CloudWatch region for streaming audit logs", + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_stream_name": { + Description: "CloudWatch stream name for the target log stream for audit logs", + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_group_name": { + Description: "CloudWatch group name of the target log stream for audit logs", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_endpoint": { + Description: "ElasticSearch endpoint for streaming audit logs", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_dataset": { + Description: "ElasticSearch dataset for streaming audit logs", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_user": { + Description: "ElasticSearch user for streaming audit logs", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_password": { + Description: "ElasticSearch password for streaming audit logs", + Type: schema.TypeString, + Computed: true, + }, }, }, }, diff --git a/internal/provider/data_source_vault_plugin.go b/internal/provider/data_source_vault_plugin.go new file mode 100644 index 000000000..c7bf51942 --- /dev/null +++ b/internal/provider/data_source_vault_plugin.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "log" + "strings" + + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + vaultmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/stable/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-plugin-sdk/v2/helper/validation" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +func dataSourceVaultPlugin() *schema.Resource { + return &schema.Resource{ + Description: "The Vault plugin data source provides information about an existing HCP Vault plugin", + ReadContext: dataSourceVaultPluginRead, + Timeouts: &schema.ResourceTimeout{ + Default: &defaultVaultPluginTimeout, + }, + Schema: map[string]*schema.Schema{ + // Required inputs + "cluster_id": { + Description: "The ID of the HCP Vault cluster.", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateSlugID, + }, + "plugin_name": { + Description: "The name of the plugin - Valid options for plugin name - 'venafi-pki-backend'", + Type: schema.TypeString, + Required: true, + }, + "plugin_type": { + Description: "The type of the plugin - Valid options for plugin type - 'SECRET', 'AUTH', 'DATABASE'", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateVaultPluginType, + }, + // Optional inputs + "project_id": { + Description: ` +The ID of the HCP project where the HCP Vault cluster is located. +If not specified, the project specified in the HCP Provider config block will be used, if configured. +If a project is not configured in the HCP Provider config block, the oldest project in the organization will be used.`, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + Computed: true, + }, + }, + } +} + +func dataSourceVaultPluginRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + clusterID := d.Get("cluster_id").(string) + pluginName := d.Get("plugin_name").(string) + pluginTypeString := d.Get("plugin_type").(string) + pluginType := vaultmodels.HashicorpCloudVault20201125PluginType(pluginTypeString) + client := meta.(*clients.Client) + + projectID, err := GetProjectID(d.Get("project_id").(string), client.Config.ProjectID) + if err != nil { + return diag.Errorf("unable to retrieve project ID: %v", err) + } + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: projectID, + } + + log.Printf("[INFO] Listing plugins for Vault cluster (%s) [project_id=%s, organization_id=%s]", clusterID, loc.ProjectID, loc.OrganizationID) + + pluginsResp, err := clients.ListPlugins(ctx, client, loc, clusterID) + if err != nil { + log.Printf("[ERROR] Vault cluster (%s) failed to list plugins", clusterID) + return diag.FromErr(err) + } + + found := false + for _, plugin := range pluginsResp.Plugins { + if strings.EqualFold(pluginName, plugin.PluginName) && pluginType == *plugin.PluginType && plugin.IsRegistered { + found = true + d.SetId(vaultPluginResourceID(projectID, clusterID, pluginTypeString, pluginName)) + break + } + } + + // If Plugin found, update resource data. + if found { + if err := setVaultPluginResourceData(d, projectID, clusterID, pluginName, pluginTypeString); err != nil { + return diag.FromErr(err) + } + return nil + } else { + return diag.Errorf("unable to retrieve registered plugin: %s", pluginName) + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 741137aaa..acc71f852 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -41,6 +41,7 @@ func New() func() *schema.Provider { "hcp_packer_run_task": dataSourcePackerRunTask(), "hcp_vault_cluster": dataSourceVaultCluster(), "hcp_vault_secrets_app": dataSourceVaultSecretsApp(), + "hcp_vault_plugin": dataSourceVaultPlugin(), }, ResourcesMap: map[string]*schema.Resource{ "hcp_aws_network_peering": resourceAwsNetworkPeering(), @@ -58,6 +59,7 @@ func New() func() *schema.Provider { "hcp_packer_run_task": resourcePackerRunTask(), "hcp_vault_cluster": resourceVaultCluster(), "hcp_vault_cluster_admin_token": resourceVaultClusterAdminToken(), + "hcp_vault_plugin": resourceVaultPlugin(), }, Schema: map[string]*schema.Schema{ "client_id": { diff --git a/internal/provider/resource_vault_cluster.go b/internal/provider/resource_vault_cluster.go index 895c5b3f2..69e440df0 100644 --- a/internal/provider/resource_vault_cluster.go +++ b/internal/provider/resource_vault_cluster.go @@ -192,6 +192,48 @@ If a project is not configured in the HCP Provider config block, the oldest proj Type: schema.TypeString, Optional: true, }, + "cloudwatch_access_key_id": { + Description: "CloudWatch access key ID for streaming metrics", + Type: schema.TypeString, + Optional: true, + }, + "cloudwatch_secret_access_key": { + Description: "CloudWatch secret access key for streaming metrics", + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + "cloudwatch_region": { + Description: "CloudWatch region for streaming metrics", + Type: schema.TypeString, + Optional: true, + }, + "cloudwatch_namespace": { + Description: "CloudWatch namespace for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_endpoint": { + Description: "ElasticSearch endpoint for streaming metrics", + Type: schema.TypeString, + Optional: true, + }, + "elasticsearch_dataset": { + Description: "ElasticSearch dataset for streaming metrics", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_user": { + Description: "ElasticSearch user for streaming metrics", + Type: schema.TypeString, + Optional: true, + }, + "elasticsearch_password": { + Description: "ElasticSearch password for streaming metrics", + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, }, }, }, @@ -240,6 +282,53 @@ If a project is not configured in the HCP Provider config block, the oldest proj Type: schema.TypeString, Optional: true, }, + "cloudwatch_access_key_id": { + Description: "CloudWatch access key ID for streaming audit logs", + Type: schema.TypeString, + Optional: true, + }, + "cloudwatch_secret_access_key": { + Description: "CloudWatch secret access key for streaming audit logs", + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + "cloudwatch_region": { + Description: "CloudWatch region for streaming audit logs", + Type: schema.TypeString, + Optional: true, + }, + "cloudwatch_stream_name": { + Description: "CloudWatch stream name for the target log stream for audit logs", + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_group_name": { + Description: "CloudWatch group name of the target log stream for audit logs", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_endpoint": { + Description: "ElasticSearch endpoint for streaming audit logs", + Type: schema.TypeString, + Optional: true, + }, + "elasticsearch_dataset": { + Description: "ElasticSearch dataset for streaming audit logs", + Type: schema.TypeString, + Computed: true, + }, + "elasticsearch_user": { + Description: "ElasticSearch user for streaming audit logs", + Type: schema.TypeString, + Optional: true, + }, + "elasticsearch_password": { + Description: "ElasticSearch password for streaming audit logs", + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, }, }, }, @@ -1031,6 +1120,46 @@ func flattenObservabilityConfig(config *vaultmodels.HashicorpCloudVault20201125O } } } + + if cloudwatch := config.Cloudwatch; cloudwatch != nil { + configMap["cloudwatch_access_key_id"] = cloudwatch.AccessKeyID + configMap["cloudwatch_region"] = cloudwatch.Region + // ensure we only set properties that are defined in metrics/audit-logs streaming + if propertyName == "metrics_config" { + // Namespace is only used for streaming metrics + configMap["cloudwatch_namespace"] = cloudwatch.Namespace + } else { + // Stream name and group name are only used for streaming audit-logs + configMap["cloudwatch_stream_name"] = cloudwatch.StreamName + configMap["cloudwatch_group_name"] = cloudwatch.GroupName + } + // Since the API return this sensitive fields as redacted, we don't update it on the config in this situations + if cloudwatch.SecretAccessKey != "redacted" { + configMap["cloudwatch_secret_access_key"] = cloudwatch.SecretAccessKey + } else { + if configParam, ok := d.GetOk(propertyName); ok && len(configParam.([]interface{})) > 0 { + config := configParam.([]interface{})[0].(map[string]interface{}) + configMap["cloudwatch_secret_access_key"] = config["cloudwatch_secret_access_key"].(string) + } + } + + if elasticsearch := config.Elasticsearch; elasticsearch != nil { + configMap["elasticsearch_endpoint"] = elasticsearch.Endpoint + configMap["elasticsearch_dataset"] = elasticsearch.Dataset + configMap["elasticsearch_user"] = elasticsearch.User + + // Since the API return this sensitive fields as redacted, we don't update it on the config in this situations + if elasticsearch.Password != "redacted" { + configMap["elasticsearch_password"] = elasticsearch.Password + } else { + if configParam, ok := d.GetOk(propertyName); ok && len(configParam.([]interface{})) > 0 { + config := configParam.([]interface{})[0].(map[string]interface{}) + configMap["elasticsearch_password"] = config["elasticsearch_password"].(string) + } + } + } + } + return []interface{}{configMap} } @@ -1040,9 +1169,11 @@ func getObservabilityConfig(propertyName string, d *schema.ResourceData) (*vault } emptyConfig := vaultmodels.HashicorpCloudVault20201125ObservabilityConfig{ - Grafana: &vaultmodels.HashicorpCloudVault20201125Grafana{}, - Splunk: &vaultmodels.HashicorpCloudVault20201125Splunk{}, - Datadog: &vaultmodels.HashicorpCloudVault20201125Datadog{}, + Grafana: &vaultmodels.HashicorpCloudVault20201125Grafana{}, + Splunk: &vaultmodels.HashicorpCloudVault20201125Splunk{}, + Datadog: &vaultmodels.HashicorpCloudVault20201125Datadog{}, + Cloudwatch: &vaultmodels.HashicorpCloudVault20201125CloudWatch{}, + Elasticsearch: &vaultmodels.HashicorpCloudVault20201125Elasticsearch{}, } // If we don't find the property we return the empty object to be updated and delete the configuration. @@ -1063,53 +1194,109 @@ func getObservabilityConfig(propertyName string, d *schema.ResourceData) (*vault } func getValidObservabilityConfig(config map[string]interface{}) (*vaultmodels.HashicorpCloudVault20201125ObservabilityConfig, diag.Diagnostics) { - - observabilityConfig := vaultmodels.HashicorpCloudVault20201125ObservabilityConfig{} - - grafanaEndpoint := config["grafana_endpoint"].(string) - grafanaUser := config["grafana_user"].(string) - grafanaPassword := config["grafana_password"].(string) - splunkEndpoint := config["splunk_hecendpoint"].(string) - splunkToken := config["splunk_token"].(string) - datadogAPIKey := config["datadog_api_key"].(string) - datadogRegion := config["datadog_region"].(string) + grafanaEndpoint, _ := config["grafana_endpoint"].(string) + grafanaUser, _ := config["grafana_user"].(string) + grafanaPassword, _ := config["grafana_password"].(string) + splunkEndpoint, _ := config["splunk_hecendpoint"].(string) + splunkToken, _ := config["splunk_token"].(string) + datadogAPIKey, _ := config["datadog_api_key"].(string) + datadogRegion, _ := config["datadog_region"].(string) + cloudwatchAccessKeyID, _ := config["cloudwatch_access_key_id"].(string) + cloudwatchAccessKeySecret, _ := config["cloudwatch_secret_access_key"].(string) + cloudwatchRegion, _ := config["cloudwatch_region"].(string) + elasticsearchEndpoint, _ := config["elasticsearch_endpoint"].(string) + elasticsearchUser, _ := config["elasticsearch_user"].(string) + elasticsearchPassword, _ := config["elasticsearch_password"].(string) + + var observabilityConfig *vaultmodels.HashicorpCloudVault20201125ObservabilityConfig + // only return an error about a missing field for a specific provider after ensuring there's a single provider + var missingParamErr diag.Diagnostics + tooManyProvidersErr := diag.Errorf("multiple configurations found: must contain configuration for only one provider") if grafanaEndpoint != "" || grafanaUser != "" || grafanaPassword != "" { if grafanaEndpoint == "" || grafanaUser == "" || grafanaPassword == "" { - return nil, diag.Errorf("grafana configuration is invalid: configuration information missing") - } else if splunkEndpoint != "" || splunkToken != "" || datadogAPIKey != "" || datadogRegion != "" { - return nil, diag.Errorf("multiple configurations found: must contain configuration for only one provider") + missingParamErr = diag.Errorf("grafana configuration is invalid: configuration information missing") } - observabilityConfig.Grafana = &vaultmodels.HashicorpCloudVault20201125Grafana{ - Endpoint: grafanaEndpoint, - User: grafanaUser, - Password: grafanaPassword, + + observabilityConfig = &vaultmodels.HashicorpCloudVault20201125ObservabilityConfig{ + Grafana: &vaultmodels.HashicorpCloudVault20201125Grafana{ + Endpoint: grafanaEndpoint, + User: grafanaUser, + Password: grafanaPassword, + }, } } if splunkEndpoint != "" || splunkToken != "" { + if observabilityConfig != nil { + return nil, tooManyProvidersErr + } if splunkEndpoint == "" || splunkToken == "" { - return nil, diag.Errorf("splunk configuration is invalid: configuration information missing") - } else if datadogAPIKey != "" || datadogRegion != "" { - return nil, diag.Errorf("multiple configurations found: must contain configuration for only one provider") + missingParamErr = diag.Errorf("splunk configuration is invalid: configuration information missing") } - observabilityConfig.Splunk = &vaultmodels.HashicorpCloudVault20201125Splunk{ - HecEndpoint: splunkEndpoint, - Token: splunkToken, + observabilityConfig = &vaultmodels.HashicorpCloudVault20201125ObservabilityConfig{ + Splunk: &vaultmodels.HashicorpCloudVault20201125Splunk{ + HecEndpoint: splunkEndpoint, + Token: splunkToken, + }, } } if datadogAPIKey != "" || datadogRegion != "" { + if observabilityConfig != nil { + return nil, tooManyProvidersErr + } if datadogAPIKey == "" || datadogRegion == "" { - return nil, diag.Errorf("datadog configuration is invalid: configuration information missing") + missingParamErr = diag.Errorf("datadog configuration is invalid: configuration information missing") + } + observabilityConfig = &vaultmodels.HashicorpCloudVault20201125ObservabilityConfig{ + Datadog: &vaultmodels.HashicorpCloudVault20201125Datadog{ + APIKey: datadogAPIKey, + Region: datadogRegion, + }, + } + } + + if cloudwatchAccessKeyID != "" || cloudwatchAccessKeySecret != "" || cloudwatchRegion != "" { + if observabilityConfig != nil { + return nil, tooManyProvidersErr + } + if cloudwatchAccessKeyID == "" || cloudwatchAccessKeySecret == "" || cloudwatchRegion == "" { + missingParamErr = diag.Errorf("cloudwatch configuration is invalid: configuration information missing") } - observabilityConfig.Datadog = &vaultmodels.HashicorpCloudVault20201125Datadog{ - APIKey: datadogAPIKey, - Region: datadogRegion, + observabilityConfig = &vaultmodels.HashicorpCloudVault20201125ObservabilityConfig{ + Cloudwatch: &vaultmodels.HashicorpCloudVault20201125CloudWatch{ + AccessKeyID: cloudwatchAccessKeyID, + Region: cloudwatchRegion, + SecretAccessKey: cloudwatchAccessKeySecret, + // other fields are only set by the external provider + }, + } + } + + if elasticsearchEndpoint != "" || elasticsearchUser != "" || elasticsearchPassword != "" { + if observabilityConfig != nil { + return nil, tooManyProvidersErr + } + + if elasticsearchEndpoint == "" || elasticsearchUser == "" || elasticsearchPassword == "" { + missingParamErr = diag.Errorf("elasticsearch configuration is invalid: configuration information missing") } + + observabilityConfig = &vaultmodels.HashicorpCloudVault20201125ObservabilityConfig{ + Elasticsearch: &vaultmodels.HashicorpCloudVault20201125Elasticsearch{ + Endpoint: elasticsearchEndpoint, + User: elasticsearchUser, + Password: elasticsearchPassword, + }, + } + } + + if missingParamErr != nil { + return nil, missingParamErr } - return &observabilityConfig, nil + return observabilityConfig, nil } func getMajorVersionUpgradeConfig(d *schema.ResourceData) (*vaultmodels.HashicorpCloudVault20201125MajorVersionUpgradeConfig, diag.Diagnostics) { diff --git a/internal/provider/resource_vault_cluster_config_test.go b/internal/provider/resource_vault_cluster_config_test.go index 5bf830569..b00309814 100644 --- a/internal/provider/resource_vault_cluster_config_test.go +++ b/internal/provider/resource_vault_cluster_config_test.go @@ -11,74 +11,80 @@ import ( ) func TestGetValidObservabilityConfig(t *testing.T) { - cases := []struct { + cases := map[string]struct { config map[string]interface{} expectedError string }{ - { + "multiple providers not allowed": { config: map[string]interface{}{ - "grafana_user": "test", - "grafana_password": "pwd", - "grafana_endpoint": "https://grafana", - "splunk_hecendpoint": "https://http-input-splunkcloud.com", - "splunk_token": "test", - "datadog_api_key": "test_datadog", - "datadog_region": "us1", + "grafana_user": "test", + "grafana_password": "pwd", + "grafana_endpoint": "https://grafana", + "splunk_hecendpoint": "https://http-input-splunkcloud.com", + "splunk_token": "test", + "datadog_api_key": "test_datadog", + "datadog_region": "us1", + "elasticsearch_user": "test", + "elasticsearch_password": "test_elasticsearch", + "elasticsearch_endpoint": "https://elasticsearch", }, expectedError: "multiple configurations found: must contain configuration for only one provider", }, - { + "grafana missing params": { config: map[string]interface{}{ - "grafana_user": "test", - "grafana_password": "", - "grafana_endpoint": "", - "splunk_hecendpoint": "", - "splunk_token": "", - "datadog_api_key": "", - "datadog_region": "", + "grafana_user": "test", }, expectedError: "grafana configuration is invalid: configuration information missing", }, - { + "splunk missing params": { config: map[string]interface{}{ - "grafana_user": "", - "grafana_password": "", - "grafana_endpoint": "", - "splunk_hecendpoint": "", - "splunk_token": "test", - "datadog_api_key": "", - "datadog_region": "", + "splunk_token": "test", }, expectedError: "splunk configuration is invalid: configuration information missing", }, - { + "datadog missing params": { config: map[string]interface{}{ - "grafana_user": "", - "grafana_password": "", - "grafana_endpoint": "", - "splunk_hecendpoint": "", - "splunk_token": "", - "datadog_api_key": "", - "datadog_region": "us1", + "datadog_region": "us1", }, expectedError: "datadog configuration is invalid: configuration information missing", }, + "cloudwatch missing params": { + config: map[string]interface{}{ + "cloudwatch_access_key_id": "1111111", + }, + expectedError: "cloudwatch configuration is invalid: configuration information missing", + }, + "elasticsearch missing params": { + config: map[string]interface{}{ + "elasticsearch_user": "test", + }, + expectedError: "elasticsearch configuration is invalid: configuration information missing", + }, + "too many providers takes precedence over missing params": { + config: map[string]interface{}{ + "datadog_region": "us1", + "cloudwatch_access_key_id": "1111111", + }, + expectedError: "multiple configurations found: must contain configuration for only one provider", + }, } - for _, c := range cases { - _, diags := getValidObservabilityConfig(c.config) - foundError := false - if diags.HasError() { - for _, d := range diags { - if strings.Contains(d.Summary, c.expectedError) { - foundError = true - break + for tcName, c := range cases { + t.Run(tcName, func(t *testing.T) { + _, diags := getValidObservabilityConfig(c.config) + foundError := false + if diags.HasError() { + for _, d := range diags { + if strings.Contains(d.Summary, c.expectedError) { + foundError = true + break + } } } - } - if !foundError { - t.Fatalf("Expected an error: %v", c.expectedError) - } + if !foundError { + t.Fatalf("Expected an error: %v", c.expectedError) + } + }) } } diff --git a/internal/provider/resource_vault_plugin.go b/internal/provider/resource_vault_plugin.go new file mode 100644 index 000000000..5a0c009ca --- /dev/null +++ b/internal/provider/resource_vault_plugin.go @@ -0,0 +1,312 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + vaultmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/stable/2020-11-25/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "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" +) + +// defaultClusterTimeout is the amount of time that can elapse +// before a vault plugin operation should timeout. +var defaultVaultPluginTimeout = time.Minute * 1 + +func resourceVaultPlugin() *schema.Resource { + return &schema.Resource{ + Description: "The Vault plugin resource allows you to manage an HCP Vault plugin.", + CreateContext: resourceVaultPluginCreate, + ReadContext: resourceVaultPluginRead, + UpdateContext: resourceVaultPluginUpdate, + DeleteContext: resourceVaultPluginDelete, + Timeouts: &schema.ResourceTimeout{ + Default: &defaultVaultPluginTimeout, + }, + Importer: &schema.ResourceImporter{ + StateContext: resourceVaultPluginImport, + }, + 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, + }, + "plugin_name": { + Description: "The name of the plugin - Valid options for plugin name - 'venafi-pki-backend'", + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: func(_, old, new string, _ *schema.ResourceData) bool { + return strings.EqualFold(old, new) + }, + }, + "plugin_type": { + Description: "The type of the plugin - Valid options for plugin type - 'SECRET', 'AUTH', 'DATABASE'", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateVaultPluginType, + DiffSuppressFunc: func(_, old, new string, _ *schema.ResourceData) bool { + return strings.EqualFold(old, new) + }, + }, + // Optional inputs + "project_id": { + Description: ` +The ID of the HCP project where the HCP Vault cluster is located. +If not specified, the project specified in the HCP Provider config block will be used, if configured. +If a project is not configured in the HCP Provider config block, the oldest project in the organization will be used.`, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IsUUID, + Computed: true, + }, + }, + } +} + +func resourceVaultPluginCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + clusterID := d.Get("cluster_id").(string) + projectID, err := GetProjectID(d.Get("project_id").(string), client.Config.ProjectID) + if err != nil { + return diag.Errorf("unable to retrieve project ID: %v", err) + } + pluginName := d.Get("plugin_name").(string) + pluginType := d.Get("plugin_type").(string) + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: projectID, + } + + log.Printf("[INFO] Adding Vault Plugin (%s) on Vault Cluster (%s) [project_id=%s, organization_id=%s]", pluginName, clusterID, loc.ProjectID, loc.OrganizationID) + + req := &vaultmodels.HashicorpCloudVault20201125AddPluginRequest{PluginName: pluginName, PluginType: pluginType} + _, err = clients.AddPlugin(ctx, client, loc, clusterID, req) + if err != nil { + return diag.Errorf("error adding plugin (%s) to Vault cluster (%s): %v", pluginName, clusterID, err) + } + + d.SetId(vaultPluginResourceID(projectID, clusterID, pluginType, pluginName)) + + if err := setVaultPluginResourceData(d, projectID, clusterID, pluginName, pluginType); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceVaultPluginRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + idParts := strings.SplitN(d.Id(), "/", 8) + + clusterID := idParts[4] + projectID := idParts[2] + pluginName := idParts[7] + pluginTypeString := idParts[6] + pluginType := vaultmodels.HashicorpCloudVault20201125PluginType(pluginTypeString) + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: projectID, + } + + log.Printf("[INFO] Listing plugins for Vault cluster (%s) [project_id=%s, organization_id=%s]", clusterID, loc.ProjectID, loc.OrganizationID) + + pluginsResp, err := clients.ListPlugins(ctx, client, loc, clusterID) + if err != nil { + log.Printf("[ERROR] Vault cluster (%s) failed to list plugins", clusterID) + return diag.FromErr(err) + } + + for _, plugin := range pluginsResp.Plugins { + if strings.EqualFold(pluginName, plugin.PluginName) && pluginType == *plugin.PluginType && plugin.IsRegistered { + // Cluster found, update resource data. + if err := setVaultPluginResourceData(d, loc.ProjectID, clusterID, pluginName, pluginTypeString); err != nil { + return diag.FromErr(err) + } + return nil + } + } + + // if plugin is not registered, remove from state + d.SetId("") + return nil +} + +func resourceVaultPluginUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + clusterID := d.Get("cluster_id").(string) + projectID, err := GetProjectID(d.Get("project_id").(string), client.Config.ProjectID) + pluginName := d.Get("plugin_name").(string) + pluginType := d.Get("plugin_type").(string) + if err != nil { + return diag.Errorf("unable to retrieve project ID: %v", err) + } + + if d.HasChanges("cluster_id", "project_id", "plugin_name", "plugin_type") { + + oldPlugin, _ := d.GetChange("vault_plugin") + + config, ok := oldPlugin.(map[string]interface{}) + if !ok { + return diag.Errorf("could not parse old plugin config: %v", err) + } + + oldPluginName := config["plugin_name"].(string) + oldPluginType := config["plugin_type"].(string) + oldProjectID, err := GetProjectID(config["project_id"].(string), client.Config.ProjectID) + if err != nil { + return diag.Errorf("unable to retrieve project ID: %v", err) + } + oldClusterID := config["cluster_id"].(string) + + oldLoc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: oldProjectID, + } + + log.Printf("[INFO] Deleting Vault Plugin (%s) on Vault Cluster (%s)", pluginName, clusterID) + req := &vaultmodels.HashicorpCloudVault20201125DeletePluginRequest{PluginName: oldPluginName, PluginType: oldPluginType} + _, err = clients.DeletePlugin(ctx, client, oldLoc, clusterID, req) + if err != nil { + return diag.Errorf("error deleting plugin (%s) on Vault cluster (%s): %v", oldPluginName, oldClusterID, err) + } + } + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: projectID, + } + + log.Printf("[INFO] Adding Vault Plugin (%s) on Vault Cluster (%s)", pluginName, clusterID) + req := &vaultmodels.HashicorpCloudVault20201125AddPluginRequest{PluginName: pluginName, PluginType: pluginType} + _, err = clients.AddPlugin(ctx, client, loc, clusterID, req) + if err != nil { + return diag.Errorf("error adding plugin (%s) to Vault cluster (%s): %v", pluginName, clusterID, err) + } + + if err := setVaultPluginResourceData(d, projectID, clusterID, pluginName, pluginType); err != nil { + return diag.FromErr(err) + } + return nil +} + +func resourceVaultPluginDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + clusterID := d.Get("cluster_id").(string) + projectID, err := GetProjectID(d.Get("project_id").(string), client.Config.ProjectID) + pluginName := d.Get("plugin_name").(string) + pluginType := d.Get("plugin_type").(string) + if err != nil { + return diag.Errorf("unable to retrieve project ID: %v", err) + } + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: projectID, + } + + log.Printf("[INFO] Deleting Vault Plugin (%s) on Vault Cluster (%s)", pluginName, clusterID) + + req := &vaultmodels.HashicorpCloudVault20201125DeletePluginRequest{PluginName: pluginName, PluginType: pluginType} + _, err = clients.DeletePlugin(ctx, client, loc, clusterID, req) + if err != nil { + return diag.Errorf("error deleting plugin (%s) on Vault cluster (%s): %v", pluginName, pluginType, err) + } + + return nil +} + +// setVaultPluginResourceData sets the KV pairs of the Vault cluster resource schema. +func setVaultPluginResourceData(d *schema.ResourceData, projectID string, clusterID string, pluginName string, pluginType string) error { + if err := d.Set("cluster_id", clusterID); err != nil { + return err + } + + if err := d.Set("project_id", projectID); err != nil { + return err + } + + if err := d.Set("plugin_name", pluginName); err != nil { + return err + } + + if err := d.Set("plugin_type", pluginType); err != nil { + return err + } + + return nil +} + +// resourceHVNRouteImport implements the logic necessary to import an +// un-tracked (by Terraform) HVN route resource into Terraform state. +func resourceVaultPluginImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + // with multi-projects, import arguments must become dynamic: + // use explicit project ID with terraform import: + // terraform import hcp_vault_plugin.test {project_id}:{cluster_id}:{plugin_type}:{plugin_name} + // use default project ID from provider: + // terraform import hcp_vault_plugin.test {cluster_id}:{plugin_type}:{plugin_name} + + client := meta.(*clients.Client) + projectID := "" + clusterID := "" + pluginType := "" + pluginName := "" + var err error + + idParts := strings.SplitN(d.Id(), ":", 4) + if len(idParts) == 4 { // {project_id}:{cluster_id}:{plugin_type}:{plugin_name} + if idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + return nil, fmt.Errorf("unexpected format of ID (%q), expected {project_id}:{cluster_id}:{plugin_type}:{plugin_name}", d.Id()) + } + projectID = idParts[0] + clusterID = idParts[1] + pluginType = idParts[2] + pluginName = idParts[3] + } else if len(idParts) == 3 { // {cluster_id}:{plugin_type}:{plugin_name} + if idParts[0] == "" || idParts[1] == "" { + return nil, fmt.Errorf("unexpected format of ID (%q), expected {cluster_id}:{plugin_type}:{plugin_name}", d.Id()) + } + projectID, err = GetProjectID(projectID, client.Config.ProjectID) + if err != nil { + return nil, fmt.Errorf("unable to retrieve project ID: %v", err) + } + clusterID = idParts[0] + pluginType = idParts[1] + pluginName = idParts[2] + } else { + return nil, fmt.Errorf("unexpected format of ID (%q), expected {cluster_id}:{plugin_type}:{plugin_name} or {project_id}:{cluster_id}:{plugin_type}:{plugin_name}", d.Id()) + } + + d.SetId(vaultPluginResourceID(projectID, clusterID, pluginType, pluginName)) + + return []*schema.ResourceData{d}, nil +} + +func vaultPluginResourceID(projectID string, clusterID string, pluginType string, pluginName string) string { + return fmt.Sprintf("/project/%s/%s/%s/plugin/%s/%s", + projectID, + VaultClusterResourceType, + clusterID, + pluginType, + pluginName) +} diff --git a/internal/provider/resource_vault_plugin_test.go b/internal/provider/resource_vault_plugin_test.go new file mode 100644 index 000000000..836c8e069 --- /dev/null +++ b/internal/provider/resource_vault_plugin_test.go @@ -0,0 +1,188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "strings" + "testing" + + sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + vaultmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/stable/2020-11-25/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + grpcstatus "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +var ( + testAccVaultPluginConfig = fmt.Sprintf(` +resource "hcp_hvn" "test" { + hvn_id = "%s" + cloud_provider = "aws" + region = "us-west-2" +} + +resource "hcp_vault_cluster" "test" { + cluster_id = "%s" + hvn_id = hcp_hvn.test.hvn_id + tier = "DEV" +} + +resource "hcp_vault_plugin" "venafi_plugin" { + cluster_id = hcp_vault_cluster.test.cluster_id + plugin_name = "venafi-pki-backend" + plugin_type = "SECRET" +} +`, addTimestampSuffix("test-hvn-aws-"), addTimestampSuffix("test-cluster-")) + + testAccVaultPluginDataSourceConfig = fmt.Sprintf(`%s + data "hcp_vault_plugin" "test" { + cluster_id = hcp_vault_cluster.test.cluster_id + plugin_name = "venafi-pki-backend" + plugin_type = "SECRET" + } +`, testAccVaultPluginConfig) +) + +func TestAccVaultPlugin(t *testing.T) { + resourceName := "hcp_vault_plugin.venafi_plugin" + dataSourceName := "data.hcp_vault_plugin.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t, map[string]bool{"aws": false, "azure": false}) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckVaultPluginDestroy, + + Steps: []resource.TestStep{ + // Testing Create + { + Config: testConfig(testAccVaultPluginConfig), + Check: resource.ComposeTestCheckFunc( + testAccChecVaultPluginExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "plugin_name", "venafi-pki-backend"), + resource.TestCheckResourceAttr(resourceName, "plugin_type", "SECRET"), + ), + }, + // Testing that we can import Vault plugin created in the previous step and that the + // resource terraform state will be exactly the same + { + 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 fmt.Sprintf("%s:%s:%s:%s", + rs.Primary.Attributes["project_id"], + rs.Primary.Attributes["cluster_id"], + rs.Primary.Attributes["plugin_type"], + rs.Primary.Attributes["plugin_name"]), nil + }, + ImportStateVerify: true, + }, + // Testing Read + { + Config: testConfig(testAccVaultPluginConfig), + Check: resource.ComposeTestCheckFunc( + testAccChecVaultPluginExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "plugin_name", "venafi-pki-backend"), + resource.TestCheckResourceAttr(resourceName, "plugin_type", "SECRET"), + ), + }, + // Tests datasource + { + Config: testConfig(testAccVaultPluginDataSourceConfig), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "plugin_type", dataSourceName, "plugin_type"), + resource.TestCheckResourceAttrPair(resourceName, "plugin_name", dataSourceName, "plugin_name"), + resource.TestCheckResourceAttrPair(resourceName, "cluster_id", dataSourceName, "cluster_id"), + resource.TestCheckResourceAttrPair(resourceName, "project_id", dataSourceName, "project_id"), + ), + }, + }, + }) +} + +func testAccChecVaultPluginExists(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) + + isRegistered, err := isPluginRegistered(client, id) + if err != nil { + return err + } + + if !isRegistered { + return fmt.Errorf("unable to find plugin: %q", id) + } + + return nil + } +} + +func testAccCheckVaultPluginDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*clients.Client) + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "hcp_vault_plugin": + id := rs.Primary.ID + isRegistered, err := isPluginRegistered(client, id) + if err != nil { + return err + } + if isRegistered { + return fmt.Errorf("plugin status is still reporting that plugin is registered: %s", id) + } + default: + continue + } + } + return nil +} + +func isPluginRegistered(client *clients.Client, id string) (bool, error) { + idParts := strings.SplitN(id, "/", 8) + + clusterID := idParts[4] + pluginType := vaultmodels.HashicorpCloudVault20201125PluginType(idParts[6]) + pluginName := idParts[7] + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: client.Config.ProjectID, + } + + pluginsResp, err := clients.ListPlugins(context.Background(), client, loc, clusterID) + if err != nil { + // if cluster is deleted, plugin doesn't exist + if clients.IsResponseCodeNotFound(err) { + return false, nil + } + return false, fmt.Errorf("unable to list plugins %q: %v. code: %d", id, err, grpcstatus.Code(err)) + } + + for _, plugin := range pluginsResp.Plugins { + if strings.EqualFold(pluginName, plugin.PluginName) && pluginType == *plugin.PluginType && plugin.IsRegistered { + return true, nil + } + } + + return false, nil +} diff --git a/internal/provider/validators.go b/internal/provider/validators.go index 1315931fb..f27379218 100644 --- a/internal/provider/validators.go +++ b/internal/provider/validators.go @@ -12,6 +12,7 @@ import ( "github.com/go-openapi/strfmt" "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/go-version" consulmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-consul-service/stable/2021-02-04/models" vaultmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-service/stable/2020-11-25/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -95,8 +96,7 @@ func validateStringInSlice(valid []string, ignoreCase bool) schema.SchemaValidat // validateSemVer ensures a specified string is a SemVer. func validateSemVer(v interface{}, path cty.Path) diag.Diagnostics { var diagnostics diag.Diagnostics - - if !regexp.MustCompile(`^v?\d+.\d+.\d+$`).MatchString(v.(string)) { + if _, err := version.NewSemver(v.(string)); err != nil { msg := "must be a valid semver" diagnostics = append(diagnostics, diag.Diagnostic{ Severity: diag.Error, @@ -471,3 +471,28 @@ func validateBoundaryPassword(v interface{}, path cty.Path) diag.Diagnostics { return diagnostics } + +func validateVaultPluginType(v interface{}, path cty.Path) diag.Diagnostics { + var diagnostics diag.Diagnostics + + err := vaultmodels.HashicorpCloudVault20201125PluginType(strings.ToUpper(v.(string))).Validate(strfmt.Default) + if err != nil { + enumList := regexp.MustCompile(`\[.*\]`).FindString(err.Error()) + expectedEnumList := strings.ToLower(enumList) + // Remove invalid option from allowed list in error message + expectedEnumList = strings.ReplaceAll( + expectedEnumList, + strings.ToLower(string(vaultmodels.HashicorpCloudVault20201125PluginTypePLUGINTYPEINVALID)), + "", + ) + msg := fmt.Sprintf("expected '%v' to be one of: %v", v, expectedEnumList) + diagnostics = append(diagnostics, diag.Diagnostic{ + Severity: diag.Error, + Summary: msg, + Detail: msg + " (value is case-insensitive).", + AttributePath: path, + }) + } + + return diagnostics +} diff --git a/internal/provider/validators_test.go b/internal/provider/validators_test.go index fb54a91f7..fd1a524cc 100644 --- a/internal/provider/validators_test.go +++ b/internal/provider/validators_test.go @@ -102,8 +102,12 @@ func Test_validateSemVer(t *testing.T) { input: "1.2.3", expected: nil, }, + "valid pre-release semver": { + input: "1.2.3-rc", + expected: nil, + }, "invalid semver": { - input: "v1.2.3.4.5", + input: "v1.2.3.beta", expected: diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error,