From e216aaa03559df40df034090cfb40886412b5f2c Mon Sep 17 00:00:00 2001 From: Dharsan Date: Tue, 16 Apr 2024 10:25:07 +0530 Subject: [PATCH] feat(keys): ability to add keys using Humanitec --- internal/provider/provider.go | 1 + .../provider/resource_application_user.go | 4 +- internal/provider/resource_keys.go | 245 ++++++++++++++++++ internal/provider/resource_keys_test.go | 49 ++++ 4 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 internal/provider/resource_keys.go create mode 100644 internal/provider/resource_keys_test.go 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_keys.go b/internal/provider/resource_keys.go new file mode 100644 index 0000000..1f83df1 --- /dev/null +++ b/internal/provider/resource_keys.go @@ -0,0 +1,245 @@ +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/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"` + CreatedAt types.String `tfsdk:"created_at"` + CreatedBy types.String `tfsdk:"created_by"` + ExpiredAt types.String `tfsdk:"expired_at"` + 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, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "Time that the key was created.", + Computed: true, + }, + "created_by": schema.StringAttribute{ + MarkdownDescription: "The ID of the user who created the key.", + Computed: true, + }, + "expired_at": schema.StringAttribute{ + MarkdownDescription: "Date time of the key expiration.", + Computed: true, + }, + "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.CreatedAt = types.StringValue(res.CreatedAt.String()) + data.CreatedBy = types.StringValue(res.CreatedBy) + data.ExpiredAt = types.StringValue(res.ExpiredAt.String()) + 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_keys_test.go b/internal/provider/resource_keys_test.go new file mode 100644 index 0000000..ad2ed0d --- /dev/null +++ b/internal/provider/resource_keys_test.go @@ -0,0 +1,49 @@ +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) + 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), + ), + }, + // 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 + }, + }, + // 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)) +}