diff --git a/docs/resources/key.md b/docs/resources/key.md new file mode 100644 index 0000000..44944c2 --- /dev/null +++ b/docs/resources/key.md @@ -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 + +### 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. + + +### 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 +``` diff --git a/examples/resources/humanitec_key/import.sh b/examples/resources/humanitec_key/import.sh new file mode 100755 index 0000000..aadb666 --- /dev/null +++ b/examples/resources/humanitec_key/import.sh @@ -0,0 +1 @@ +terraform import humanitec_key.example key_id diff --git a/examples/resources/humanitec_key/resource.tf b/examples/resources/humanitec_key/resource.tf new file mode 100644 index 0000000..1268fd1 --- /dev/null +++ b/examples/resources/humanitec_key/resource.tf @@ -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" +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 3cbdd4f..89d3fa8 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -161,6 +161,7 @@ func (p *HumanitecProvider) Resources(ctx context.Context) []func() resource.Res NewResourceDefinitionResource, NewResourceEnvironmentType, NewResourceEnvironmentTypeUser, + NewResourceKey, NewResourcePipeline, NewResourcePipelineCriteria, NewResourceRegistry, diff --git a/internal/provider/resource_application_user.go b/internal/provider/resource_application_user.go index d0e3bc8..2e1aab6 100644 --- a/internal/provider/resource_application_user.go +++ b/internal/provider/resource_application_user.go @@ -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"` diff --git a/internal/provider/resource_key.go b/internal/provider/resource_key.go new file mode 100644 index 0000000..be3a5bf --- /dev/null +++ b/internal/provider/resource_key.go @@ -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) +} diff --git a/internal/provider/resource_key_test.go b/internal/provider/resource_key_test.go new file mode 100644 index 0000000..3df7091 --- /dev/null +++ b/internal/provider/resource_key_test.go @@ -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)) +}