Skip to content

Commit

Permalink
feat(keys): ability to add keys using Humanitec
Browse files Browse the repository at this point in the history
  • Loading branch information
dharsanb authored Apr 16, 2024
1 parent e33c9af commit bb3c53d
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 2 deletions.
53 changes: 53 additions & 0 deletions docs/resources/key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "humanitec_key Resource - terraform-provider-humanitec"
subcategory: ""
description: |-
A key is used by Humanitec to ensure ensure access to Humanitec hosted drivers.
The key helps Humanitec operator to establish identity against the Humanitec Driver API
---

# humanitec_key (Resource)

A key is used by Humanitec to ensure ensure access to Humanitec hosted drivers.
The key helps Humanitec operator to establish identity against the Humanitec Driver API

## Example Usage

```terraform
resource "humanitec_key" "example" {
key = "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx49tM67P+PDklVOXbbGo\nH3Z5KveonK/bjdiAXsgD8TwGG5w9cP4IRSXKFboHS16Sg4CiZOZdBuJmmfFT7VHK\ni/NIThcD0vuF8UoOQV72Fla+Qb315kWxhlxhVVd6kdqQf4SqVthzzExBMfDyYnLl\n12uFy24XVPGWp9yrFOCrI2pX9/F3aUZh4S1/vDq8pdVBaE302v31aQmMboJqgQVf\nUuvDlFsBPnzvjPjVZhlI/pAP6qfySJ2P6yU6RKCE2HlYtGs499Hvuy+GZTBzd/9+\nsZBqJHwtG2Qwh9vu8PNKUqmAqiSOoOKX4H0xz3Nj4SD/6/qPiCW0e/M2Ws/hXJSv\ntTLud8KNHP6u7aNPYg+V/l6cWcsFr/ZOoMMhqzkEOtKaxCH9c0NqCBv7QxxzF5Md\nt2oHyGrg1QiZd4U2BgWToMbyEaUKJ4G0nFPKYfZh7Udcrt7Vpgpci7jd2W73oWzS\nVhaEyCWgZRnZXXicgT8R55OQdSPXyZcLg57tBP4oursMHGYteSOYSw6nOpc+npW+\nishTpHN52g+z0GLsP7YHZ4oggveKK/7ZNUgBLrJrbhBmPsU/xNqu2jewfC3rEO1X\nbIyD6471lEhdiooy8piRl05vv5uJb3A+vPVvHt6l2koCqKGKOYnfY/okxV7rVD0i\ncOVo7D7KNwPy+CNwZIEDJAcCAwEAAQ==\n-----END PUBLIC KEY-----\n"
}
```

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

### Required

- `key` (String) The public key that is used for authentication.

### Optional

- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

### Read-Only

- `fingerprint` (String) Hexadecimal representation of the SHA256 hash of the DER representation of the key.
- `id` (String) The ID which refers to a specific key.

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

Optional:

- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs.
- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled.

## Import

Import is supported using the following syntax:

```shell
terraform import humanitec_key.example key_id
```
1 change: 1 addition & 0 deletions examples/resources/humanitec_key/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import humanitec_key.example key_id
3 changes: 3 additions & 0 deletions examples/resources/humanitec_key/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource "humanitec_key" "example" {
key = "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx49tM67P+PDklVOXbbGo\nH3Z5KveonK/bjdiAXsgD8TwGG5w9cP4IRSXKFboHS16Sg4CiZOZdBuJmmfFT7VHK\ni/NIThcD0vuF8UoOQV72Fla+Qb315kWxhlxhVVd6kdqQf4SqVthzzExBMfDyYnLl\n12uFy24XVPGWp9yrFOCrI2pX9/F3aUZh4S1/vDq8pdVBaE302v31aQmMboJqgQVf\nUuvDlFsBPnzvjPjVZhlI/pAP6qfySJ2P6yU6RKCE2HlYtGs499Hvuy+GZTBzd/9+\nsZBqJHwtG2Qwh9vu8PNKUqmAqiSOoOKX4H0xz3Nj4SD/6/qPiCW0e/M2Ws/hXJSv\ntTLud8KNHP6u7aNPYg+V/l6cWcsFr/ZOoMMhqzkEOtKaxCH9c0NqCBv7QxxzF5Md\nt2oHyGrg1QiZd4U2BgWToMbyEaUKJ4G0nFPKYfZh7Udcrt7Vpgpci7jd2W73oWzS\nVhaEyCWgZRnZXXicgT8R55OQdSPXyZcLg57tBP4oursMHGYteSOYSw6nOpc+npW+\nishTpHN52g+z0GLsP7YHZ4oggveKK/7ZNUgBLrJrbhBmPsU/xNqu2jewfC3rEO1X\nbIyD6471lEhdiooy8piRl05vv5uJb3A+vPVvHt6l2koCqKGKOYnfY/okxV7rVD0i\ncOVo7D7KNwPy+CNwZIEDJAcCAwEAAQ==\n-----END PUBLIC KEY-----\n"
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func (p *HumanitecProvider) Resources(ctx context.Context) []func() resource.Res
NewResourceDefinitionResource,
NewResourceEnvironmentType,
NewResourceEnvironmentTypeUser,
NewResourceKey,
NewResourcePipeline,
NewResourcePipelineCriteria,
NewResourceRegistry,
Expand Down
4 changes: 2 additions & 2 deletions internal/provider/resource_application_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ func NewResourceApplicationUser() resource.Resource {
return &ResourceApplicationUser{}
}

// ResourceDefinitionResource defines the resource implementation.
// ResourceApplicationUser defines the application user implementation.
type ResourceApplicationUser struct {
client *humanitec.Client
orgId string
}

// DefinitionResourceModel describes the resource data model.
// ResourceApplicationUserModel describes the application user data model.
type ResourceApplicationUserModel struct {
ID types.String `tfsdk:"id"`
AppID types.String `tfsdk:"app_id"`
Expand Down
233 changes: 233 additions & 0 deletions internal/provider/resource_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package provider

import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"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/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"

"github.com/humanitec/humanitec-go-autogen"
"github.com/humanitec/humanitec-go-autogen/client"
)

// Ensure provider defined types fully satisfy framework interfaces
var _ resource.Resource = &ResourceKey{}
var _ resource.ResourceWithImportState = &ResourceKey{}

var defaultKeysReadTimeout = 2 * time.Minute
var defaultKeysDeleteTimeout = 2 * time.Minute

func NewResourceKey() resource.Resource {
return &ResourceKey{}
}

// ResourceKey defines the resource implementation.
type ResourceKey struct {
client *humanitec.Client
orgId string
}

// OperatorKeyModel describes the key data model.
type OperatorKeyModel struct {
ID types.String `tfsdk:"id"`
Key types.String `tfsdk:"key"`
Fingerprint types.String `tfsdk:"fingerprint"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}

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

func (r *ResourceKey) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: `A key is used by Humanitec to ensure ensure access to Humanitec hosted drivers.
The key helps Humanitec operator to establish identity against the Humanitec Driver API`,

Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The ID which refers to a specific key.",
Computed: true,
},
"key": schema.StringAttribute{
MarkdownDescription: "The public key that is used for authentication.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"fingerprint": schema.StringAttribute{
MarkdownDescription: "Hexadecimal representation of the SHA256 hash of the DER representation of the key.",
Computed: true,
},
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
Read: true,
Delete: true,
}),
},
}
}

func (r *ResourceKey) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

resdata, ok := req.ProviderData.(*HumanitecData)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = resdata.Client
r.orgId = resdata.OrgID
}

func parseKeysResponse(res *client.PublicKey, data *OperatorKeyModel) {
data.ID = types.StringValue(res.Id)
data.Key = types.StringValue(res.Key)
data.Fingerprint = types.StringValue(res.Fingerprint)
}

func (r *ResourceKey) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *OperatorKeyModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

key := data.Key.ValueString()

httpResp, err := r.client.CreatePublicKeyWithResponse(ctx, r.orgId, key)
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to upload key, got error: %s", err))
return
}

if httpResp.StatusCode() != 200 {
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to upload key, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body))
return
}

parseKeysResponse(httpResp.JSON200, data)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ResourceKey) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *OperatorKeyModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

readTimeout, diags := data.Timeouts.Read(ctx, defaultKeysReadTimeout)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

var httpResp *client.GetPublicKeyResponse

err := retry.RetryContext(ctx, readTimeout, func() *retry.RetryError {
var err error

httpResp, err = r.client.GetPublicKeyWithResponse(ctx, r.orgId, data.ID.ValueString())
if err != nil {
return retry.NonRetryableError(err)
}

if httpResp.StatusCode() == 404 {
return nil
}

if httpResp.StatusCode() != 200 {
return retry.RetryableError(err)
}

return nil
})
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to read key, got error: %s", err))
return
}

if httpResp.StatusCode() == 404 {
resp.Diagnostics.AddWarning("Key not found", fmt.Sprintf("The key (%s) was deleted outside Terraform", data.ID.ValueString()))
resp.State.RemoveResource(ctx)
return
}

parseKeysResponse(httpResp.JSON200, data)

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ResourceKey) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
resp.Diagnostics.AddError("UNSUPPORTED_OPERATION", "Updating a key is currently not supported")
}

func (r *ResourceKey) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *OperatorKeyModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

deleteTimeout, diags := data.Timeouts.Delete(ctx, defaultKeysDeleteTimeout)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

// Remove the key
keyID := data.ID.ValueString()
err := retry.RetryContext(ctx, deleteTimeout, func() *retry.RetryError {
httpResp, err := r.client.DeletePublicKeyWithResponse(ctx, r.orgId, keyID)
if err != nil {
return retry.NonRetryableError(err)
}

if httpResp.StatusCode() == 204 || httpResp.StatusCode() == 404 {
return nil
}

if httpResp.StatusCode() == 403 {
return retry.NonRetryableError(fmt.Errorf("unable to delete key, unauthorized access. status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body))
}

return retry.RetryableError(fmt.Errorf("unable to delete key, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body))
})
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete key, got error: %s", err))
return
}
}

func (r *ResourceKey) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
56 changes: 56 additions & 0 deletions internal/provider/resource_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package provider

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

func TestAccResourceKeys(t *testing.T) {
key := getPublicKey(t)
var id string
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: testAccResourceKey(key),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("humanitec_key.key_test", "key", key),
resource.TestCheckResourceAttrSet("humanitec_key.key_test", "id"),
resource.TestCheckResourceAttrSet("humanitec_key.key_test", "fingerprint"),
),
},
// ImportState testing
{
ResourceName: "humanitec_key.key_test",
ImportState: true,
ImportStateVerify: true,
ImportStateIdFunc: func(s *terraform.State) (string, error) {
id = s.RootModule().Resources["humanitec_key.key_test"].Primary.Attributes["id"]
return id, nil
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("humanitec_key.key_test", "key", key),
resource.TestCheckResourceAttr("humanitec_key.key_test", "id", id),
),
},
// Delete testing automatically occurs in TestCase
},
})
}

func testAccResourceKey(key string) string {
return fmt.Sprintf(`
resource "humanitec_key" "key_test" {
key = %v
}
output "key_id" {
value = humanitec_key.key_test.id
}
`, toSingleLineTerraformString(key))
}

0 comments on commit bb3c53d

Please sign in to comment.