diff --git a/.changelog/1116.txt b/.changelog/1116.txt new file mode 100644 index 000000000..df7e7ef06 --- /dev/null +++ b/.changelog/1116.txt @@ -0,0 +1,7 @@ +```release-note:feature +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 +hcp_vault_radar_integration_slack_subscription +``` \ No newline at end of file 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..387897dac --- /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, Sensitive) 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..1c40a51d6 --- /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 = "71509:11bb945b-c0de-4bac-9d57-9f09db2f7bc9" + 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 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. 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. + +### 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..652d26428 --- /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..8d325efe7 --- /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) 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. + +### 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..447ac11e6 --- /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 = "71509:11bb945b-c0de-4bac-9d57-9f09db2f7bc9" + 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..71d1348b7 --- /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 + 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. +// 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) { + conn, diags := r.GetConnectionFromPlan(ctx, req.Plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + 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, 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.", conn.GetName().ValueString())) + return + } + + authKey, diags := conn.GetAuthKey() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + details, diags := conn.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: conn.GetName().ValueString(), + AuthKey: authKey, + Details: details, + }) + if err != nil { + resp.Diagnostics.AddError(errSummary, err.Error()) + return + } + + 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) { + conn, diags := r.GetConnectionFromState(ctx, req.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !conn.GetProjectID().IsUnknown() { + projectID = conn.GetProjectID().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. + 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 + } + + 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, &conn)...) +} + +func (r *integrationConnectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn, diags := r.GetConnectionFromState(ctx, req.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !conn.GetProjectID().IsUnknown() { + projectID = conn.GetProjectID().ValueString() + } + + // Assert resource still exists. + 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.") + 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, conn.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..f960691fb --- /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 + 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. +// 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) { + subscription, diags := r.GetSubscriptionFromPlan(ctx, req.Plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + 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, 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.", subscription.GetName().ValueString())) + return + } + + details, diags := subscription.GetDetails() + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + res, err := clients.CreateIntegrationSubscription(ctx, r.client, projectID, service.CreateIntegrationSubscriptionBody{ + Name: subscription.GetName().ValueString(), + ConnectionID: subscription.GetConnectionID().ValueString(), + Details: details, + }) + if err != nil { + resp.Diagnostics.AddError(errSummary, err.Error()) + return + } + + 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) { + subscription, diags := r.GetSubscriptionFromState(ctx, req.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !subscription.GetProjectID().IsUnknown() { + projectID = subscription.GetProjectID().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. + 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() + subscription.SetName(types.StringValue(payload.Name)) + subscription.SetConnectionID(types.StringValue(payload.ConnectionID)) + + diags = subscription.SetDetails(res.GetPayload().Details) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &subscription)...) +} + +func (r *integrationSubscriptionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + subscription, diags := r.GetSubscriptionFromState(ctx, req.State) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectID := r.client.Config.ProjectID + if !subscription.GetProjectID().IsUnknown() { + projectID = subscription.GetProjectID().ValueString() + } + + // Assert resource still exists. + 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 subscription.") + 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, subscription.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..e482671db --- /dev/null +++ b/internal/provider/vaultradar/resource_radar_integration_jira_connection.go @@ -0,0 +1,168 @@ +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, + GetConnectionFromPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) { + var conn jiraConnectionResourceData + diags := plan.Get(ctx, &conn) + return &conn, diags + }, + GetConnectionFromState: 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, + Sensitive: 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..c4a761171 --- /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, + GetSubscriptionFromPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) { + var sub jiraSubscriptionResourceData + diags := plan.Get(ctx, &sub) + return &sub, diags + }, + GetSubscriptionFromState: 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 event(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. 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(), + }, + 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..f61bb9835 --- /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, + GetConnectionFromPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationConnection, diag.Diagnostics) { + var conn slackConnectionResourceData + diags := plan.Get(ctx, &conn) + return &conn, diags + }, + GetConnectionFromState: 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..04104f2f4 --- /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, + GetSubscriptionFromPlan: func(ctx context.Context, plan tfsdk.Plan) (integrationSubscription, diag.Diagnostics) { + var sub slackSubscriptionResourceData + diags := plan.Get(ctx, &sub) + return &sub, diags + }, + GetSubscriptionFromState: 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: "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(), + }, + 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 }}