Skip to content

Commit

Permalink
internal/provider: add a new cockroach_private_endpoint_trusted_owner…
Browse files Browse the repository at this point in the history
… resource (#159)

* internal/provider: add a new cockroach_private_endpoint_trusted_owner resource

This commit adds a new cockroach_private_endpoint_trusted_owner terraform
resource to manage private endpoint trusted owner entries.

* docs: update docs to include cockroach_private_endpoint_connection

This commit updates the docs to include the new
cockroach_private_endpoint_connection resource. At the same time, we add
an example for such a resource, and update the CHANGELOG file as well.
  • Loading branch information
jaylim-crl authored Aug 29, 2023
1 parent aede5f1 commit 2e0d6b7
Show file tree
Hide file tree
Showing 9 changed files with 482 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- New cockroach_private_endpoint_connection resource allows users to configure
trusted owner entries for private endpoints.

## [1.1.0] - 2023-08-15

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/resources/private_endpoint_connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ AWS PrivateLink Endpoint Connection.
### Read-Only

- `cloud_provider` (String) Cloud provider associated with this connection.
- `id` (String) Used with `terrform import`. Format is "<cluster ID>:<endpoint ID>".
- `id` (String) Used with `terraform import`. Format is "<cluster ID>:<endpoint ID>".
- `region_name` (String) Cloud provider region code associated with this connection.
- `service_id` (String) Server side ID of the PrivateLink connection.

Expand Down
30 changes: 30 additions & 0 deletions docs/resources/private_endpoint_trusted_owner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "cockroach_private_endpoint_trusted_owner Resource - terraform-provider-cockroach"
subcategory: ""
description: |-
Private Endpoint Trusted Owner.
---

# cockroach_private_endpoint_trusted_owner (Resource)

Private Endpoint Trusted Owner.



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

### Required

- `cluster_id` (String) UUID of the cluster the private endpoint trusted owner entry belongs to.
- `external_owner_id` (String) Owner ID of the private endpoint connection in the cloud provider.
- `type` (String) Representation of the external_owner_id field. Allowed values are:
* AWS_ACCOUNT_ID

### Read-Only

- `id` (String) Used with `terraform import`. Format is "<cluster ID>:<owner ID>".
- `owner_id` (String) UUID of the private endpoint trusted owner entry.


Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
variable "cluster_id" {
type = string
}

resource "cockroach_private_endpoint_trusted_owner" "example" {
cluster_id = var.cluster_id
type = "AWS_ACCOUNT_ID"
external_owner_id = "012345678901"
}
8 changes: 8 additions & 0 deletions internal/provider/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ type PrivateEndpointConnection struct {
ClusterID types.String `tfsdk:"cluster_id"`
}

type PrivateEndpointTrustedOwner struct {
ID types.String `tfsdk:"id"`
ClusterID types.String `tfsdk:"cluster_id"`
OwnerID types.String `tfsdk:"owner_id"`
Type types.String `tfsdk:"type"`
ExternalOwnerID types.String `tfsdk:"external_owner_id"`
}

type CMEKKey struct {
Status types.String `tfsdk:"status"`
UserMessage types.String `tfsdk:"user_message"`
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/private_endpoint_connection_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (r *privateEndpointConnectionResource) Schema(
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Used with `terrform import`. Format is \"<cluster ID>:<endpoint ID>\".",
MarkdownDescription: "Used with `terraform import`. Format is \"<cluster ID>:<endpoint ID>\".",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Expand Down
267 changes: 267 additions & 0 deletions internal/provider/private_endpoint_trusted_owner_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/*
Copyright 2023 The Cockroach Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package provider

import (
"context"
"fmt"
"net/http"
"regexp"

"github.com/cockroachdb/cockroach-cloud-sdk-go/pkg/client"
"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/types"
)

type privateEndpointTrustedOwnerResource struct {
provider *provider
}

// clusterID:ownerID
var (
trustedOwnerIDFmt = "%s:%s"
trustedOWnerIDRegex = regexp.MustCompile(fmt.Sprintf("^(%s):(%s)$", uuidRegexString, uuidRegexString))
)

func (r *privateEndpointTrustedOwnerResource) Schema(
_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
MarkdownDescription: "Private Endpoint Trusted Owner.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Used with `terraform import`. Format is \"<cluster ID>:<owner ID>\".",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"owner_id": schema.StringAttribute{
Computed: true,
Description: "UUID of the private endpoint trusted owner entry.",
},
"cluster_id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "UUID of the cluster the private endpoint trusted owner entry belongs to.",
},
"type": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
MarkdownDescription: "Representation of the external_owner_id field. Allowed values are:" +
formatEnumMarkdownList(client.AllowedPrivateEndpointTrustedOwnerTypeTypeEnumValues),
},
"external_owner_id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "Owner ID of the private endpoint connection in the cloud provider.",
},
},
}
}

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

func (r *privateEndpointTrustedOwnerResource) Configure(
_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
var ok bool
if r.provider, ok = req.ProviderData.(*provider); !ok {
resp.Diagnostics.AddError("Internal provider error",
fmt.Sprintf("Error in Configure: expected %T but got %T", provider{}, req.ProviderData))
}
}

func (r *privateEndpointTrustedOwnerResource) Create(
ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse,
) {
if r.provider == nil || !r.provider.configured {
addConfigureProviderErr(&resp.Diagnostics)
return
}

var plan PrivateEndpointTrustedOwner
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

addReq := client.NewCockroachCloudAddPrivateEndpointTrustedOwnerRequest(
plan.ExternalOwnerID.ValueString(),
client.PrivateEndpointTrustedOwnerTypeType(plan.Type.ValueString()),
)
addResp, _, err := r.provider.service.AddPrivateEndpointTrustedOwner(ctx, plan.ClusterID.ValueString(), addReq)
if err != nil {
resp.Diagnostics.AddError(
"Error creating private endpoint trusted owner entry",
fmt.Sprintf("Unexpected error occurred while creating private endpoint trusted owner entry: %s", formatAPIErrorMessage(err)),
)
return
}

var state PrivateEndpointTrustedOwner
loadPrivateEndpointTrustedOwnerIntoTerraformState(&addResp.TrustedOwner, &state)
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}

func (r *privateEndpointTrustedOwnerResource) Read(
ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse,
) {
if r.provider == nil || !r.provider.configured {
addConfigureProviderErr(&resp.Diagnostics)
return
}

var state PrivateEndpointTrustedOwner
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

// Validate input values.
clusterID := state.ClusterID.ValueString()
if !uuidRegex.MatchString(clusterID) {
resp.Diagnostics.AddError(
"Unexpected cluster ID format",
fmt.Sprintf("'%s' is not a valid cluster ID format. Expected UUID.", clusterID),
)
return
}
ownerUUID := state.OwnerID.ValueString()
if !uuidRegex.MatchString(ownerUUID) {
resp.Diagnostics.AddError(
"Unexpected owner ID format",
fmt.Sprintf("'%s' is not a valid owner ID format. Expected UUID.", clusterID),
)
return
}

getResp, httpResp, err := r.provider.service.GetPrivateEndpointTrustedOwner(ctx, clusterID, ownerUUID)
if err != nil {
if httpResp != nil && httpResp.StatusCode == http.StatusNotFound {
resp.Diagnostics.AddWarning(
"Private endpoint trusted owner entry not found",
fmt.Sprintf("Private endpoint trusted owner entry with ID %s is not found. Removing from state.", ownerUUID))
resp.State.RemoveResource(ctx)
} else {
resp.Diagnostics.AddError(
"Error getting private endpoint trusted owner entry",
fmt.Sprintf("Unexpected error retrieving private endpoint trusted owner entry: %s", formatAPIErrorMessage(err)))
}
return
}
loadPrivateEndpointTrustedOwnerIntoTerraformState(&getResp.TrustedOwner, &state)
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}

func loadPrivateEndpointTrustedOwnerIntoTerraformState(
trustedOwner *client.PrivateEndpointTrustedOwner, state *PrivateEndpointTrustedOwner,
) {
state.OwnerID = types.StringValue(trustedOwner.GetId())
state.ClusterID = types.StringValue(trustedOwner.GetClusterId())
state.Type = types.StringValue(string(trustedOwner.GetType()))
state.ExternalOwnerID = types.StringValue(trustedOwner.GetExternalOwnerId())
state.ID = types.StringValue(
fmt.Sprintf(trustedOwnerIDFmt, state.ClusterID.ValueString(), state.OwnerID.ValueString()),
)
}

func (r *privateEndpointTrustedOwnerResource) Update(
_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse,
) {
// No-op. Contains only requires-replace or computed fields.
}

func (r *privateEndpointTrustedOwnerResource) Delete(
ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse,
) {
if r.provider == nil || !r.provider.configured {
addConfigureProviderErr(&resp.Diagnostics)
return
}

var state PrivateEndpointTrustedOwner
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

_, _, err := r.provider.service.RemovePrivateEndpointTrustedOwner(
ctx,
state.ClusterID.ValueString(),
state.OwnerID.ValueString(),
)
if err != nil {
resp.Diagnostics.AddError(
"Error deleting private endpoint trusted owner entry",
fmt.Sprintf("Unexpected error occurred while deleting private endpoint trusted owner entry: %s", formatAPIErrorMessage(err)),
)
return
}

resp.State.RemoveResource(ctx)
}

func (r *privateEndpointTrustedOwnerResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
// A trusted owner entry is identified by two fields: the cluster ID, and
// the owner ID. In theory, the entry can be uniquely identified by the
// owner ID, but we'd like to scope that to the cluster.
//
// For import to work here, we will deserialize the terraform ID back into
// the cluster and owner IDs.
matches := trustedOWnerIDRegex.FindStringSubmatch(req.ID)
if len(matches) != 3 {
resp.Diagnostics.AddError(
"Invalid trusted owner terraform ID format",
`When importing a trusted owner entry, the ID field should follow the format "<cluster ID>:<owner ID>")`)
}
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics = resp.State.Set(ctx, &PrivateEndpointTrustedOwner{
ID: types.StringValue(req.ID),
ClusterID: types.StringValue(matches[1]),
OwnerID: types.StringValue(matches[2]),
})
}

func NewPrivateEndpointTrustedOwnerResource() resource.Resource {
return &privateEndpointTrustedOwnerResource{}
}
Loading

0 comments on commit 2e0d6b7

Please sign in to comment.