From 8e9daa559e7ba573d5eba37fa71dfb8bbaa6f50b Mon Sep 17 00:00:00 2001 From: Mateusz Jenek Date: Fri, 24 May 2024 14:00:09 +0200 Subject: [PATCH] feat: add resource class resource --- docs/resources/resource_class.md | 41 +++ .../humanitec_resource_class/import.sh | 1 + .../humanitec_resource_class/resource.tf | 5 + go.mod | 2 +- go.sum | 4 +- internal/provider/provider.go | 1 + internal/provider/resource_environment.go | 2 +- internal/provider/resource_resource_class.go | 242 ++++++++++++++++++ .../provider/resource_resource_class_test.go | 62 +++++ internal/provider/resource_rule.go | 30 +-- 10 files changed, 361 insertions(+), 29 deletions(-) create mode 100644 docs/resources/resource_class.md create mode 100644 examples/resources/humanitec_resource_class/import.sh create mode 100644 examples/resources/humanitec_resource_class/resource.tf create mode 100644 internal/provider/resource_resource_class.go create mode 100644 internal/provider/resource_resource_class_test.go diff --git a/docs/resources/resource_class.md b/docs/resources/resource_class.md new file mode 100644 index 0000000..e265244 --- /dev/null +++ b/docs/resources/resource_class.md @@ -0,0 +1,41 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "humanitec_resource_class Resource - terraform-provider-humanitec" +subcategory: "" +description: |- + Resource Classes provide a way of specializing Resource Types. Developers can set the class of a Resource alongside the type in their Score File. Platform teams can match the class of a Resource via Matching Criteria. +--- + +# humanitec_resource_class (Resource) + +Resource Classes provide a way of specializing Resource Types. Developers can set the class of a Resource alongside the type in their Score File. Platform teams can match the class of a Resource via Matching Criteria. + +## Example Usage + +```terraform +resource "humanitec_resource_class" "resource_class" { + id = "example" + resource_type = "mysql" + description = "An example resource class" +} +``` + + +## Schema + +### Required + +- `id` (String) Reflects the class string. +- `resource_type` (String) Defines the resource type this class is applicable for. + +### Optional + +- `description` (String) A human readable description when this class should be used. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import humanitec_resource_class.example resource_type/class_id +``` diff --git a/examples/resources/humanitec_resource_class/import.sh b/examples/resources/humanitec_resource_class/import.sh new file mode 100644 index 0000000..8b2ec66 --- /dev/null +++ b/examples/resources/humanitec_resource_class/import.sh @@ -0,0 +1 @@ +terraform import humanitec_resource_class.example resource_type/class_id diff --git a/examples/resources/humanitec_resource_class/resource.tf b/examples/resources/humanitec_resource_class/resource.tf new file mode 100644 index 0000000..2d5669b --- /dev/null +++ b/examples/resources/humanitec_resource_class/resource.tf @@ -0,0 +1,5 @@ +resource "humanitec_resource_class" "resource_class" { + id = "example" + resource_type = "mysql" + description = "An example resource class" +} diff --git a/go.mod b/go.mod index 7cf0596..e145b4e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/terraform-plugin-go v0.22.2 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 - github.com/humanitec/humanitec-go-autogen v0.0.0-20240516095603-93b078e55cd9 + github.com/humanitec/humanitec-go-autogen v0.0.0-20240523102513-6cf639116144 github.com/stretchr/testify v1.9.0 sigs.k8s.io/yaml v1.4.0 ) diff --git a/go.sum b/go.sum index 0b8b73f..208a8fb 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,8 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/humanitec/humanitec-go-autogen v0.0.0-20240516095603-93b078e55cd9 h1:rW4rt9sN3+4JbltxMkrCkHtqFm7ZInNkjCaY9YhD/u0= -github.com/humanitec/humanitec-go-autogen v0.0.0-20240516095603-93b078e55cd9/go.mod h1:WqItJ/MhAHcjP7LIhIt2/NrgXeXRbLuxvXlin7qY0j4= +github.com/humanitec/humanitec-go-autogen v0.0.0-20240523102513-6cf639116144 h1:h1HURx3DvUaYFxGEFF+iK4eQ4mvKFJtRVnDBHeEZNBM= +github.com/humanitec/humanitec-go-autogen v0.0.0-20240523102513-6cf639116144/go.mod h1:WqItJ/MhAHcjP7LIhIt2/NrgXeXRbLuxvXlin7qY0j4= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 61db663..90529e0 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -214,6 +214,7 @@ func (p *HumanitecProvider) Resources(ctx context.Context) []func() resource.Res NewResourcePipeline, NewResourcePipelineCriteria, NewResourceRegistry, + NewResourceResourceClass, NewResourceResourceDriver, NewResourceRule, NewResourceSecretStore, diff --git a/internal/provider/resource_environment.go b/internal/provider/resource_environment.go index e0fda08..4d5f382 100644 --- a/internal/provider/resource_environment.go +++ b/internal/provider/resource_environment.go @@ -292,7 +292,7 @@ func parseEnvironmentResponse(appID string, res *client.EnvironmentResponse, dat if res.FromDeploy != nil { fromDeployId = &res.FromDeploy.Id } - + data.FromDeployID = types.StringPointerValue(fromDeployId) data.AppID = types.StringValue(appID) data.ID = types.StringValue(res.Id) diff --git a/internal/provider/resource_resource_class.go b/internal/provider/resource_resource_class.go new file mode 100644 index 0000000..2ecd4b8 --- /dev/null +++ b/internal/provider/resource_resource_class.go @@ -0,0 +1,242 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "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/humanitec/humanitec-go-autogen" + "github.com/humanitec/humanitec-go-autogen/client" +) + +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &ResourceResourceClass{} +var _ resource.ResourceWithImportState = &ResourceResourceClass{} + +func NewResourceResourceClass() resource.Resource { + return &ResourceResourceClass{} +} + +// ResourceResourceClass defines the resource implementation. +type ResourceResourceClass struct { + client *humanitec.Client + orgId string +} + +// ResourceClassModel describes the app data model. +type ResourceClassModel struct { + ID types.String `tfsdk:"id"` + ResourceType types.String `tfsdk:"resource_type"` + Description types.String `tfsdk:"description"` +} + +func (r *ResourceResourceClass) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_resource_class" +} + +func (r *ResourceResourceClass) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Resource Classes provide a way of specializing Resource Types. Developers can set the class of a Resource alongside the type in their Score File. Platform teams can match the class of a Resource via Matching Criteria.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Reflects the class string.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "resource_type": schema.StringAttribute{ + MarkdownDescription: "Defines the resource type this class is applicable for.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "A human readable description when this class should be used.", + Optional: true, + }, + }, + } +} + +func (r *ResourceResourceClass) 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 (r *ResourceResourceClass) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *ResourceClassModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + id := data.ID.ValueString() + resourceType := data.ResourceType.ValueString() + description := data.Description.ValueString() + + httpResp, err := r.client.CreateResourceClassWithResponse(ctx, r.orgId, resourceType, client.ResourceClassRequest{ + Id: id, + Description: description, + }) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create resource class, got error: %s", err)) + return + } + + if httpResp.StatusCode() != 200 { + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to create resource class, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body)) + return + } + + parseResourceClassResponse(httpResp.JSON200, data) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ResourceResourceClass) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *ResourceClassModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + id := data.ID.ValueString() + resourceType := data.ResourceType.ValueString() + + httpResp, err := r.client.GetResourceClassWithResponse(ctx, r.orgId, resourceType, id) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to read resource class, got error: %s", err)) + return + } + + if httpResp.StatusCode() != 200 { + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to read resource class, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body)) + return + } + + parseResourceClassResponse(httpResp.JSON200, data) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ResourceResourceClass) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *ResourceClassModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id := data.ID.ValueString() + resourceType := data.ResourceType.ValueString() + description := data.Description.ValueString() + + httpResp, err := r.client.UpdateResourceClassWithResponse(ctx, r.orgId, resourceType, id, client.UpdateResourceClassRequest{ + Description: description, + }) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update resource class, got error: %s", err)) + return + } + + if httpResp.StatusCode() != 200 { + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to update resource class, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body)) + return + } + + parseResourceClassResponse(httpResp.JSON200, data) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ResourceResourceClass) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *ResourceClassModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id := data.ID.ValueString() + resourceType := data.ResourceType.ValueString() + + httpResp, err := r.client.DeleteResourceClassWithResponse(ctx, r.orgId, resourceType, id) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete resource class, got error: %s", err)) + return + } + + if httpResp.StatusCode() != 204 { + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to delete resource class, unexpected status code: %d, body: %s", httpResp.StatusCode(), httpResp.Body)) + return + } +} + +func (r *ResourceResourceClass) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, "/") + + // ensure idParts elements are not empty + for _, idPart := range idParts { + if idPart == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: resource_type/class_id. Got: %q", req.ID), + ) + return + } + } + + if len(idParts) == 2 { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("resource_type"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...) + } else { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: resource_type/class_id. Got: %q", req.ID), + ) + return + } +} + +func parseResourceClassResponse(resp *client.ResourceClassResponse, data *ResourceClassModel) { + data.ID = types.StringValue(resp.Id) + data.ResourceType = types.StringValue(resp.ResourceType) + data.Description = types.StringValue(resp.Description) +} diff --git a/internal/provider/resource_resource_class_test.go b/internal/provider/resource_resource_class_test.go new file mode 100644 index 0000000..8b2f636 --- /dev/null +++ b/internal/provider/resource_resource_class_test.go @@ -0,0 +1,62 @@ +package provider + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccResourceClass(t *testing.T) { + id := fmt.Sprintf("test-%d", time.Now().UnixNano()) + description := "test-description" + updatedDescription := "test-updated-description" + resourceType := "mysql" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccResourceClass(id, description, resourceType), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("humanitec_resource_class.class_test", "id", id), + resource.TestCheckResourceAttr("humanitec_resource_class.class_test", "description", description), + resource.TestCheckResourceAttr("humanitec_resource_class.class_test", "resource_type", resourceType), + ), + }, + // Update testing + { + Config: testAccResourceClass(id, updatedDescription, resourceType), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("humanitec_resource_class.class_test", "id", id), + resource.TestCheckResourceAttr("humanitec_resource_class.class_test", "description", updatedDescription), + resource.TestCheckResourceAttr("humanitec_resource_class.class_test", "resource_type", resourceType), + ), + }, + // ImportState testing + { + ResourceName: "humanitec_resource_class.class_test", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return fmt.Sprintf("%s/%s", resourceType, id), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccResourceClass(id, description, resourceType string) string { + return fmt.Sprintf(` +resource "humanitec_resource_class" "class_test" { + id = "%s" + description = "%s" + resource_type = "%s" +} +`, id, description, resourceType) +} diff --git a/internal/provider/resource_rule.go b/internal/provider/resource_rule.go index abf7166..0c73bfc 100644 --- a/internal/provider/resource_rule.go +++ b/internal/provider/resource_rule.go @@ -173,7 +173,7 @@ func (r *ResourceRule) Create(ctx context.Context, req resource.CreateRequest, r return } - httpResp, err := r.client.PostOrgsOrgIdAppsAppIdEnvsEnvIdRulesWithResponse(ctx, r.orgId, appID, envID, *httpBody) + httpResp, err := r.client.CreateAutomationRuleWithResponse(ctx, r.orgId, appID, envID, *httpBody) if err != nil { resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create rule, got error: %s", err)) return @@ -204,7 +204,7 @@ func (r *ResourceRule) Read(ctx context.Context, req resource.ReadRequest, resp envID := data.EnvID.ValueString() id := data.ID.ValueString() - httpResp, err := r.client.GetOrgsOrgIdAppsAppIdEnvsEnvIdRulesWithResponse(ctx, r.orgId, appID, envID) + httpResp, err := r.client.GetAutomationRuleWithResponse(ctx, r.orgId, appID, envID, id) if err != nil { resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to read rule, got error: %s", err)) return @@ -214,27 +214,7 @@ func (r *ResourceRule) Read(ctx context.Context, req resource.ReadRequest, resp return } - if httpResp.JSON200 == nil { - resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to read rule, missing body, body: %s", httpResp.Body)) - return - } - - var rule *client.AutomationRuleResponse - - for _, v := range *httpResp.JSON200 { - if id == v.Id { - rule = &v - break - } - } - - if rule == nil { - resp.Diagnostics.AddWarning("Rule not found", fmt.Sprintf("The rule (%s) was deleted outside Terraform", id)) - resp.State.RemoveResource(ctx) - return - } - - parseAutomationRuleResponse(rule, data) + parseAutomationRuleResponse(httpResp.JSON200, data) // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -260,7 +240,7 @@ func (r *ResourceRule) Update(ctx context.Context, req resource.UpdateRequest, r return } - httpResp, err := r.client.PutOrgsOrgIdAppsAppIdEnvsEnvIdRulesRuleIdWithResponse(ctx, r.orgId, appID, envID, id, *httpBody) + httpResp, err := r.client.UpdateAutomationRuleWithResponse(ctx, r.orgId, appID, envID, id, *httpBody) if err != nil { resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update rule, got error: %s", err)) return @@ -290,7 +270,7 @@ func (r *ResourceRule) Delete(ctx context.Context, req resource.DeleteRequest, r envID := data.EnvID.ValueString() id := data.ID.ValueString() - httpResp, err := r.client.DeleteOrgsOrgIdAppsAppIdEnvsEnvIdRulesRuleIdWithResponse(ctx, r.orgId, appID, envID, id) + httpResp, err := r.client.DeleteAutomationRuleWithResponse(ctx, r.orgId, appID, envID, id) if err != nil { resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete rule, got error: %s", err)) return