Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented HVS Twilio Integration resource #1081

Merged
merged 6 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/1081.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
add vault_secrets_integration_twilio resource
```
64 changes: 64 additions & 0 deletions docs/resources/vault_secrets_integration_twilio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "hcp_vault_secrets_integration_twilio Resource - terraform-provider-hcp"
subcategory: ""
description: |-
The Vault Secrets Twilio integration resource manages a Twilio integration.
---

# hcp_vault_secrets_integration_twilio (Resource)

The Vault Secrets Twilio integration resource manages a Twilio integration.

## Example Usage

```terraform
resource "hcp_vault_secrets_integration_twilio" "example" {
name = "my-twilio-1"
capabilities = ["ROTATION"]
static_credential_details = {
account_sid = "AC7..."
api_key_sid = "TKa..."
api_key_secret = "6aG..."
}
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `capabilities` (Set of String) Capabilities enabled for the integration. See the Vault Secrets documentation for the list of supported capabilities per provider.
- `name` (String) The Vault Secrets integration name.

### Optional

- `project_id` (String) HCP project ID that owns the HCP Vault Secrets integration. Inferred from the provider configuration if omitted.
- `static_credential_details` (Attributes) Twilio API key parts used to authenticate against the target Twilio account. (see [below for nested schema](#nestedatt--static_credential_details))

### Read-Only

- `organization_id` (String) HCP organization ID that owns the HCP Vault Secrets integration.
- `resource_id` (String) Resource ID used to uniquely identify the integration instance on the HCP platform.
- `resource_name` (String) Resource name used to uniquely identify the integration instance on the HCP platform.

<a id="nestedatt--static_credential_details"></a>
### Nested Schema for `static_credential_details`

Required:

- `account_sid` (String) Account SID for the target Twilio account.
- `api_key_secret` (String, Sensitive) Api key secret used with the api key SID to authenticate against the target Twilio account.
- `api_key_sid` (String) Api key SID to authenticate against the target Twilio account.

## Import

Import is supported using the following syntax:

```shell
# Vault Secrets Twilio Integration can be imported by specifying the name of the integration
# Note that since the Api Key secret is never returned on the Vault Secrets API,
# the next plan or apply will show a diff for that field.
terraform import hcp_vault_secrets_integration_twilio.example my-twilio-1
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Vault Secrets Twilio Integration can be imported by specifying the name of the integration
# Note that since the Api Key secret is never returned on the Vault Secrets API,
# the next plan or apply will show a diff for that field.
terraform import hcp_vault_secrets_integration_twilio.example my-twilio-1
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "hcp_vault_secrets_integration_twilio" "example" {
name = "my-twilio-1"
capabilities = ["ROTATION"]
static_credential_details = {
account_sid = "AC7..."
api_key_sid = "TKa..."
api_key_secret = "6aG..."
}
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res
vaultsecrets.NewVaultSecretsAppIAMPolicyResource,
vaultsecrets.NewVaultSecretsAppIAMBindingResource,
vaultsecrets.NewVaultSecretsIntegrationAWSResource,
vaultsecrets.NewVaultSecretsIntegrationTwilioResource,
// IAM
iam.NewServicePrincipalResource,
iam.NewServicePrincipalKeyResource,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vaultsecrets

import (
"context"
"fmt"

"github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service"
secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-provider-hcp/internal/clients"
"github.com/hashicorp/terraform-provider-hcp/internal/provider/modifiers"
"golang.org/x/exp/maps"
)

type IntegrationTwilio struct {
// Input fields
ProjectID types.String `tfsdk:"project_id"`
Name types.String `tfsdk:"name"`
Capabilities types.Set `tfsdk:"capabilities"`
StaticCredentialDetails types.Object `tfsdk:"static_credential_details"`

// Computed fields
OrganizationID types.String `tfsdk:"organization_id"`
ResourceID types.String `tfsdk:"resource_id"`
ResourceName types.String `tfsdk:"resource_name"`

// Inner API-compatible models derived from the Terraform fields
capabilities []*secretmodels.Secrets20231128Capability `tfsdk:"-"`
staticCredentialDetails *secretmodels.Secrets20231128TwilioStaticCredentialsRequest `tfsdk:"-"`
}

// Helper structs to help populate concrete targets from types.Object fields
type staticCredentialDetails struct {
AccountSID types.String `tfsdk:"account_sid"`
APIKeySID types.String `tfsdk:"api_key_sid"`
APIKeySecret types.String `tfsdk:"api_key_secret"`
}

var _ resource.Resource = &resourceVaultSecretsIntegrationTwilio{}
var _ resource.ResourceWithConfigure = &resourceVaultSecretsIntegrationTwilio{}
var _ resource.ResourceWithModifyPlan = &resourceVaultSecretsIntegrationTwilio{}
var _ resource.ResourceWithImportState = &resourceVaultSecretsIntegrationTwilio{}

func NewVaultSecretsIntegrationTwilioResource() resource.Resource {
return &resourceVaultSecretsIntegrationTwilio{}
}

type resourceVaultSecretsIntegrationTwilio struct {
client *clients.Client
}

func (r *resourceVaultSecretsIntegrationTwilio) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vault_secrets_integration_twilio"
}

func (r *resourceVaultSecretsIntegrationTwilio) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
attributes := map[string]schema.Attribute{
"static_credential_details": schema.SingleNestedAttribute{
Description: "Twilio API key parts used to authenticate against the target Twilio account.",
Optional: true,
Attributes: map[string]schema.Attribute{
"account_sid": schema.StringAttribute{
Description: "Account SID for the target Twilio account.",
Required: true,
},
"api_key_sid": schema.StringAttribute{
Description: "Api key SID to authenticate against the target Twilio account.",
Required: true,
},
"api_key_secret": schema.StringAttribute{
Description: "Api key secret used with the api key SID to authenticate against the target Twilio account.",
Required: true,
Sensitive: true,
},
},
},
}

maps.Copy(attributes, sharedIntegrationAttributes)

resp.Schema = schema.Schema{
MarkdownDescription: "The Vault Secrets Twilio integration resource manages a Twilio integration.",
Attributes: attributes,
}
}

func (r *resourceVaultSecretsIntegrationTwilio) 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 Data Source Configure Type",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: is the error expected to be formatted in Title Case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, this config block is used by ~30 different resources this is just some shared boilerplate so I'm not sure if this was intended but I'll keep it for consistency with the others.

fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}

func (r *resourceVaultSecretsIntegrationTwilio) 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 *resourceVaultSecretsIntegrationTwilio) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.State.Get, "reading", func(i integration) (any, error) {
integration, ok := i.(*IntegrationTwilio)
if !ok {
return nil, fmt.Errorf("invalid integration type, expected *IntegrationTwilio, got: %T, this is a bug on the provider", i)
}

response, err := r.client.VaultSecretsPreview.GetTwilioIntegration(
secret_service.NewGetTwilioIntegrationParamsWithContext(ctx).
WithOrganizationID(integration.OrganizationID.ValueString()).
WithProjectID(integration.ProjectID.ValueString()).
WithName(integration.Name.ValueString()), nil)
if err != nil && !clients.IsResponseCodeNotFound(err) {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Integration, nil
})...)
}

func (r *resourceVaultSecretsIntegrationTwilio) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.Plan.Get, "creating", func(i integration) (any, error) {
integration, ok := i.(*IntegrationTwilio)
if !ok {
return nil, fmt.Errorf("invalid integration type, expected *IntegrationTwilio, got: %T, this is a bug on the provider", i)
}

response, err := r.client.VaultSecretsPreview.CreateTwilioIntegration(&secret_service.CreateTwilioIntegrationParams{
Body: &secretmodels.SecretServiceCreateTwilioIntegrationBody{
Capabilities: integration.capabilities,
StaticCredentialDetails: integration.staticCredentialDetails,
Name: integration.Name.ValueString(),
},
OrganizationID: integration.OrganizationID.ValueString(),
ProjectID: integration.ProjectID.ValueString(),
}, nil)
if err != nil && !clients.IsResponseCodeNotFound(err) {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Integration, nil
})...)
}

func (r *resourceVaultSecretsIntegrationTwilio) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.Plan.Get, "updating", func(i integration) (any, error) {
integration, ok := i.(*IntegrationTwilio)
if !ok {
return nil, fmt.Errorf("invalid integration type, expected *IntegrationTwilio, got: %T, this is a bug on the provider", i)
}

response, err := r.client.VaultSecretsPreview.UpdateTwilioIntegration(&secret_service.UpdateTwilioIntegrationParams{
Body: &secretmodels.SecretServiceUpdateTwilioIntegrationBody{
Capabilities: integration.capabilities,
StaticCredentialDetails: integration.staticCredentialDetails,
},
Name: integration.Name.ValueString(),
OrganizationID: integration.OrganizationID.ValueString(),
ProjectID: integration.ProjectID.ValueString(),
}, nil)
if err != nil && !clients.IsResponseCodeNotFound(err) {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Integration, nil
})...)
}

func (r *resourceVaultSecretsIntegrationTwilio) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
resp.Diagnostics.Append(decorateOperation[*IntegrationTwilio](ctx, r.client, &resp.State, req.State.Get, "deleting", func(i integration) (any, error) {
integration, ok := i.(*IntegrationTwilio)
if !ok {
return nil, fmt.Errorf("invalid integration type, expected *IntegrationTwilio, got: %T, this is a bug on the provider", i)
}

_, err := r.client.VaultSecretsPreview.DeleteTwilioIntegration(
secret_service.NewDeleteTwilioIntegrationParamsWithContext(ctx).
WithOrganizationID(integration.OrganizationID.ValueString()).
WithProjectID(integration.ProjectID.ValueString()).
WithName(integration.Name.ValueString()), nil)
if err != nil && !clients.IsResponseCodeNotFound(err) {
return nil, err
}
return nil, nil
})...)
}

func (r *resourceVaultSecretsIntegrationTwilio) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// The Vault Secrets API does not return sensitive values like the secret access key, so they will be initialized to an empty value
// It means the first plan/apply after a successful import will always show a diff for the secret access key
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), r.client.Config.OrganizationID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), r.client.Config.ProjectID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), req.ID)...)
}

var _ integration = &IntegrationTwilio{}

func (i *IntegrationTwilio) projectID() types.String {
return i.ProjectID

}
func (i *IntegrationTwilio) initModel(ctx context.Context, orgID, projID string) diag.Diagnostics {
// Init fields that depend on the Terraform provider configuration
i.OrganizationID = types.StringValue(orgID)
i.ProjectID = types.StringValue(projID)

// Init the HVS domain models from the Terraform domain models
var capabilities []types.String
diags := i.Capabilities.ElementsAs(ctx, &capabilities, false)
if diags.HasError() {
return diags
}
for _, c := range capabilities {
i.capabilities = append(i.capabilities, secretmodels.Secrets20231128Capability(c.ValueString()).Pointer())
}

if !i.StaticCredentialDetails.IsNull() {
scd := staticCredentialDetails{}
diags = i.StaticCredentialDetails.As(ctx, &scd, basetypes.ObjectAsOptions{})
if diags.HasError() {
return diags
}

i.staticCredentialDetails = &secretmodels.Secrets20231128TwilioStaticCredentialsRequest{
AccountSid: scd.AccountSID.ValueString(),
APIKeySecret: scd.APIKeySecret.ValueString(),
APIKeySid: scd.APIKeySID.ValueString(),
}
}

return diag.Diagnostics{}
}

func (i *IntegrationTwilio) fromModel(ctx context.Context, orgID, projID string, model any) diag.Diagnostics {
diags := diag.Diagnostics{}

integrationModel, ok := model.(*secretmodels.Secrets20231128TwilioIntegration)
if !ok {
diags.AddError("Invalid model type, this is a bug on the provider.", fmt.Sprintf("Expected *secretmodels.Secrets20231128TwilioIntegration, got: %T", model))
return diags
}

i.OrganizationID = types.StringValue(orgID)
i.ProjectID = types.StringValue(projID)
// TODO These fields are not returned by the API on updates, so we use the state value if they are blank on the model as a stopgap until it gets fixed on HVS
if integrationModel.ResourceID != "" {
i.ResourceID = types.StringValue(integrationModel.ResourceID)
}
if integrationModel.ResourceName != "" {
i.ResourceName = types.StringValue(integrationModel.ResourceName)
}
if integrationModel.Name != "" {
i.Name = types.StringValue(integrationModel.Name)
}

var values []attr.Value
for _, c := range integrationModel.Capabilities {
values = append(values, types.StringValue(string(*c)))
}
i.Capabilities, diags = types.SetValue(types.StringType, values)
if diags.HasError() {
return diags
}

if integrationModel.StaticCredentialDetails != nil {
// The secret key is not returned by the API, so we use an empty value (e.g. for imports) or the state value (e.g. for updates)
apiKeySecret := ""
if i.staticCredentialDetails != nil {
apiKeySecret = i.staticCredentialDetails.APIKeySecret
}

i.StaticCredentialDetails, diags = types.ObjectValue(i.StaticCredentialDetails.AttributeTypes(ctx), map[string]attr.Value{
"account_sid": types.StringValue(integrationModel.StaticCredentialDetails.AccountSid),
"api_key_sid": types.StringValue(integrationModel.StaticCredentialDetails.APIKeySid),
"api_key_secret": types.StringValue(apiKeySecret),
})
if diags.HasError() {
return diags
}
}

return diags
}
Loading