From 8406144b0653f12e19b81ef08f7d9341285fd911 Mon Sep 17 00:00:00 2001 From: Daniel Huckins Date: Wed, 9 Oct 2024 13:25:51 -0400 Subject: [PATCH 01/11] update hcp-sdk-go --- go.mod | 2 +- go.sum | 2 ++ .../provider/vaultsecrets/rotating_secret_mongodb_atlas.go | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 95fb77de3..32412af0e 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hcp-sdk-go v0.115.0 + github.com/hashicorp/hcp-sdk-go v0.116.0 github.com/hashicorp/terraform-plugin-docs v0.19.4 github.com/hashicorp/terraform-plugin-framework v1.5.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 diff --git a/go.sum b/go.sum index 68280d437..2d9143208 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5R github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/hashicorp/hcp-sdk-go v0.115.0 h1:q6viFNFPd4H4cHm/B9KGYvkpkT5ZSBQASh9KR/zYHEI= github.com/hashicorp/hcp-sdk-go v0.115.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/hcp-sdk-go v0.116.0 h1:WhmEzOxoswQsX0s8Hk84RE1avu+rwV2e51R8uOb9ZhY= +github.com/hashicorp/hcp-sdk-go v0.116.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= 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.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= diff --git a/internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go b/internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go index fa6f48ccf..98f22b1f0 100644 --- a/internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go +++ b/internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go @@ -44,10 +44,10 @@ func (s *mongoDBAtlasRotatingSecret) create(ctx context.Context, client secret_s WithAppName(secret.AppName.ValueString()). WithBody(&secretmodels.SecretServiceCreateMongoDBAtlasRotatingSecretBody{ IntegrationName: secret.IntegrationName.ValueString(), - MongodbGroupID: secret.MongoDBAtlasUser.ProjectID.ValueString(), // Group ID must be at this level, not in the secret details RotationPolicyName: secret.RotationPolicyName.ValueString(), SecretDetails: &secretmodels.Secrets20231128MongoDBAtlasSecretDetails{ - MongodbRoles: secret.mongoDBRoles, + MongodbGroupID: secret.MongoDBAtlasUser.ProjectID.ValueString(), // Group ID must be at this level, not in the secret details + MongodbRoles: secret.mongoDBRoles, }, SecretName: secret.Name.ValueString(), }), From 4e83b2b53a094ae9f4c96101e917873e3c675be9 Mon Sep 17 00:00:00 2001 From: Daniel Huckins Date: Wed, 9 Oct 2024 13:27:03 -0400 Subject: [PATCH 02/11] remove comment --- internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go b/internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go index 98f22b1f0..d72b99e46 100644 --- a/internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go +++ b/internal/provider/vaultsecrets/rotating_secret_mongodb_atlas.go @@ -46,7 +46,7 @@ func (s *mongoDBAtlasRotatingSecret) create(ctx context.Context, client secret_s IntegrationName: secret.IntegrationName.ValueString(), RotationPolicyName: secret.RotationPolicyName.ValueString(), SecretDetails: &secretmodels.Secrets20231128MongoDBAtlasSecretDetails{ - MongodbGroupID: secret.MongoDBAtlasUser.ProjectID.ValueString(), // Group ID must be at this level, not in the secret details + MongodbGroupID: secret.MongoDBAtlasUser.ProjectID.ValueString(), MongodbRoles: secret.mongoDBRoles, }, SecretName: secret.Name.ValueString(), From 16232da008a4bfe518879497f936476951b31366 Mon Sep 17 00:00:00 2001 From: Daniel Huckins Date: Wed, 9 Oct 2024 13:28:52 -0400 Subject: [PATCH 03/11] go mod tidy --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 2d9143208..a9e3f83ca 100644 --- a/go.sum +++ b/go.sum @@ -122,8 +122,6 @@ github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC16 github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA= github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= -github.com/hashicorp/hcp-sdk-go v0.115.0 h1:q6viFNFPd4H4cHm/B9KGYvkpkT5ZSBQASh9KR/zYHEI= -github.com/hashicorp/hcp-sdk-go v0.115.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/hashicorp/hcp-sdk-go v0.116.0 h1:WhmEzOxoswQsX0s8Hk84RE1avu+rwV2e51R8uOb9ZhY= github.com/hashicorp/hcp-sdk-go v0.116.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= From 64ea8ce332dd84c587c6fabdb4b08e3db3d17498 Mon Sep 17 00:00:00 2001 From: Daniel Huckins Date: Wed, 9 Oct 2024 13:30:59 -0400 Subject: [PATCH 04/11] add changelog --- .changelog/1113.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/1113.txt diff --git a/.changelog/1113.txt b/.changelog/1113.txt new file mode 100644 index 000000000..ad9a63cf2 --- /dev/null +++ b/.changelog/1113.txt @@ -0,0 +1,3 @@ +```release-note:improvement +Upgrade the HCP SDK and fix breakign change w/ Vault Secrets +``` From d80fbb528fcb0a5ec334c96d684addb0d1931a32 Mon Sep 17 00:00:00 2001 From: Daniel Huckins Date: Wed, 9 Oct 2024 13:35:21 -0400 Subject: [PATCH 05/11] spelling error --- .changelog/1113.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/1113.txt b/.changelog/1113.txt index ad9a63cf2..63a838948 100644 --- a/.changelog/1113.txt +++ b/.changelog/1113.txt @@ -1,3 +1,3 @@ ```release-note:improvement -Upgrade the HCP SDK and fix breakign change w/ Vault Secrets +Upgrade the HCP SDK and fix breaking change w/ Vault Secrets ``` From 7bd9d3a53ae0181a745a54bf840b80a72f2ba737 Mon Sep 17 00:00:00 2001 From: Daniel Huckins Date: Wed, 9 Oct 2024 14:08:19 -0400 Subject: [PATCH 06/11] update test --- ...a_source_vault_secrets_rotating_secret_test.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/internal/provider/vaultsecrets/data_source_vault_secrets_rotating_secret_test.go b/internal/provider/vaultsecrets/data_source_vault_secrets_rotating_secret_test.go index a2f9c1a5c..75e33ce88 100644 --- a/internal/provider/vaultsecrets/data_source_vault_secrets_rotating_secret_test.go +++ b/internal/provider/vaultsecrets/data_source_vault_secrets_rotating_secret_test.go @@ -69,13 +69,16 @@ func TestAcc_dataSourceVaultSecretsRotatingSecret(t *testing.T) { SecretName: testSecretName, IntegrationName: testIntegrationName, RotationPolicyName: "built-in:30-days-2-active", - MongodbGroupID: mongodbAtlasGroupID, - MongodbRoles: []*secretmodels.Secrets20231128MongoDBRole{ - { - DatabaseName: mongodbAtlasDBName, - RoleName: "read", - CollectionName: "", + SecretDetails: &secretmodels.Secrets20231128MongoDBAtlasSecretDetails{ + MongodbGroupID: mongodbAtlasGroupID, + MongodbRoles: []*secretmodels.Secrets20231128MongoDBRole{ + { + DatabaseName: mongodbAtlasDBName, + RoleName: "read", + CollectionName: "", + }, }, + MongodbScopes: nil, }, } _, err = clients.CreateMongoDBAtlasRotatingSecret(ctx, client, loc, testAppName, &reqBody) From 03c2b23ef41b257f60e1fa1cb9a4646dc8573a6a Mon Sep 17 00:00:00 2001 From: Trent DiBacco Date: Thu, 10 Oct 2024 13:09:06 -0700 Subject: [PATCH 07/11] Implement connection and subscription resources for jira and slack. --- ...vault_radar_integration_jira_connection.md | 47 +++++ ...ult_radar_integration_jira_subscription.md | 59 ++++++ ...ault_radar_integration_slack_connection.md | 43 ++++ ...lt_radar_integration_slack_subscription.md | 51 +++++ .../resource.tf | 11 + .../resource.tf | 21 ++ .../resource.tf | 9 + .../resource.tf | 16 ++ internal/clients/client.go | 6 + internal/clients/vault_radar.go | 130 +++++++++++- internal/provider/provider.go | 4 + .../vaultradar/integration_connection.go | 199 ++++++++++++++++++ .../vaultradar/integration_subscription.go | 193 +++++++++++++++++ ...ource_radar_integration_jira_connection.go | 167 +++++++++++++++ ..._radar_integration_jira_connection_test.go | 51 +++++ ...rce_radar_integration_jira_subscription.go | 191 +++++++++++++++++ ...adar_integration_jira_subscription_test.go | 77 +++++++ ...urce_radar_integration_slack_connection.go | 121 +++++++++++ ...radar_integration_slack_connection_test.go | 45 ++++ ...ce_radar_integration_slack_subscription.go | 141 +++++++++++++ ...dar_integration_slack_subscription_test.go | 56 +++++ ..._radar_integration_jira_connection.md.tmpl | 19 ++ ...adar_integration_jira_subscription.md.tmpl | 19 ++ ...radar_integration_slack_connection.md.tmpl | 19 ++ ...dar_integration_slack_subscription.md.tmpl | 19 ++ 25 files changed, 1707 insertions(+), 7 deletions(-) create mode 100644 docs/resources/vault_radar_integration_jira_connection.md create mode 100644 docs/resources/vault_radar_integration_jira_subscription.md create mode 100644 docs/resources/vault_radar_integration_slack_connection.md create mode 100644 docs/resources/vault_radar_integration_slack_subscription.md create mode 100644 examples/resources/hcp_vault_radar_integration_jira_connection/resource.tf create mode 100644 examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf create mode 100644 examples/resources/hcp_vault_radar_integration_slack_connection/resource.tf create mode 100644 examples/resources/hcp_vault_radar_integration_slack_subscription/resource.tf create mode 100644 internal/provider/vaultradar/integration_connection.go create mode 100644 internal/provider/vaultradar/integration_subscription.go create mode 100644 internal/provider/vaultradar/resource_radar_integration_jira_connection.go create mode 100644 internal/provider/vaultradar/resource_radar_integration_jira_connection_test.go create mode 100644 internal/provider/vaultradar/resource_radar_integration_jira_subscription.go create mode 100644 internal/provider/vaultradar/resource_radar_integration_jira_subscription_test.go create mode 100644 internal/provider/vaultradar/resource_radar_integration_slack_connection.go create mode 100644 internal/provider/vaultradar/resource_radar_integration_slack_connection_test.go create mode 100644 internal/provider/vaultradar/resource_radar_integration_slack_subscription.go create mode 100644 internal/provider/vaultradar/resource_radar_integration_slack_subscription_test.go create mode 100644 templates/resources/vault_radar_integration_jira_connection.md.tmpl create mode 100644 templates/resources/vault_radar_integration_jira_subscription.md.tmpl create mode 100644 templates/resources/vault_radar_integration_slack_connection.md.tmpl create mode 100644 templates/resources/vault_radar_integration_slack_subscription.md.tmpl diff --git a/docs/resources/vault_radar_integration_jira_connection.md b/docs/resources/vault_radar_integration_jira_connection.md new file mode 100644 index 000000000..0a247f025 --- /dev/null +++ b/docs/resources/vault_radar_integration_jira_connection.md @@ -0,0 +1,47 @@ +--- +page_title: "hcp_vault_radar_integration_jira_connection Resource - terraform-provider-hcp" +subcategory: "" +description: |- + This terraform resource manages an Integration Jira Connection in Vault Radar. +--- + +# hcp_vault_radar_integration_jira_connection (Resource) + +-> **Note:** HCP Vault Radar Terraform resources are in preview. + +This terraform resource manages an Integration Jira Connection in Vault Radar. + +## Example Usage + +```terraform +variable "jira_token" { + type = string + sensitive = true +} + +resource "hcp_vault_radar_integration_jira_connection" "jira_connection" { + name = "example connection to jira" + email = "jane.smith@example.com" + token = var.jira_token + base_url = "https://example.atlassian.net" +} +``` + + + +## Schema + +### Required + +- `base_url` (String) The Jira base URL. Example: https://acme.atlassian.net +- `email` (String) Jira user's email. +- `name` (String) Name of connection. Name must be unique. +- `token` (String, Sensitive) A Jira API token. + +### Optional + +- `project_id` (String) The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/vault_radar_integration_jira_subscription.md b/docs/resources/vault_radar_integration_jira_subscription.md new file mode 100644 index 000000000..95d682743 --- /dev/null +++ b/docs/resources/vault_radar_integration_jira_subscription.md @@ -0,0 +1,59 @@ +--- +page_title: "hcp_vault_radar_integration_jira_subscription Resource - terraform-provider-hcp" +subcategory: "" +description: |- + This terraform resource manages an Integration Jira Subscription in Vault Radar. +--- + +# hcp_vault_radar_integration_jira_subscription (Resource) + +-> **Note:** HCP Vault Radar Terraform resources are in preview. + +This terraform resource manages an Integration Jira Subscription in Vault Radar. + +## Example Usage + +```terraform +variable "jira_token" { + type = string + sensitive = true +} + +# A Jira subscription requires a Jira connection. +resource "hcp_vault_radar_integration_jira_connection" "jira_connection" { + name = "example integration jira connection" + email = "jane.smith@example.com" + token = var.jira_token + base_url = "https://example.atlassian.net" +} + +resource "hcp_vault_radar_integration_jira_subscription" "jira_subscription" { + name = "example integration jira subscription" + connection_id = hcp_vault_radar_integration_jira_connection.jira_connection.id + jira_project_key = "SEC" + issue_type = "Task" + assignee = "id-of-assignee" + message = "Example message" +} +``` + + + +## Schema + +### Required + +- `connection_id` (String) id of the integration jira connection to use for the subscription. +- `issue_type` (String) The type of issue to be created from the alert(s). Example: Task +- `jira_project_key` (String) The name of the project under which the jira issue will be created. Example: OPS +- `name` (String) Name of subscription. Name must be unique. + +### Optional + +- `assignee` (String) The identifier of the Jira user who will be assigned the ticket. Example: 1e25fbc8895d5b0c9703c19c +- `message` (String) This message will be included in the ticket description. +- `project_id` (String) The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/vault_radar_integration_slack_connection.md b/docs/resources/vault_radar_integration_slack_connection.md new file mode 100644 index 000000000..ed8170869 --- /dev/null +++ b/docs/resources/vault_radar_integration_slack_connection.md @@ -0,0 +1,43 @@ +--- +page_title: "hcp_vault_radar_integration_slack_connection Resource - terraform-provider-hcp" +subcategory: "" +description: |- + This terraform resource manages an Integration Slack Connection in Vault Radar. +--- + +# hcp_vault_radar_integration_slack_connection (Resource) + +-> **Note:** HCP Vault Radar Terraform resources are in preview. + +This terraform resource manages an Integration Slack Connection in Vault Radar. + +## Example Usage + +```terraform +variable "slack_token" { + type = string + sensitive = true +} + +resource "hcp_vault_radar_integration_slack_connection" "slack_connection" { + name = "example connection to slack" + token = var.slack_token +} +``` + + + +## Schema + +### Required + +- `name` (String) Name of connection. Name must be unique. +- `token` (String, Sensitive) Slack bot user oAuth token. Example: Bot token strings begin with 'xoxb'. + +### Optional + +- `project_id` (String) The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/vault_radar_integration_slack_subscription.md b/docs/resources/vault_radar_integration_slack_subscription.md new file mode 100644 index 000000000..3c421107f --- /dev/null +++ b/docs/resources/vault_radar_integration_slack_subscription.md @@ -0,0 +1,51 @@ +--- +page_title: "hcp_vault_radar_integration_slack_subscription Resource - terraform-provider-hcp" +subcategory: "" +description: |- + This terraform resource manages an Integration Slack Subscription in Vault Radar. +--- + +# hcp_vault_radar_integration_slack_subscription (Resource) + +-> **Note:** HCP Vault Radar Terraform resources are in preview. + +This terraform resource manages an Integration Slack Subscription in Vault Radar. + +## Example Usage + +```terraform +variable "slack_token" { + type = string + sensitive = true +} + +# A Slack subscription requires a Slack connection. +resource "hcp_vault_radar_integration_slack_connection" "slack_connection" { + name = "example connection to slack" + token = var.slack_token +} + +resource "hcp_vault_radar_integration_slack_subscription" "slack_subscription" { + name = "example integration slack subscription" + connection_id = hcp_vault_radar_integration_slack_connection.slack_connection.id + channel = "sec-ops-team" +} +``` + + + +## Schema + +### Required + +- `channel` (String) Slack channel that messages will be sent to. Note that HashiCorp Vault Radar will send a test message to verify channel's name. Example: dev-ops-team +- `connection_id` (String) id of the integration slack connection to use for the subscription. +- `name` (String) Name of subscription. Name must be unique. + +### Optional + +- `project_id` (String) The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/resources/hcp_vault_radar_integration_jira_connection/resource.tf b/examples/resources/hcp_vault_radar_integration_jira_connection/resource.tf new file mode 100644 index 000000000..4c6c56941 --- /dev/null +++ b/examples/resources/hcp_vault_radar_integration_jira_connection/resource.tf @@ -0,0 +1,11 @@ +variable "jira_token" { + type = string + sensitive = true +} + +resource "hcp_vault_radar_integration_jira_connection" "jira_connection" { + name = "example connection to jira" + email = "jane.smith@example.com" + token = var.jira_token + base_url = "https://example.atlassian.net" +} \ No newline at end of file diff --git a/examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf b/examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf new file mode 100644 index 000000000..2aee45cad --- /dev/null +++ b/examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf @@ -0,0 +1,21 @@ +variable "jira_token" { + type = string + sensitive = true +} + +# A Jira subscription requires a Jira connection. +resource "hcp_vault_radar_integration_jira_connection" "jira_connection" { + name = "example integration jira connection" + email = "jane.smith@example.com" + token = var.jira_token + base_url = "https://example.atlassian.net" +} + +resource "hcp_vault_radar_integration_jira_subscription" "jira_subscription" { + name = "example integration jira subscription" + connection_id = hcp_vault_radar_integration_jira_connection.jira_connection.id + jira_project_key = "SEC" + issue_type = "Task" + assignee = "id-of-assignee" + message = "Example message" +} \ No newline at end of file diff --git a/examples/resources/hcp_vault_radar_integration_slack_connection/resource.tf b/examples/resources/hcp_vault_radar_integration_slack_connection/resource.tf new file mode 100644 index 000000000..90367a4d3 --- /dev/null +++ b/examples/resources/hcp_vault_radar_integration_slack_connection/resource.tf @@ -0,0 +1,9 @@ +variable "slack_token" { + type = string + sensitive = true +} + +resource "hcp_vault_radar_integration_slack_connection" "slack_connection" { + name = "example connection to slack" + token = var.slack_token +} diff --git a/examples/resources/hcp_vault_radar_integration_slack_subscription/resource.tf b/examples/resources/hcp_vault_radar_integration_slack_subscription/resource.tf new file mode 100644 index 000000000..aa67f6a70 --- /dev/null +++ b/examples/resources/hcp_vault_radar_integration_slack_subscription/resource.tf @@ -0,0 +1,16 @@ +variable "slack_token" { + type = string + sensitive = true +} + +# A Slack subscription requires a Slack connection. +resource "hcp_vault_radar_integration_slack_connection" "slack_connection" { + name = "example connection to slack" + token = var.slack_token +} + +resource "hcp_vault_radar_integration_slack_subscription" "slack_subscription" { + name = "example integration slack subscription" + connection_id = hcp_vault_radar_integration_slack_connection.slack_connection.id + channel = "sec-ops-team" +} \ No newline at end of file diff --git a/internal/clients/client.go b/internal/clients/client.go index e00801bdc..c7eed2049 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -63,6 +63,8 @@ import ( cloud_vault_radar "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client" radar_src_registration_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/data_source_registration_service" + radar_connection_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/integration_connection_service" + radar_subscription_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/integration_subscription_service" hcpConfig "github.com/hashicorp/hcp-sdk-go/config" sdk "github.com/hashicorp/hcp-sdk-go/httpclient" @@ -92,6 +94,8 @@ type Client struct { LogService log_service.ClientService ResourceService resource_service.ClientService RadarSourceRegistrationService radar_src_registration_service.ClientService + RadarConnectionService radar_connection_service.ClientService + RadarSubscriptionService radar_subscription_service.ClientService } // ClientConfig specifies configuration for the client that interacts with HCP @@ -182,6 +186,8 @@ func NewClient(config ClientConfig) (*Client, error) { Webhook: cloud_webhook.New(httpClient, nil).WebhookService, ResourceService: cloud_resource_manager.New(httpClient, nil).ResourceService, RadarSourceRegistrationService: cloud_vault_radar.New(httpClient, nil).DataSourceRegistrationService, + RadarConnectionService: cloud_vault_radar.New(httpClient, nil).IntegrationConnectionService, + RadarSubscriptionService: cloud_vault_radar.New(httpClient, nil).IntegrationSubscriptionService, } return client, nil diff --git a/internal/clients/vault_radar.go b/internal/clients/vault_radar.go index 2cc391c3f..7b158c82a 100644 --- a/internal/clients/vault_radar.go +++ b/internal/clients/vault_radar.go @@ -5,13 +5,15 @@ import ( "errors" "time" - radar_service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/data_source_registration_service" + dsrs "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/data_source_registration_service" + ics "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/integration_connection_service" + iss "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/integration_subscription_service" "github.com/hashicorp/terraform-plugin-log/tflog" ) -func OnboardRadarSource(ctx context.Context, client *Client, projectID string, source radar_service.OnboardDataSourceBody) (*radar_service.OnboardDataSourceOK, error) { - onboardParams := radar_service.NewOnboardDataSourceParams() +func OnboardRadarSource(ctx context.Context, client *Client, projectID string, source dsrs.OnboardDataSourceBody) (*dsrs.OnboardDataSourceOK, error) { + onboardParams := dsrs.NewOnboardDataSourceParams() onboardParams.Context = ctx onboardParams.LocationProjectID = projectID onboardParams.Body = source @@ -24,8 +26,8 @@ func OnboardRadarSource(ctx context.Context, client *Client, projectID string, s return onboardResp, nil } -func GetRadarSource(ctx context.Context, client *Client, projectID, sourceID string) (*radar_service.GetDataSourceByIDOK, error) { - getParams := radar_service.NewGetDataSourceByIDParams() +func GetRadarSource(ctx context.Context, client *Client, projectID, sourceID string) (*dsrs.GetDataSourceByIDOK, error) { + getParams := dsrs.NewGetDataSourceByIDParams() getParams.Context = ctx getParams.ID = sourceID getParams.LocationProjectID = projectID @@ -41,10 +43,10 @@ func GetRadarSource(ctx context.Context, client *Client, projectID, sourceID str func OffboardRadarSource(ctx context.Context, client *Client, projectID, sourceID string) error { tflog.SetField(ctx, "radar_source_id", sourceID) - deleteParams := radar_service.NewOffboardDataSourceParams() + deleteParams := dsrs.NewOffboardDataSourceParams() deleteParams.Context = ctx deleteParams.LocationProjectID = projectID - deleteParams.Body = radar_service.OffboardDataSourceBody{ + deleteParams.Body = dsrs.OffboardDataSourceBody{ ID: sourceID, } @@ -117,3 +119,117 @@ func waitFor(ctx context.Context, retry, timeout time.Duration, maxConsecutiveEr } } } + +func CreateIntegrationConnection(ctx context.Context, client *Client, projectID string, connection ics.CreateIntegrationConnectionBody) (*ics.CreateIntegrationConnectionOK, error) { + params := ics.NewCreateIntegrationConnectionParams() + params.Context = ctx + params.LocationProjectID = projectID + params.Body = connection + + resp, err := client.RadarConnectionService.CreateIntegrationConnection(params, nil) + if err != nil { + return nil, err + } + + return resp, nil +} + +func GetIntegrationConnectionByID(ctx context.Context, client *Client, projectID, connectionID string) (*ics.GetIntegrationConnectionByIDOK, error) { + params := ics.NewGetIntegrationConnectionByIDParams() + params.Context = ctx + params.ID = connectionID + params.LocationProjectID = projectID + + resp, err := client.RadarConnectionService.GetIntegrationConnectionByID(params, nil) + if err != nil { + return nil, err + } + + return resp, nil +} + +func GetIntegrationConnectionByName(ctx context.Context, client *Client, projectID, connectionName string) (*ics.GetIntegrationConnectionByNameOK, error) { + params := ics.NewGetIntegrationConnectionByNameParams() + params.Context = ctx + params.LocationProjectID = projectID + params.Body = ics.GetIntegrationConnectionByNameBody{ + Name: connectionName, + } + + resp, err := client.RadarConnectionService.GetIntegrationConnectionByName(params, nil) + if err != nil { + return nil, err + } + + return resp, nil +} + +func DeleteIntegrationConnection(ctx context.Context, client *Client, projectID, connectionID string) error { + params := ics.NewDeleteIntegrationConnectionParams() + params.Context = ctx + params.ID = connectionID + params.LocationProjectID = projectID + + if _, err := client.RadarConnectionService.DeleteIntegrationConnection(params, nil); err != nil { + return err + } + + return nil +} + +func CreateIntegrationSubscription(ctx context.Context, client *Client, projectID string, connection iss.CreateIntegrationSubscriptionBody) (*iss.CreateIntegrationSubscriptionOK, error) { + params := iss.NewCreateIntegrationSubscriptionParams() + params.Context = ctx + params.LocationProjectID = projectID + params.Body = connection + + resp, err := client.RadarSubscriptionService.CreateIntegrationSubscription(params, nil) + if err != nil { + return nil, err + } + + return resp, nil +} + +func GetIntegrationSubscriptionByID(ctx context.Context, client *Client, projectID, connectionID string) (*iss.GetIntegrationSubscriptionByIDOK, error) { + params := iss.NewGetIntegrationSubscriptionByIDParams() + params.Context = ctx + params.ID = connectionID + params.LocationProjectID = projectID + + resp, err := client.RadarSubscriptionService.GetIntegrationSubscriptionByID(params, nil) + if err != nil { + return nil, err + } + + return resp, nil +} + +func GetIntegrationSubscriptionByName(ctx context.Context, client *Client, projectID, connectionName string) (*iss.GetIntegrationSubscriptionByNameOK, error) { + params := iss.NewGetIntegrationSubscriptionByNameParams() + params.Context = ctx + params.LocationProjectID = projectID + params.Body = iss.GetIntegrationSubscriptionByNameBody{ + Name: connectionName, + } + + resp, err := client.RadarSubscriptionService.GetIntegrationSubscriptionByName(params, nil) + if err != nil { + return nil, err + } + + return resp, nil +} + +func DeleteIntegrationSubscription(ctx context.Context, client *Client, projectID, connectionID string) error { + params := iss.NewDeleteIntegrationSubscriptionParams() + params.Context = ctx + params.ID = connectionID + params.LocationProjectID = projectID + + if _, err := client.RadarSubscriptionService.DeleteIntegrationSubscription(params, nil); err != nil { + return err + } + + return nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 62e47ec99..db70f0a2a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -181,6 +181,10 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res waypoint.NewTfcConfigResource, // Radar vaultradar.NewSourceGitHubEnterpriseResource, + vaultradar.NewIntegrationJiraConnectionResource, + vaultradar.NewIntegrationJiraSubscriptionResource, + vaultradar.NewIntegrationSlackConnectionResource, + vaultradar.NewIntegrationSlackSubscriptionResource, }, packer.ResourceSchemaBuilders...) } diff --git a/internal/provider/vaultradar/integration_connection.go b/internal/provider/vaultradar/integration_connection.go new file mode 100644 index 000000000..8db87dd74 --- /dev/null +++ b/internal/provider/vaultradar/integration_connection.go @@ -0,0 +1,199 @@ +package vaultradar + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/modifiers" + + service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/integration_connection_service" +) + +var ( + _ resource.Resource = &integrationConnectionResource{} + _ resource.ResourceWithConfigure = &integrationConnectionResource{} +) + +// integrationConnectionResource is an implementation for configuring specific types of integration connections. +// Examples: hcp_vault_radar_integration_jira_connection and hcp_vault_radar_integration_slack_connection make use of +// this implementation to define resources with specific schemas, validation, and state details related to their types. +type integrationConnectionResource struct { + client *clients.Client + TypeName string + IntegrationType string + ConnectionSchema schema.Schema + GetPlan func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) + GetState func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) +} + +// integrationConnection is the minimal plan/state that a connection must have. +// Specifics to the type of connection should use the GetAuthKey and Get/Set Details for specific plan and state. +type integrationConnection interface { + GetID() types.String + SetID(types.String) + GetProjectID() types.String + SetProjectID(types.String) + GetName() types.String + SetName(types.String) + GetAuthKey() (string, diag.Diagnostics) + GetDetails() (string, diag.Diagnostics) + SetDetails(string) diag.Diagnostics +} + +func (r *integrationConnectionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + r.TypeName +} + +func (r *integrationConnectionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.ConnectionSchema +} + +func (r *integrationConnectionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*clients.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.client = client +} + +func (r *integrationConnectionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + modifiers.ModifyPlanForDefaultProjectChange(ctx, r.client.Config.ProjectID, req.State, req.Config, req.Plan, resp) +} + +func (r *integrationConnectionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + plan, diags := r.GetPlan(ctx, req.Plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !plan.GetProjectID().IsUnknown() { + projectID = plan.GetProjectID().ValueString() + } + + errSummary := "Error creating Radar Integration Connection" + + // Check for an existing connection with the same name. + existing, err := clients.GetIntegrationConnectionByName(ctx, r.client, projectID, plan.GetName().ValueString()) + if err != nil && !clients.IsResponseCodeNotFound(err) { + resp.Diagnostics.AddError(errSummary, err.Error()) + } + if existing != nil { + resp.Diagnostics.AddError(errSummary, fmt.Sprintf("Connection with name: %q already exists.", plan.GetName().ValueString())) + return + } + + authKey, diags := plan.GetAuthKey() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + details, diags := plan.GetDetails() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + res, err := clients.CreateIntegrationConnection(ctx, r.client, projectID, service.CreateIntegrationConnectionBody{ + IntegrationType: r.IntegrationType, + IsSink: true, + IsSource: false, + Name: plan.GetName().ValueString(), + AuthKey: authKey, + Details: details, + }) + if err != nil { + resp.Diagnostics.AddError(errSummary, err.Error()) + return + } + + plan.SetID(types.StringValue(res.GetPayload().ID)) + plan.SetProjectID(types.StringValue(projectID)) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *integrationConnectionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + state, diags := r.GetState(ctx, req.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !state.GetProjectID().IsUnknown() { + projectID = state.GetProjectID().ValueString() + } + + res, err := clients.GetIntegrationConnectionByID(ctx, r.client, projectID, state.GetID().ValueString()) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + // Resource is no longer on the server. + tflog.Info(ctx, "Radar integration connection not found, removing from state.") + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Unable to get Radar integration connection", err.Error()) + return + } + + state.SetName(types.StringValue(res.GetPayload().Name)) + diags = state.SetDetails(res.GetPayload().Details) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *integrationConnectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + state, diags := r.GetState(ctx, req.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !state.GetProjectID().IsUnknown() { + projectID = state.GetProjectID().ValueString() + } + + // Assert resource still exists. + if _, err := clients.GetIntegrationConnectionByID(ctx, r.client, projectID, state.GetID().ValueString()); err != nil { + if clients.IsResponseCodeNotFound(err) { + // Resource is no longer on the server. + tflog.Info(ctx, "Radar integration connection not found, removing from state.") + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Unable to get Radar integration connection", err.Error()) + return + } + + if err := clients.DeleteIntegrationConnection(ctx, r.client, projectID, state.GetID().ValueString()); err != nil { + resp.Diagnostics.AddError("Unable to delete Radar integration connection", err.Error()) + return + } +} + +func (r *integrationConnectionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // In-place update is not supported. + // Plans to support updating the token will be in a future iteration. + resp.Diagnostics.AddError("Unexpected provider error", "This is an internal error, please report this issue to the provider developers") +} diff --git a/internal/provider/vaultradar/integration_subscription.go b/internal/provider/vaultradar/integration_subscription.go new file mode 100644 index 000000000..113edecb0 --- /dev/null +++ b/internal/provider/vaultradar/integration_subscription.go @@ -0,0 +1,193 @@ +package vaultradar + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/modifiers" + + service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/integration_subscription_service" +) + +var ( + _ resource.Resource = &integrationSubscriptionResource{} + _ resource.ResourceWithConfigure = &integrationSubscriptionResource{} +) + +// integrationSubscriptionResource is an implementation for configuring specific types of integration subscriptions. +// Examples: hcp_vault_radar_integration_jira_subscription and hcp_vault_radar_integration_slack_subscription make use of +// this implementation to define resources with specific schemas, validation, and state details related to their types. +type integrationSubscriptionResource struct { + client *clients.Client + TypeName string + SubscriptionSchema schema.Schema + GetPlan func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) + GetState func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) +} + +// integrationSubscription is the minimal plan/state that a subscription must have. +// Specifics to the type of subscription should use the Get/Set Details for specific plan and state. +type integrationSubscription interface { + GetID() types.String + SetID(types.String) + GetProjectID() types.String + SetProjectID(types.String) + GetName() types.String + SetName(types.String) + GetConnectionID() types.String + SetConnectionID(types.String) + GetDetails() (string, diag.Diagnostics) + SetDetails(string) diag.Diagnostics +} + +func (r *integrationSubscriptionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + r.TypeName +} + +func (r *integrationSubscriptionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.SubscriptionSchema +} + +func (r *integrationSubscriptionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*clients.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.client = client +} + +func (r *integrationSubscriptionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + modifiers.ModifyPlanForDefaultProjectChange(ctx, r.client.Config.ProjectID, req.State, req.Config, req.Plan, resp) +} + +func (r *integrationSubscriptionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + plan, diags := r.GetPlan(ctx, req.Plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !plan.GetProjectID().IsUnknown() { + projectID = plan.GetProjectID().ValueString() + } + + errSummary := "Error creating Radar Integration Subscription" + + // Check for an existing subscription with the same name. + existing, err := clients.GetIntegrationSubscriptionByName(ctx, r.client, projectID, plan.GetName().ValueString()) + if err != nil && !clients.IsResponseCodeNotFound(err) { + resp.Diagnostics.AddError(errSummary, err.Error()) + } + if existing != nil { + resp.Diagnostics.AddError(errSummary, fmt.Sprintf("Subscription with name: %q already exists.", plan.GetName().ValueString())) + return + } + + details, diags := plan.GetDetails() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + res, err := clients.CreateIntegrationSubscription(ctx, r.client, projectID, service.CreateIntegrationSubscriptionBody{ + Name: plan.GetName().ValueString(), + ConnectionID: plan.GetConnectionID().ValueString(), + Details: details, + }) + if err != nil { + resp.Diagnostics.AddError(errSummary, err.Error()) + return + } + + plan.SetID(types.StringValue(res.GetPayload().ID)) + plan.SetProjectID(types.StringValue(projectID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *integrationSubscriptionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + state, diags := r.GetState(ctx, req.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !state.GetProjectID().IsUnknown() { + projectID = state.GetProjectID().ValueString() + } + + res, err := clients.GetIntegrationSubscriptionByID(ctx, r.client, projectID, state.GetID().ValueString()) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + // Resource is no longer on the server. + tflog.Info(ctx, "Radar integration subscription not found, removing from state.") + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Unable to get Radar integration subscription", err.Error()) + return + } + + payload := res.GetPayload() + state.SetName(types.StringValue(payload.Name)) + state.SetConnectionID(types.StringValue(payload.ConnectionID)) + + diags = state.SetDetails(res.GetPayload().Details) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *integrationSubscriptionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + state, diags := r.GetState(ctx, req.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !state.GetProjectID().IsUnknown() { + projectID = state.GetProjectID().ValueString() + } + + // Assert resource still exists. + if _, err := clients.GetIntegrationSubscriptionByID(ctx, r.client, projectID, state.GetID().ValueString()); err != nil { + if clients.IsResponseCodeNotFound(err) { + // Resource is no longer on the server. + tflog.Info(ctx, "Radar integration subscription not found, removing from state.") + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("Unable to get Radar integration subscription", err.Error()) + return + } + + if err := clients.DeleteIntegrationSubscription(ctx, r.client, projectID, state.GetID().ValueString()); err != nil { + resp.Diagnostics.AddError("Unable to delete Radar integration subscription", err.Error()) + return + } +} + +func (r *integrationSubscriptionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // In-place update is not supported. + // Plans to support updating subscription details will be in a future iteration. + resp.Diagnostics.AddError("Unexpected provider error", "This is an internal error, please report this issue to the provider developers") +} diff --git a/internal/provider/vaultradar/resource_radar_integration_jira_connection.go b/internal/provider/vaultradar/resource_radar_integration_jira_connection.go new file mode 100644 index 000000000..c68087978 --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_jira_connection.go @@ -0,0 +1,167 @@ +package vaultradar + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-hcp/internal/hcpvalidator" +) + +func NewIntegrationJiraConnectionResource() resource.Resource { + return &integrationConnectionResource{ + TypeName: "_vault_radar_integration_jira_connection", + IntegrationType: "jira", + ConnectionSchema: integrationJiraConnectionSchema, + GetPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) { + var conn jiraConnectionResourceData + diags := plan.Get(ctx, &conn) + return &conn, diags + }, + GetState: func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) { + var conn jiraConnectionResourceData + diags := state.Get(ctx, &conn) + return &conn, diags + }, + } +} + +var integrationJiraConnectionSchema = schema.Schema{ + MarkdownDescription: "This terraform resource manages an Integration Jira Connection in Vault Radar.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of this resource.", + }, + "name": schema.StringAttribute{ + Description: "Name of connection. Name must be unique.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "email": schema.StringAttribute{ + Description: `Jira user's email.`, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "token": schema.StringAttribute{ + Description: "A Jira API token.", + Required: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "base_url": schema.StringAttribute{ + Description: "The Jira base URL. Example: https://acme.atlassian.net", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + hcpvalidator.URL(), + }, + }, + + // Optional inputs + "project_id": schema.StringAttribute{ + Description: "The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, +} + +type jiraConnectionResourceData struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Email types.String `tfsdk:"email"` + Token types.String `tfsdk:"token"` + BaseURL types.String `tfsdk:"base_url"` + ProjectID types.String `tfsdk:"project_id"` +} + +type jiraAuthKey struct { + Email string `json:"email"` + Token string `json:"token"` +} + +type jiraConnectionDetails struct { + TenantURL string `json:"tenant_url"` +} + +func (d *jiraConnectionResourceData) GetID() types.String { return d.ID } + +func (d *jiraConnectionResourceData) SetID(id types.String) { d.ID = id } + +func (d *jiraConnectionResourceData) GetProjectID() types.String { return d.ProjectID } + +func (d *jiraConnectionResourceData) SetProjectID(projectID types.String) { d.ProjectID = projectID } + +func (d *jiraConnectionResourceData) GetName() types.String { return d.Name } + +func (d *jiraConnectionResourceData) SetName(name types.String) { d.Name = name } + +func (d *jiraConnectionResourceData) GetAuthKey() (string, diag.Diagnostics) { + var diags diag.Diagnostics + + authKey := jiraAuthKey{ + Email: d.Email.ValueString(), + Token: d.Token.ValueString(), + } + + authKeyBytes, err := json.Marshal(authKey) + if err != nil { + diags.Append(diag.NewErrorDiagnostic("Error getting Radar Integration Connection auth key", err.Error())) + return "", diags + } + + return string(authKeyBytes), nil +} + +func (d *jiraConnectionResourceData) GetDetails() (string, diag.Diagnostics) { + var diags diag.Diagnostics + + details := jiraConnectionDetails{ + TenantURL: d.BaseURL.ValueString(), + } + + detailsBytes, err := json.Marshal(details) + if err != nil { + diags.Append(diag.NewErrorDiagnostic("Error getting Radar Integration Connection details", err.Error())) + return "", diags + } + + return string(detailsBytes), nil +} + +func (d *jiraConnectionResourceData) SetDetails(details string) diag.Diagnostics { + var diags diag.Diagnostics + + var detailsData jiraConnectionDetails + if err := json.Unmarshal([]byte(details), &detailsData); err != nil { + diags.Append(diag.NewErrorDiagnostic("Error setting Radar Integration Connection details", err.Error())) + return diags + } + d.BaseURL = types.StringValue(detailsData.TenantURL) + + return nil +} diff --git a/internal/provider/vaultradar/resource_radar_integration_jira_connection_test.go b/internal/provider/vaultradar/resource_radar_integration_jira_connection_test.go new file mode 100644 index 000000000..cfef6e5d3 --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_jira_connection_test.go @@ -0,0 +1,51 @@ +package vaultradar_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest" +) + +func TestRadarIntegrationJiraConnection(t *testing.T) { + // Requires Project to be with Radar tenant provisioned. + // Requires a Service Account with an Admin role on the Project. + // Requires access to a Jira instance. + // Requires the following environment variables to be set: + projectID := os.Getenv("HCP_PROJECT_ID") + baseURL := os.Getenv("RADAR_INTEGRATION_JIRA_BASE_URL") + email := os.Getenv("RADAR_INTEGRATION_JIRA_EMAIL") + token := os.Getenv("RADAR_INTEGRATION_JIRA_TOKEN") + + if projectID == "" || baseURL == "" || email == "" || token == "" { + t.Skip("HCP_PROJECT_ID, RADAR_INTEGRATION_JIRA_BASE_URL, RADAR_INTEGRATION_JIRA_EMAIL, and RADAR_INTEGRATION_JIRA_TOKEN must be set for acceptance tests") + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // CREATE + { + Config: fmt.Sprintf(` + resource "hcp_vault_radar_integration_jira_connection" "example" { + project_id = %q + name = "AC Test of Jira Connect from TF" + base_url = %q + email = %q + token = %q + } + `, projectID, baseURL, email, token), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("hcp_vault_radar_integration_jira_connection.example", "project_id", projectID), + resource.TestCheckResourceAttr("hcp_vault_radar_integration_jira_connection.example", "base_url", baseURL), + resource.TestCheckResourceAttr("hcp_vault_radar_integration_jira_connection.example", "email", email), + resource.TestCheckResourceAttrSet("hcp_vault_radar_integration_jira_connection.example", "id"), + ), + }, + // UPDATE not supported at this time. + // DELETE happens automatically. + }, + }) +} diff --git a/internal/provider/vaultradar/resource_radar_integration_jira_subscription.go b/internal/provider/vaultradar/resource_radar_integration_jira_subscription.go new file mode 100644 index 000000000..c68409934 --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_jira_subscription.go @@ -0,0 +1,191 @@ +package vaultradar + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func NewIntegrationJiraSubscriptionResource() resource.Resource { + return &integrationSubscriptionResource{ + TypeName: "_vault_radar_integration_jira_subscription", + SubscriptionSchema: integrationJiraSubscriptionSchema, + GetPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) { + var sub jiraSubscriptionResourceData + diags := plan.Get(ctx, &sub) + return &sub, diags + }, + GetState: func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) { + var sub jiraSubscriptionResourceData + diags := state.Get(ctx, &sub) + return &sub, diags + }, + } +} + +var integrationJiraSubscriptionSchema = schema.Schema{ + MarkdownDescription: "This terraform resource manages an Integration Jira Subscription in Vault Radar.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of this resource.", + }, + "name": schema.StringAttribute{ + Description: "Name of subscription. Name must be unique.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "connection_id": schema.StringAttribute{ + Description: "id of the integration jira connection to use for the subscription.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "jira_project_key": schema.StringAttribute{ + Description: "The name of the project under which the jira issue will be created. Example: OPS", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "issue_type": schema.StringAttribute{ + Description: "The type of issue to be created from the alert(s). Example: Task", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + // Optional inputs + "assignee": schema.StringAttribute{ + Description: "The identifier of the Jira user who will be assigned the ticket. Example: 1e25fbc8895d5b0c9703c19c", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "message": schema.StringAttribute{ + Description: "This message will be included in the ticket description.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, +} + +type jiraSubscriptionResourceData struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + ConnectionID types.String `tfsdk:"connection_id"` + JiraProjectKey types.String `tfsdk:"jira_project_key"` + IssueType types.String `tfsdk:"issue_type"` + Assignee types.String `tfsdk:"assignee"` + Message types.String `tfsdk:"message"` + ProjectID types.String `tfsdk:"project_id"` +} + +type jiraSubscriptionDetails struct { + ProjectKey string `json:"project_key"` + IssueType string `json:"issuetype"` + Assignee string `json:"assignee"` + Instructions string `json:"instructions"` +} + +func (d *jiraSubscriptionResourceData) GetID() types.String { return d.ID } + +func (d *jiraSubscriptionResourceData) SetID(id types.String) { d.ID = id } + +func (d *jiraSubscriptionResourceData) GetProjectID() types.String { return d.ProjectID } + +func (d *jiraSubscriptionResourceData) SetProjectID(projectID types.String) { d.ProjectID = projectID } + +func (d *jiraSubscriptionResourceData) GetName() types.String { return d.Name } + +func (d *jiraSubscriptionResourceData) SetName(name types.String) { d.Name = name } + +func (d *jiraSubscriptionResourceData) GetConnectionID() types.String { return d.ConnectionID } + +func (d *jiraSubscriptionResourceData) SetConnectionID(connectionID types.String) { + d.ConnectionID = connectionID +} + +func (d *jiraSubscriptionResourceData) GetDetails() (string, diag.Diagnostics) { + var diags diag.Diagnostics + + details := jiraSubscriptionDetails{ + ProjectKey: d.JiraProjectKey.ValueString(), + IssueType: d.IssueType.ValueString(), + Assignee: d.Assignee.ValueString(), + Instructions: d.Message.ValueString(), + } + + detailsBytes, err := json.Marshal(details) + if err != nil { + diags.Append(diag.NewErrorDiagnostic("Error getting Radar Integration Subscription details", err.Error())) + return "", diags + } + + return string(detailsBytes), nil +} + +func (d *jiraSubscriptionResourceData) SetDetails(details string) diag.Diagnostics { + var diags diag.Diagnostics + + var detailsData jiraSubscriptionDetails + if err := json.Unmarshal([]byte(details), &detailsData); err != nil { + diags.Append(diag.NewErrorDiagnostic("Error reading Radar Integration Jira Subscription", err.Error())) + return diags + } + + d.JiraProjectKey = types.StringValue(detailsData.ProjectKey) + d.IssueType = types.StringValue(detailsData.IssueType) + + // Only update the assignee state if the value is not empty. + if !(d.Assignee.IsNull() && detailsData.Assignee == "") { + d.Assignee = types.StringValue(detailsData.Assignee) + } + + // Only update the message state if the value is not empty. + if !(d.Message.IsNull() && detailsData.Instructions == "") { + d.Message = types.StringValue(detailsData.Instructions) + } + + return nil +} diff --git a/internal/provider/vaultradar/resource_radar_integration_jira_subscription_test.go b/internal/provider/vaultradar/resource_radar_integration_jira_subscription_test.go new file mode 100644 index 000000000..1adbc1b13 --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_jira_subscription_test.go @@ -0,0 +1,77 @@ +package vaultradar_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest" +) + +func TestRadarIntegrationJiraSubscription(t *testing.T) { + // Requires Project to be with Radar tenant provisioned. + // Requires a Service Account with an Admin role on the Project. + // Requires access to a Jira instance. + // Requires the following environment variables to be set: + projectID := os.Getenv("HCP_PROJECT_ID") + baseURL := os.Getenv("RADAR_INTEGRATION_JIRA_BASE_URL") + email := os.Getenv("RADAR_INTEGRATION_JIRA_EMAIL") + token := os.Getenv("RADAR_INTEGRATION_JIRA_TOKEN") + jiraProjectKey := os.Getenv("RADAR_INTEGRATION_JIRA_PROJECT_KEY") + issueType := os.Getenv("RADAR_INTEGRATION_JIRA_ISSUE_TYPE") + assignee := os.Getenv("RADAR_INTEGRATION_JIRA_ASSIGNEE") + + // For the connection resource. + if projectID == "" || baseURL == "" || email == "" || token == "" { + t.Skip("HCP_PROJECT_ID, RADAR_INTEGRATION_JIRA_BASE_URL, RADAR_INTEGRATION_JIRA_EMAIL, and RADAR_INTEGRATION_JIRA_TOKEN must be set for acceptance tests") + } + + // For the subscription resource. + if jiraProjectKey == "" || issueType == "" || assignee == "" { + t.Skip("RADAR_INTEGRATION_JIRA_PROJECT_KEY, RADAR_INTEGRATION_JIRA_ISSUE_TYPE, and RADAR_INTEGRATION_JIRA_ASSIGNEE must be set for acceptance tests") + } + + message := "AC test message" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + + Steps: []resource.TestStep{ + // CREATE + { + Config: fmt.Sprintf(` + # An integration_jira_subscription is required to create a hcp_vault_radar_integration_jira_subscription. + resource "hcp_vault_radar_integration_jira_connection" "jira_connection" { + project_id = %q + name = "AC Test of Jira Connect from TF" + base_url = %q + email = %q + token = %q + } + + resource "hcp_vault_radar_integration_jira_subscription" "jira_subscription" { + project_id = hcp_vault_radar_integration_jira_connection.jira_connection.project_id + name = "AC Test of Jira Subscription From TF" + connection_id = hcp_vault_radar_integration_jira_connection.jira_connection.id + jira_project_key = %q + issue_type = %q + assignee = %q + message = %q + } + + `, projectID, baseURL, email, token, + jiraProjectKey, issueType, assignee, message), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("hcp_vault_radar_integration_jira_subscription.jira_subscription", "connection_id"), + resource.TestCheckResourceAttr("hcp_vault_radar_integration_jira_subscription.jira_subscription", "jira_project_key", jiraProjectKey), + resource.TestCheckResourceAttr("hcp_vault_radar_integration_jira_subscription.jira_subscription", "issue_type", issueType), + resource.TestCheckResourceAttr("hcp_vault_radar_integration_jira_subscription.jira_subscription", "assignee", assignee), + resource.TestCheckResourceAttr("hcp_vault_radar_integration_jira_subscription.jira_subscription", "message", message), + ), + }, + // UPDATE not supported at this time. + // DELETE happens automatically. + }, + }) +} diff --git a/internal/provider/vaultradar/resource_radar_integration_slack_connection.go b/internal/provider/vaultradar/resource_radar_integration_slack_connection.go new file mode 100644 index 000000000..9188e8baa --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_slack_connection.go @@ -0,0 +1,121 @@ +package vaultradar + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func NewIntegrationSlackConnectionResource() resource.Resource { + return &integrationConnectionResource{ + TypeName: "_vault_radar_integration_slack_connection", + IntegrationType: "slack", + ConnectionSchema: integrationSlackConnectionSchema, + GetPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) { + var conn slackConnectionResourceData + diags := plan.Get(ctx, &conn) + return &conn, diags + }, + GetState: func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) { + var conn slackConnectionResourceData + diags := state.Get(ctx, &conn) + return &conn, diags + }, + } +} + +var integrationSlackConnectionSchema = schema.Schema{ + MarkdownDescription: "This terraform resource manages an Integration Slack Connection in Vault Radar.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of this resource.", + }, + "name": schema.StringAttribute{ + Description: "Name of connection. Name must be unique.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "token": schema.StringAttribute{ + Description: "Slack bot user oAuth token. Example: Bot token strings begin with 'xoxb'.", + Required: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + + // Optional inputs + "project_id": schema.StringAttribute{ + Description: "The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, +} + +type slackConnectionResourceData struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Token types.String `tfsdk:"token"` + ProjectID types.String `tfsdk:"project_id"` +} + +type slackAuthKey struct { + Token string `json:"token"` +} + +func (d *slackConnectionResourceData) GetID() types.String { return d.ID } + +func (d *slackConnectionResourceData) SetID(id types.String) { d.ID = id } + +func (d *slackConnectionResourceData) GetProjectID() types.String { return d.ProjectID } + +func (d *slackConnectionResourceData) SetProjectID(projectID types.String) { d.ProjectID = projectID } + +func (d *slackConnectionResourceData) GetName() types.String { return d.Name } + +func (d *slackConnectionResourceData) SetName(name types.String) { d.Name = name } + +func (d *slackConnectionResourceData) GetAuthKey() (string, diag.Diagnostics) { + var diags diag.Diagnostics + + authKey := slackAuthKey{ + Token: d.Token.ValueString(), + } + + authKeyBytes, err := json.Marshal(authKey) + if err != nil { + diags.Append(diag.NewErrorDiagnostic("Error getting Radar Integration Connection state", err.Error())) + return "", diags + } + + return string(authKeyBytes), nil +} + +func (d *slackConnectionResourceData) GetDetails() (string, diag.Diagnostics) { + return "{}", nil +} + +func (d *slackConnectionResourceData) SetDetails(string) diag.Diagnostics { + // no-op + return nil +} diff --git a/internal/provider/vaultradar/resource_radar_integration_slack_connection_test.go b/internal/provider/vaultradar/resource_radar_integration_slack_connection_test.go new file mode 100644 index 000000000..90ecb9a37 --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_slack_connection_test.go @@ -0,0 +1,45 @@ +package vaultradar_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest" +) + +func TestRadarIntegrationSlackConnection(t *testing.T) { + // Requires Project to be with Radar tenant provisioned. + // Requires a Service Account with an Admin role on the Project. + // Requires access to a Slack instance. + // Requires the following environment variables to be set: + projectID := os.Getenv("HCP_PROJECT_ID") + token := os.Getenv("RADAR_INTEGRATION_SLACK_TOKEN") + + if projectID == "" || token == "" { + t.Skip("HCP_PROJECT_ID and RADAR_INTEGRATION_SLACK_TOKEN must be set for acceptance tests") + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // CREATE + { + Config: fmt.Sprintf(` + resource "hcp_vault_radar_integration_slack_connection" "example" { + project_id = %q + name = "AC Test of Slack Connect from TF" + token = %q + } + `, projectID, token), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("hcp_vault_radar_integration_slack_connection.example", "project_id", projectID), + resource.TestCheckResourceAttrSet("hcp_vault_radar_integration_slack_connection.example", "id"), + ), + }, + // UPDATE not supported at this time. + // DELETE happens automatically. + }, + }) +} diff --git a/internal/provider/vaultradar/resource_radar_integration_slack_subscription.go b/internal/provider/vaultradar/resource_radar_integration_slack_subscription.go new file mode 100644 index 000000000..bef6dee64 --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_slack_subscription.go @@ -0,0 +1,141 @@ +package vaultradar + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func NewIntegrationSlackSubscriptionResource() resource.Resource { + return &integrationSubscriptionResource{ + TypeName: "_vault_radar_integration_slack_subscription", + SubscriptionSchema: integrationSlackSubscriptionSchema, + GetPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) { + var sub slackSubscriptionResourceData + diags := plan.Get(ctx, &sub) + return &sub, diags + }, + GetState: func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) { + var sub slackSubscriptionResourceData + diags := state.Get(ctx, &sub) + return &sub, diags + }, + } +} + +var integrationSlackSubscriptionSchema = schema.Schema{ + MarkdownDescription: "This terraform resource manages an Integration Slack Subscription in Vault Radar.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of this resource.", + }, + "name": schema.StringAttribute{ + Description: "Name of subscription. Name must be unique.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "connection_id": schema.StringAttribute{ + Description: "id of the integration slack connection to use for the subscription.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "channel": schema.StringAttribute{ + Description: "Slack channel that messages will be sent to. Note that HashiCorp Vault Radar will send a test message to verify channel's name. Example: dev-ops-team", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + // Optional inputs + "project_id": schema.StringAttribute{ + Description: "The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, +} + +type slackSubscriptionResourceData struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + ConnectionID types.String `tfsdk:"connection_id"` + Channel types.String `tfsdk:"channel"` + ProjectID types.String `tfsdk:"project_id"` +} + +type slackSubscriptionDetails struct { + Channel string `json:"channel"` +} + +func (d *slackSubscriptionResourceData) GetID() types.String { return d.ID } + +func (d *slackSubscriptionResourceData) SetID(id types.String) { d.ID = id } + +func (d *slackSubscriptionResourceData) GetProjectID() types.String { return d.ProjectID } + +func (d *slackSubscriptionResourceData) SetProjectID(projectID types.String) { d.ProjectID = projectID } + +func (d *slackSubscriptionResourceData) GetName() types.String { return d.Name } + +func (d *slackSubscriptionResourceData) SetName(name types.String) { d.Name = name } + +func (d *slackSubscriptionResourceData) GetConnectionID() types.String { return d.ConnectionID } + +func (d *slackSubscriptionResourceData) SetConnectionID(connectionID types.String) { + d.ConnectionID = connectionID +} + +func (d *slackSubscriptionResourceData) GetDetails() (string, diag.Diagnostics) { + var diags diag.Diagnostics + + details := slackSubscriptionDetails{ + Channel: d.Channel.ValueString(), + } + + detailsBytes, err := json.Marshal(details) + if err != nil { + diags.Append(diag.NewErrorDiagnostic("Error getting Radar Integration Subscription details", err.Error())) + return "", diags + } + + return string(detailsBytes), nil +} + +func (d *slackSubscriptionResourceData) SetDetails(details string) diag.Diagnostics { + var diags diag.Diagnostics + + var detailsData slackSubscriptionDetails + if err := json.Unmarshal([]byte(details), &detailsData); err != nil { + diags.Append(diag.NewErrorDiagnostic("Error reading Radar Integration Slack Subscription", err.Error())) + return diags + } + + d.Channel = types.StringValue(detailsData.Channel) + + return nil +} diff --git a/internal/provider/vaultradar/resource_radar_integration_slack_subscription_test.go b/internal/provider/vaultradar/resource_radar_integration_slack_subscription_test.go new file mode 100644 index 000000000..93ecc2e67 --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_slack_subscription_test.go @@ -0,0 +1,56 @@ +package vaultradar_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest" +) + +func TestRadarIntegrationSlackSubscription(t *testing.T) { + // Requires Project to be with Radar tenant provisioned. + // Requires a Service Account with an Admin role on the Project. + // Requires access to a Slack instance. + // Requires the following environment variables to be set: + projectID := os.Getenv("HCP_PROJECT_ID") + token := os.Getenv("RADAR_INTEGRATION_SLACK_TOKEN") + channel := os.Getenv("RADAR_INTEGRATION_SLACK_CHANNEL") + + if projectID == "" || token == "" || channel == "" { + t.Skip("HCP_PROJECT_ID, RADAR_INTEGRATION_SLACK_TOKEN and RADAR_INTEGRATION_SLACK_CHANNEL must be set for acceptance tests") + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + + Steps: []resource.TestStep{ + // CREATE + { + Config: fmt.Sprintf(` + # An integration_slack_subscription is required to create a hcp_vault_radar_integration_slack_subscription. + resource "hcp_vault_radar_integration_slack_connection" "slack_connection" { + project_id = %q + name = "AC Test of Slack Connect from TF" + token = %q + } + + resource "hcp_vault_radar_integration_slack_subscription" "slack_subscription" { + project_id = hcp_vault_radar_integration_slack_connection.slack_connection.project_id + name = "AC Test of Slack Subscription From TF" + connection_id = hcp_vault_radar_integration_slack_connection.slack_connection.id + channel = %q + } + + `, projectID, token, channel), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("hcp_vault_radar_integration_slack_subscription.slack_subscription", "connection_id"), + resource.TestCheckResourceAttr("hcp_vault_radar_integration_slack_subscription.slack_subscription", "channel", channel), + ), + }, + // UPDATE not supported at this time. + // DELETE happens automatically. + }, + }) +} diff --git a/templates/resources/vault_radar_integration_jira_connection.md.tmpl b/templates/resources/vault_radar_integration_jira_connection.md.tmpl new file mode 100644 index 000000000..3206ac2a8 --- /dev/null +++ b/templates/resources/vault_radar_integration_jira_connection.md.tmpl @@ -0,0 +1,19 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +-> **Note:** HCP Vault Radar Terraform resources are in preview. + +{{ .Description | trimspace }} + +## Example Usage + +{{ tffile "examples/resources/hcp_vault_radar_integration_jira_connection/resource.tf" }} + + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/vault_radar_integration_jira_subscription.md.tmpl b/templates/resources/vault_radar_integration_jira_subscription.md.tmpl new file mode 100644 index 000000000..2e1636484 --- /dev/null +++ b/templates/resources/vault_radar_integration_jira_subscription.md.tmpl @@ -0,0 +1,19 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +-> **Note:** HCP Vault Radar Terraform resources are in preview. + +{{ .Description | trimspace }} + +## Example Usage + +{{ tffile "examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf" }} + + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/vault_radar_integration_slack_connection.md.tmpl b/templates/resources/vault_radar_integration_slack_connection.md.tmpl new file mode 100644 index 000000000..86895511c --- /dev/null +++ b/templates/resources/vault_radar_integration_slack_connection.md.tmpl @@ -0,0 +1,19 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +-> **Note:** HCP Vault Radar Terraform resources are in preview. + +{{ .Description | trimspace }} + +## Example Usage + +{{ tffile "examples/resources/hcp_vault_radar_integration_slack_connection/resource.tf" }} + + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/vault_radar_integration_slack_subscription.md.tmpl b/templates/resources/vault_radar_integration_slack_subscription.md.tmpl new file mode 100644 index 000000000..044f7d1b5 --- /dev/null +++ b/templates/resources/vault_radar_integration_slack_subscription.md.tmpl @@ -0,0 +1,19 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +-> **Note:** HCP Vault Radar Terraform resources are in preview. + +{{ .Description | trimspace }} + +## Example Usage + +{{ tffile "examples/resources/hcp_vault_radar_integration_slack_subscription/resource.tf" }} + + +{{ .SchemaMarkdown | trimspace }} From c20ff209ddc82e48ea899be090c94bb99ee73f73 Mon Sep 17 00:00:00 2001 From: Trent DiBacco Date: Thu, 10 Oct 2024 14:03:26 -0700 Subject: [PATCH 08/11] Add change log. --- .changelog/1116.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/1116.txt diff --git a/.changelog/1116.txt b/.changelog/1116.txt new file mode 100644 index 000000000..b5f7c27d7 --- /dev/null +++ b/.changelog/1116.txt @@ -0,0 +1,7 @@ +```release-note:feature +Add preview of the following Vault Radar connection and subscriptions: +hcp_vault_radar_integration_jira_connection +hcp_vault_radar_integration_jira_subscription +hcp_vault_radar_integration_slack_connection +hcp_vault_radar_integration_slack_subscription +``` \ No newline at end of file From 687d23a1de8fe8ffa1d9433c056aa0cde58392fc Mon Sep 17 00:00:00 2001 From: Trent DiBacco Date: Thu, 10 Oct 2024 14:04:33 -0700 Subject: [PATCH 09/11] fix change log. --- .changelog/1116.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/1116.txt b/.changelog/1116.txt index b5f7c27d7..df7e7ef06 100644 --- a/.changelog/1116.txt +++ b/.changelog/1116.txt @@ -1,5 +1,5 @@ ```release-note:feature -Add preview of the following Vault Radar connection and subscriptions: +Add preview of the following Vault Radar connections and subscriptions: hcp_vault_radar_integration_jira_connection hcp_vault_radar_integration_jira_subscription hcp_vault_radar_integration_slack_connection From d3e341f6a5f111501870605f4f8643c04af70247 Mon Sep 17 00:00:00 2001 From: Trent DiBacco Date: Mon, 14 Oct 2024 12:06:24 -0700 Subject: [PATCH 10/11] Fix spelling of OAuth. --- docs/resources/vault_radar_integration_slack_connection.md | 2 +- .../vaultradar/resource_radar_integration_slack_connection.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/vault_radar_integration_slack_connection.md b/docs/resources/vault_radar_integration_slack_connection.md index ed8170869..652d26428 100644 --- a/docs/resources/vault_radar_integration_slack_connection.md +++ b/docs/resources/vault_radar_integration_slack_connection.md @@ -32,7 +32,7 @@ resource "hcp_vault_radar_integration_slack_connection" "slack_connection" { ### Required - `name` (String) Name of connection. Name must be unique. -- `token` (String, Sensitive) Slack bot user oAuth token. Example: Bot token strings begin with 'xoxb'. +- `token` (String, Sensitive) Slack bot user OAuth token. Example: Bot token strings begin with 'xoxb'. ### Optional diff --git a/internal/provider/vaultradar/resource_radar_integration_slack_connection.go b/internal/provider/vaultradar/resource_radar_integration_slack_connection.go index 9188e8baa..e50b8ef17 100644 --- a/internal/provider/vaultradar/resource_radar_integration_slack_connection.go +++ b/internal/provider/vaultradar/resource_radar_integration_slack_connection.go @@ -51,7 +51,7 @@ var integrationSlackConnectionSchema = schema.Schema{ }, }, "token": schema.StringAttribute{ - Description: "Slack bot user oAuth token. Example: Bot token strings begin with 'xoxb'.", + Description: "Slack bot user OAuth token. Example: Bot token strings begin with 'xoxb'.", Required: true, Sensitive: true, PlanModifiers: []planmodifier.String{ From ed13132cf6f3e455efccb82219a016c2bb13dc78 Mon Sep 17 00:00:00 2001 From: Trent DiBacco Date: Tue, 15 Oct 2024 14:27:34 -0700 Subject: [PATCH 11/11] Address most of the feedback from kathy and kartheek. --- ...vault_radar_integration_jira_connection.md | 2 +- ...ult_radar_integration_jira_subscription.md | 6 +- ...lt_radar_integration_slack_subscription.md | 2 +- .../resource.tf | 2 +- .../vaultradar/integration_connection.go | 58 +++++++++--------- .../vaultradar/integration_subscription.go | 60 +++++++++---------- ...ource_radar_integration_jira_connection.go | 5 +- ...rce_radar_integration_jira_subscription.go | 8 +-- ...urce_radar_integration_slack_connection.go | 4 +- ...ce_radar_integration_slack_subscription.go | 6 +- 10 files changed, 77 insertions(+), 76 deletions(-) diff --git a/docs/resources/vault_radar_integration_jira_connection.md b/docs/resources/vault_radar_integration_jira_connection.md index 0a247f025..387897dac 100644 --- a/docs/resources/vault_radar_integration_jira_connection.md +++ b/docs/resources/vault_radar_integration_jira_connection.md @@ -34,7 +34,7 @@ resource "hcp_vault_radar_integration_jira_connection" "jira_connection" { ### Required - `base_url` (String) The Jira base URL. Example: https://acme.atlassian.net -- `email` (String) Jira user's email. +- `email` (String, Sensitive) Jira user's email. - `name` (String) Name of connection. Name must be unique. - `token` (String, Sensitive) A Jira API token. diff --git a/docs/resources/vault_radar_integration_jira_subscription.md b/docs/resources/vault_radar_integration_jira_subscription.md index 95d682743..1c40a51d6 100644 --- a/docs/resources/vault_radar_integration_jira_subscription.md +++ b/docs/resources/vault_radar_integration_jira_subscription.md @@ -32,7 +32,7 @@ resource "hcp_vault_radar_integration_jira_subscription" "jira_subscription" { connection_id = hcp_vault_radar_integration_jira_connection.jira_connection.id jira_project_key = "SEC" issue_type = "Task" - assignee = "id-of-assignee" + assignee = "71509:11bb945b-c0de-4bac-9d57-9f09db2f7bc9" message = "Example message" } ``` @@ -44,13 +44,13 @@ resource "hcp_vault_radar_integration_jira_subscription" "jira_subscription" { ### Required - `connection_id` (String) id of the integration jira connection to use for the subscription. -- `issue_type` (String) The type of issue to be created from the alert(s). Example: Task +- `issue_type` (String) The type of issue to be created from the event(s). Example: Task - `jira_project_key` (String) The name of the project under which the jira issue will be created. Example: OPS - `name` (String) Name of subscription. Name must be unique. ### Optional -- `assignee` (String) The identifier of the Jira user who will be assigned the ticket. Example: 1e25fbc8895d5b0c9703c19c +- `assignee` (String) The identifier of the Jira user who will be assigned the ticket. In case of Jira Cloud, this will be the Atlassian Account ID of the user. Example: 71509:11bb945b-c0de-4bac-9d57-9f09db2f7bc9 - `message` (String) This message will be included in the ticket description. - `project_id` (String) The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured. diff --git a/docs/resources/vault_radar_integration_slack_subscription.md b/docs/resources/vault_radar_integration_slack_subscription.md index 3c421107f..8d325efe7 100644 --- a/docs/resources/vault_radar_integration_slack_subscription.md +++ b/docs/resources/vault_radar_integration_slack_subscription.md @@ -38,7 +38,7 @@ resource "hcp_vault_radar_integration_slack_subscription" "slack_subscription" { ### Required -- `channel` (String) Slack channel that messages will be sent to. Note that HashiCorp Vault Radar will send a test message to verify channel's name. Example: dev-ops-team +- `channel` (String) Name of the Slack channel that messages should be sent to. Note that HashiCorp Vault Radar will send a test message to verify the channel. Example: dev-ops-team - `connection_id` (String) id of the integration slack connection to use for the subscription. - `name` (String) Name of subscription. Name must be unique. diff --git a/examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf b/examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf index 2aee45cad..447ac11e6 100644 --- a/examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf +++ b/examples/resources/hcp_vault_radar_integration_jira_subscription/resource.tf @@ -16,6 +16,6 @@ resource "hcp_vault_radar_integration_jira_subscription" "jira_subscription" { connection_id = hcp_vault_radar_integration_jira_connection.jira_connection.id jira_project_key = "SEC" issue_type = "Task" - assignee = "id-of-assignee" + assignee = "71509:11bb945b-c0de-4bac-9d57-9f09db2f7bc9" message = "Example message" } \ No newline at end of file diff --git a/internal/provider/vaultradar/integration_connection.go b/internal/provider/vaultradar/integration_connection.go index 8db87dd74..71d1348b7 100644 --- a/internal/provider/vaultradar/integration_connection.go +++ b/internal/provider/vaultradar/integration_connection.go @@ -25,12 +25,12 @@ var ( // Examples: hcp_vault_radar_integration_jira_connection and hcp_vault_radar_integration_slack_connection make use of // this implementation to define resources with specific schemas, validation, and state details related to their types. type integrationConnectionResource struct { - client *clients.Client - TypeName string - IntegrationType string - ConnectionSchema schema.Schema - GetPlan func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) - GetState func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) + client *clients.Client + TypeName string + IntegrationType string + ConnectionSchema schema.Schema + GetConnectionFromPlan func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) + GetConnectionFromState func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) } // integrationConnection is the minimal plan/state that a connection must have. @@ -75,36 +75,36 @@ func (r *integrationConnectionResource) ModifyPlan(ctx context.Context, req reso } func (r *integrationConnectionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - plan, diags := r.GetPlan(ctx, req.Plan) + conn, diags := r.GetConnectionFromPlan(ctx, req.Plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } projectID := r.client.Config.ProjectID - if !plan.GetProjectID().IsUnknown() { - projectID = plan.GetProjectID().ValueString() + if !conn.GetProjectID().IsUnknown() { + projectID = conn.GetProjectID().ValueString() } errSummary := "Error creating Radar Integration Connection" // Check for an existing connection with the same name. - existing, err := clients.GetIntegrationConnectionByName(ctx, r.client, projectID, plan.GetName().ValueString()) + existing, err := clients.GetIntegrationConnectionByName(ctx, r.client, projectID, conn.GetName().ValueString()) if err != nil && !clients.IsResponseCodeNotFound(err) { resp.Diagnostics.AddError(errSummary, err.Error()) } if existing != nil { - resp.Diagnostics.AddError(errSummary, fmt.Sprintf("Connection with name: %q already exists.", plan.GetName().ValueString())) + resp.Diagnostics.AddError(errSummary, fmt.Sprintf("Connection with name: %q already exists.", conn.GetName().ValueString())) return } - authKey, diags := plan.GetAuthKey() + authKey, diags := conn.GetAuthKey() resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - details, diags := plan.GetDetails() + details, diags := conn.GetDetails() resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -114,7 +114,7 @@ func (r *integrationConnectionResource) Create(ctx context.Context, req resource IntegrationType: r.IntegrationType, IsSink: true, IsSource: false, - Name: plan.GetName().ValueString(), + Name: conn.GetName().ValueString(), AuthKey: authKey, Details: details, }) @@ -123,24 +123,24 @@ func (r *integrationConnectionResource) Create(ctx context.Context, req resource return } - plan.SetID(types.StringValue(res.GetPayload().ID)) - plan.SetProjectID(types.StringValue(projectID)) - resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + conn.SetID(types.StringValue(res.GetPayload().ID)) + conn.SetProjectID(types.StringValue(projectID)) + resp.Diagnostics.Append(resp.State.Set(ctx, conn)...) } func (r *integrationConnectionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - state, diags := r.GetState(ctx, req.State) + conn, diags := r.GetConnectionFromState(ctx, req.State) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } projectID := r.client.Config.ProjectID - if !state.GetProjectID().IsUnknown() { - projectID = state.GetProjectID().ValueString() + if !conn.GetProjectID().IsUnknown() { + projectID = conn.GetProjectID().ValueString() } - res, err := clients.GetIntegrationConnectionByID(ctx, r.client, projectID, state.GetID().ValueString()) + res, err := clients.GetIntegrationConnectionByID(ctx, r.client, projectID, conn.GetID().ValueString()) if err != nil { if clients.IsResponseCodeNotFound(err) { // Resource is no longer on the server. @@ -152,30 +152,30 @@ func (r *integrationConnectionResource) Read(ctx context.Context, req resource.R return } - state.SetName(types.StringValue(res.GetPayload().Name)) - diags = state.SetDetails(res.GetPayload().Details) + conn.SetName(types.StringValue(res.GetPayload().Name)) + diags = conn.SetDetails(res.GetPayload().Details) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &conn)...) } func (r *integrationConnectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - state, diags := r.GetState(ctx, req.State) + conn, diags := r.GetConnectionFromState(ctx, req.State) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } projectID := r.client.Config.ProjectID - if !state.GetProjectID().IsUnknown() { - projectID = state.GetProjectID().ValueString() + if !conn.GetProjectID().IsUnknown() { + projectID = conn.GetProjectID().ValueString() } // Assert resource still exists. - if _, err := clients.GetIntegrationConnectionByID(ctx, r.client, projectID, state.GetID().ValueString()); err != nil { + if _, err := clients.GetIntegrationConnectionByID(ctx, r.client, projectID, conn.GetID().ValueString()); err != nil { if clients.IsResponseCodeNotFound(err) { // Resource is no longer on the server. tflog.Info(ctx, "Radar integration connection not found, removing from state.") @@ -186,7 +186,7 @@ func (r *integrationConnectionResource) Delete(ctx context.Context, req resource return } - if err := clients.DeleteIntegrationConnection(ctx, r.client, projectID, state.GetID().ValueString()); err != nil { + if err := clients.DeleteIntegrationConnection(ctx, r.client, projectID, conn.GetID().ValueString()); err != nil { resp.Diagnostics.AddError("Unable to delete Radar integration connection", err.Error()) return } diff --git a/internal/provider/vaultradar/integration_subscription.go b/internal/provider/vaultradar/integration_subscription.go index 113edecb0..f960691fb 100644 --- a/internal/provider/vaultradar/integration_subscription.go +++ b/internal/provider/vaultradar/integration_subscription.go @@ -25,11 +25,11 @@ var ( // Examples: hcp_vault_radar_integration_jira_subscription and hcp_vault_radar_integration_slack_subscription make use of // this implementation to define resources with specific schemas, validation, and state details related to their types. type integrationSubscriptionResource struct { - client *clients.Client - TypeName string - SubscriptionSchema schema.Schema - GetPlan func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) - GetState func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) + client *clients.Client + TypeName string + SubscriptionSchema schema.Schema + GetSubscriptionFromPlan func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) + GetSubscriptionFromState func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) } // integrationSubscription is the minimal plan/state that a subscription must have. @@ -75,38 +75,38 @@ func (r *integrationSubscriptionResource) ModifyPlan(ctx context.Context, req re } func (r *integrationSubscriptionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - plan, diags := r.GetPlan(ctx, req.Plan) + subscription, diags := r.GetSubscriptionFromPlan(ctx, req.Plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } projectID := r.client.Config.ProjectID - if !plan.GetProjectID().IsUnknown() { - projectID = plan.GetProjectID().ValueString() + if !subscription.GetProjectID().IsUnknown() { + projectID = subscription.GetProjectID().ValueString() } errSummary := "Error creating Radar Integration Subscription" // Check for an existing subscription with the same name. - existing, err := clients.GetIntegrationSubscriptionByName(ctx, r.client, projectID, plan.GetName().ValueString()) + existing, err := clients.GetIntegrationSubscriptionByName(ctx, r.client, projectID, subscription.GetName().ValueString()) if err != nil && !clients.IsResponseCodeNotFound(err) { resp.Diagnostics.AddError(errSummary, err.Error()) } if existing != nil { - resp.Diagnostics.AddError(errSummary, fmt.Sprintf("Subscription with name: %q already exists.", plan.GetName().ValueString())) + resp.Diagnostics.AddError(errSummary, fmt.Sprintf("Subscription with name: %q already exists.", subscription.GetName().ValueString())) return } - details, diags := plan.GetDetails() + details, diags := subscription.GetDetails() resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } res, err := clients.CreateIntegrationSubscription(ctx, r.client, projectID, service.CreateIntegrationSubscriptionBody{ - Name: plan.GetName().ValueString(), - ConnectionID: plan.GetConnectionID().ValueString(), + Name: subscription.GetName().ValueString(), + ConnectionID: subscription.GetConnectionID().ValueString(), Details: details, }) if err != nil { @@ -114,24 +114,24 @@ func (r *integrationSubscriptionResource) Create(ctx context.Context, req resour return } - plan.SetID(types.StringValue(res.GetPayload().ID)) - plan.SetProjectID(types.StringValue(projectID)) - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + subscription.SetID(types.StringValue(res.GetPayload().ID)) + subscription.SetProjectID(types.StringValue(projectID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &subscription)...) } func (r *integrationSubscriptionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - state, diags := r.GetState(ctx, req.State) + subscription, diags := r.GetSubscriptionFromState(ctx, req.State) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } projectID := r.client.Config.ProjectID - if !state.GetProjectID().IsUnknown() { - projectID = state.GetProjectID().ValueString() + if !subscription.GetProjectID().IsUnknown() { + projectID = subscription.GetProjectID().ValueString() } - res, err := clients.GetIntegrationSubscriptionByID(ctx, r.client, projectID, state.GetID().ValueString()) + res, err := clients.GetIntegrationSubscriptionByID(ctx, r.client, projectID, subscription.GetID().ValueString()) if err != nil { if clients.IsResponseCodeNotFound(err) { // Resource is no longer on the server. @@ -144,35 +144,35 @@ func (r *integrationSubscriptionResource) Read(ctx context.Context, req resource } payload := res.GetPayload() - state.SetName(types.StringValue(payload.Name)) - state.SetConnectionID(types.StringValue(payload.ConnectionID)) + subscription.SetName(types.StringValue(payload.Name)) + subscription.SetConnectionID(types.StringValue(payload.ConnectionID)) - diags = state.SetDetails(res.GetPayload().Details) + diags = subscription.SetDetails(res.GetPayload().Details) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &subscription)...) } func (r *integrationSubscriptionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - state, diags := r.GetState(ctx, req.State) + subscription, diags := r.GetSubscriptionFromState(ctx, req.State) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } projectID := r.client.Config.ProjectID - if !state.GetProjectID().IsUnknown() { - projectID = state.GetProjectID().ValueString() + if !subscription.GetProjectID().IsUnknown() { + projectID = subscription.GetProjectID().ValueString() } // Assert resource still exists. - if _, err := clients.GetIntegrationSubscriptionByID(ctx, r.client, projectID, state.GetID().ValueString()); err != nil { + if _, err := clients.GetIntegrationSubscriptionByID(ctx, r.client, projectID, subscription.GetID().ValueString()); err != nil { if clients.IsResponseCodeNotFound(err) { // Resource is no longer on the server. - tflog.Info(ctx, "Radar integration subscription not found, removing from state.") + tflog.Info(ctx, "Radar integration subscription not found, removing from subscription.") resp.State.RemoveResource(ctx) return } @@ -180,7 +180,7 @@ func (r *integrationSubscriptionResource) Delete(ctx context.Context, req resour return } - if err := clients.DeleteIntegrationSubscription(ctx, r.client, projectID, state.GetID().ValueString()); err != nil { + if err := clients.DeleteIntegrationSubscription(ctx, r.client, projectID, subscription.GetID().ValueString()); err != nil { resp.Diagnostics.AddError("Unable to delete Radar integration subscription", err.Error()) return } diff --git a/internal/provider/vaultradar/resource_radar_integration_jira_connection.go b/internal/provider/vaultradar/resource_radar_integration_jira_connection.go index c68087978..e482671db 100644 --- a/internal/provider/vaultradar/resource_radar_integration_jira_connection.go +++ b/internal/provider/vaultradar/resource_radar_integration_jira_connection.go @@ -21,12 +21,12 @@ func NewIntegrationJiraConnectionResource() resource.Resource { TypeName: "_vault_radar_integration_jira_connection", IntegrationType: "jira", ConnectionSchema: integrationJiraConnectionSchema, - GetPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) { + GetConnectionFromPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) { var conn jiraConnectionResourceData diags := plan.Get(ctx, &conn) return &conn, diags }, - GetState: func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) { + GetConnectionFromState: func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) { var conn jiraConnectionResourceData diags := state.Get(ctx, &conn) return &conn, diags @@ -54,6 +54,7 @@ var integrationJiraConnectionSchema = schema.Schema{ "email": schema.StringAttribute{ Description: `Jira user's email.`, Required: true, + Sensitive: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, diff --git a/internal/provider/vaultradar/resource_radar_integration_jira_subscription.go b/internal/provider/vaultradar/resource_radar_integration_jira_subscription.go index c68409934..c4a761171 100644 --- a/internal/provider/vaultradar/resource_radar_integration_jira_subscription.go +++ b/internal/provider/vaultradar/resource_radar_integration_jira_subscription.go @@ -19,12 +19,12 @@ func NewIntegrationJiraSubscriptionResource() resource.Resource { return &integrationSubscriptionResource{ TypeName: "_vault_radar_integration_jira_subscription", SubscriptionSchema: integrationJiraSubscriptionSchema, - GetPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) { + GetSubscriptionFromPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) { var sub jiraSubscriptionResourceData diags := plan.Get(ctx, &sub) return &sub, diags }, - GetState: func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) { + GetSubscriptionFromState: func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) { var sub jiraSubscriptionResourceData diags := state.Get(ctx, &sub) return &sub, diags @@ -67,7 +67,7 @@ var integrationJiraSubscriptionSchema = schema.Schema{ }, }, "issue_type": schema.StringAttribute{ - Description: "The type of issue to be created from the alert(s). Example: Task", + Description: "The type of issue to be created from the event(s). Example: Task", Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), @@ -79,7 +79,7 @@ var integrationJiraSubscriptionSchema = schema.Schema{ // Optional inputs "assignee": schema.StringAttribute{ - Description: "The identifier of the Jira user who will be assigned the ticket. Example: 1e25fbc8895d5b0c9703c19c", + Description: "The identifier of the Jira user who will be assigned the ticket. In case of Jira Cloud, this will be the Atlassian Account ID of the user. Example: 71509:11bb945b-c0de-4bac-9d57-9f09db2f7bc9", Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), diff --git a/internal/provider/vaultradar/resource_radar_integration_slack_connection.go b/internal/provider/vaultradar/resource_radar_integration_slack_connection.go index e50b8ef17..f61bb9835 100644 --- a/internal/provider/vaultradar/resource_radar_integration_slack_connection.go +++ b/internal/provider/vaultradar/resource_radar_integration_slack_connection.go @@ -20,12 +20,12 @@ func NewIntegrationSlackConnectionResource() resource.Resource { TypeName: "_vault_radar_integration_slack_connection", IntegrationType: "slack", ConnectionSchema: integrationSlackConnectionSchema, - GetPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) { + GetConnectionFromPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) { var conn slackConnectionResourceData diags := plan.Get(ctx, &conn) return &conn, diags }, - GetState: func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) { + GetConnectionFromState: func(ctx context.Context, state tfsdk.State) (integrationConnection, diag.Diagnostics) { var conn slackConnectionResourceData diags := state.Get(ctx, &conn) return &conn, diags diff --git a/internal/provider/vaultradar/resource_radar_integration_slack_subscription.go b/internal/provider/vaultradar/resource_radar_integration_slack_subscription.go index bef6dee64..04104f2f4 100644 --- a/internal/provider/vaultradar/resource_radar_integration_slack_subscription.go +++ b/internal/provider/vaultradar/resource_radar_integration_slack_subscription.go @@ -19,12 +19,12 @@ func NewIntegrationSlackSubscriptionResource() resource.Resource { return &integrationSubscriptionResource{ TypeName: "_vault_radar_integration_slack_subscription", SubscriptionSchema: integrationSlackSubscriptionSchema, - GetPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) { + GetSubscriptionFromPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) { var sub slackSubscriptionResourceData diags := plan.Get(ctx, &sub) return &sub, diags }, - GetState: func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) { + GetSubscriptionFromState: func(ctx context.Context, state tfsdk.State) (integrationSubscription, diag.Diagnostics) { var sub slackSubscriptionResourceData diags := state.Get(ctx, &sub) return &sub, diags @@ -57,7 +57,7 @@ var integrationSlackSubscriptionSchema = schema.Schema{ }, }, "channel": schema.StringAttribute{ - Description: "Slack channel that messages will be sent to. Note that HashiCorp Vault Radar will send a test message to verify channel's name. Example: dev-ops-team", + Description: "Name of the Slack channel that messages should be sent to. Note that HashiCorp Vault Radar will send a test message to verify the channel. Example: dev-ops-team", Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(),