-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(keys): ability to add keys using Humanitec
- Loading branch information
Showing
7 changed files
with
349 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
terraform import humanitec_key.example key_id |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |