diff --git a/docs/resources/pipeline_criteria.md b/docs/resources/pipeline_criteria.md new file mode 100644 index 0000000..10aee04 --- /dev/null +++ b/docs/resources/pipeline_criteria.md @@ -0,0 +1,63 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "humanitec_pipeline_criteria Resource - terraform-provider-humanitec" +subcategory: "" +description: |- + Pipeline criteria link Pipelines to applicable triggers in the application. The only + supported trigger type today is "deployment_request" which specifies that the Pipeline should be used for deployments + in any environment which matches the criteria. +--- + +# humanitec_pipeline_criteria (Resource) + +Pipeline criteria link Pipelines to applicable triggers in the application. The only +supported trigger type today is "deployment_request" which specifies that the Pipeline should be used for deployments +in any environment which matches the criteria. + +## Example Usage + +```terraform +resource "humanitec_pipeline_criteria" "example" { + app_id = humanitec_application.example.id + pipeline_id = humanitec_pipeline.example.id + deployment_request = { + env_type = "development" + deployment_type = "deploy" + } +} +``` + + +## Schema + +### Required + +- `app_id` (String) The id of the Application containing the Pipeline. +- `deployment_request` (Attributes) The criteria required to match a deployment request. (see [below for nested schema](#nestedatt--deployment_request)) +- `pipeline_id` (String) The id of the Pipeline. + +### Read-Only + +- `id` (String) The id of the Pipeline Criteria. +- `pipeline_name` (String) The name of the Pipeline. + + +### Nested Schema for `deployment_request` + +Optional: + +- `deployment_type` (String) The deployment type for this criteria to match ('deploy' or 're-deploy'). +- `env_id` (String) The environment id for this criteria to match. +- `env_type` (String) The environment type for this criteria to match. + +Read-Only: + +- `app_id` (String) The Application id for this criteria to match. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import humanitec_pipeline_criteria.example app_id/pipeline_id/criteria_id +``` diff --git a/examples/resources/humanitec_pipeline_criteria/import.sh b/examples/resources/humanitec_pipeline_criteria/import.sh new file mode 100755 index 0000000..e673902 --- /dev/null +++ b/examples/resources/humanitec_pipeline_criteria/import.sh @@ -0,0 +1 @@ +terraform import humanitec_pipeline_criteria.example app_id/pipeline_id/criteria_id diff --git a/examples/resources/humanitec_pipeline_criteria/resource.tf b/examples/resources/humanitec_pipeline_criteria/resource.tf new file mode 100644 index 0000000..ef98df7 --- /dev/null +++ b/examples/resources/humanitec_pipeline_criteria/resource.tf @@ -0,0 +1,8 @@ +resource "humanitec_pipeline_criteria" "example" { + app_id = humanitec_application.example.id + pipeline_id = humanitec_pipeline.example.id + deployment_request = { + env_type = "development" + deployment_type = "deploy" + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 6d9cb0d..e3c0378 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -172,6 +172,7 @@ func (p *HumanitecProvider) Resources(ctx context.Context) []func() resource.Res NewResourceEnvironmentTypeUser(true), NewResourceEnvironmentTypeUser(false), NewResourcePipeline, + NewResourcePipelineCriteria, NewResourceRegistry, NewResourceResourceDriver, NewResourceRule, diff --git a/internal/provider/resource_pipeline_criteria.go b/internal/provider/resource_pipeline_criteria.go new file mode 100644 index 0000000..6cb9ee2 --- /dev/null +++ b/internal/provider/resource_pipeline_criteria.go @@ -0,0 +1,279 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/objectplanmodifier" + "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 = &ResourcePipelineCriteria{} +var _ resource.ResourceWithImportState = &ResourcePipelineCriteria{} + +func NewResourcePipelineCriteria() resource.Resource { + return &ResourcePipelineCriteria{} +} + +// ResourcePipelineCriteria defines the resource implementation. +type ResourcePipelineCriteria struct { + client *humanitec.Client + orgID string +} + +func (r *ResourcePipelineCriteria) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_pipeline_criteria" +} + +func (r *ResourcePipelineCriteria) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Pipeline criteria link Pipelines to applicable triggers in the application. The only +supported trigger type today is "deployment_request" which specifies that the Pipeline should be used for deployments +in any environment which matches the criteria. +`, + Attributes: map[string]schema.Attribute{ + "app_id": schema.StringAttribute{ + MarkdownDescription: "The id of the Application containing the Pipeline.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "pipeline_id": schema.StringAttribute{ + MarkdownDescription: "The id of the Pipeline.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "pipeline_name": schema.StringAttribute{ + MarkdownDescription: "The name of the Pipeline.", + Computed: true, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "The id of the Pipeline Criteria.", + Computed: true, + }, + "deployment_request": schema.SingleNestedAttribute{ + MarkdownDescription: "The criteria required to match a deployment request.", + Required: true, + Attributes: map[string]schema.Attribute{ + "app_id": schema.StringAttribute{ + MarkdownDescription: "The Application id for this criteria to match.", + Computed: true, + }, + "env_type": schema.StringAttribute{ + MarkdownDescription: "The environment type for this criteria to match.", + Optional: true, + Computed: true, + }, + "env_id": schema.StringAttribute{ + MarkdownDescription: "The environment id for this criteria to match.", + Optional: true, + Computed: true, + }, + "deployment_type": schema.StringAttribute{ + MarkdownDescription: "The deployment type for this criteria to match ('deploy' or 're-deploy').", + Optional: true, + Computed: true, + }, + }, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *ResourcePipelineCriteria) 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 +} + +type pipelineCriteriaDeploymentRequestModel struct { + EnvType types.String `tfsdk:"env_type"` + AppID types.String `tfsdk:"app_id"` + EnvId types.String `tfsdk:"env_id"` + DeploymentType types.String `tfsdk:"deployment_type"` +} + +// pipelineCriteriaModel is used to deserialize the plan or state in order to access its attributes +type pipelineCriteriaModel struct { + AppID types.String `tfsdk:"app_id"` + PipelineId types.String `tfsdk:"pipeline_id"` + PipelineName types.String `tfsdk:"pipeline_name"` + Id types.String `tfsdk:"id"` + DeploymentRequest *pipelineCriteriaDeploymentRequestModel `tfsdk:"deployment_request"` +} + +func (pcm *pipelineCriteriaModel) updateFromContent(res *client.PipelineCriteria) diag.Diagnostics { + totalDiags := diag.Diagnostics{} + drc, err := res.AsPipelineDeploymentRequestCriteria() + if err != nil { + totalDiags.AddError(HUM_PROVIDER_ERR, "provider does not support trigger type "+res.Trigger) + return totalDiags + } + pcm.Id = types.StringValue(drc.Id) + pcm.AppID = types.StringPointerValue(drc.AppId) + pcm.PipelineId = types.StringValue(drc.PipelineId) + pcm.PipelineName = types.StringValue(drc.PipelineName) + pcm.DeploymentRequest = &pipelineCriteriaDeploymentRequestModel{ + EnvType: types.StringPointerValue(drc.EnvType), + AppID: types.StringPointerValue(drc.AppId), + EnvId: types.StringPointerValue(drc.EnvId), + DeploymentType: types.StringPointerValue(drc.DeploymentType), + } + return totalDiags +} + +func (r *ResourcePipelineCriteria) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *pipelineCriteriaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + requestBody := client.CreatePipelineCriteriaJSONRequestBody{} + request := client.PipelineDeploymentRequestCriteriaCreateBody{ + AppId: data.AppID.ValueStringPointer(), + } + if v := data.DeploymentRequest.EnvType.ValueString(); v != "" { + request.EnvType = &v + } + if v := data.DeploymentRequest.EnvId.ValueString(); v != "" { + request.EnvId = &v + } + if v := data.DeploymentRequest.DeploymentType.ValueString(); v != "" { + request.DeploymentType = &v + } + _ = requestBody.FromPipelineDeploymentRequestCriteriaCreateBody(request) + clientResp, err := r.client.CreatePipelineCriteriaWithResponse(ctx, r.orgID, data.AppID.ValueString(), data.PipelineId.ValueString(), requestBody) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create pipeline criteria, got error: %s", err)) + return + } + switch clientResp.StatusCode() { + case http.StatusCreated: + diags := data.updateFromContent(clientResp.JSON201) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + case http.StatusBadRequest: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create pipeline criteria, Humanitec returned bad request: %s", clientResp.Body)) + return + case http.StatusNotFound: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create pipeline criteria, organization or application not found: %s", clientResp.Body)) + return + case http.StatusConflict: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create pipeline criteria due to a conflicts: %s", clientResp.Body)) + return + default: + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Received unexpected status code when creating pipeline criteria: %d, body: %s", clientResp.StatusCode(), clientResp.Body)) + return + } +} + +func (r *ResourcePipelineCriteria) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Read Terraform prior state data into the model + var data *pipelineCriteriaModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + clientResp, err := r.client.GetPipelineCriteriaWithResponse(ctx, r.orgID, data.AppID.ValueString(), data.PipelineId.ValueString(), data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to get pipeline criteria, got error: %s", err)) + return + } + switch clientResp.StatusCode() { + case http.StatusOK: + diags := data.updateFromContent(clientResp.JSON200) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + case http.StatusNotFound: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to get pipeline criteria, organization or application not found: %s", clientResp.Body)) + return + default: + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Received unexpected status code when reading pipeline criteria: %d, body: %s", clientResp.StatusCode(), clientResp.Body)) + return + } +} + +func (r *ResourcePipelineCriteria) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // you can't update criteria in place, all updates should be done with a replacement + resp.Diagnostics.AddError(HUM_CLIENT_ERR, "Unable to update pipeline criteria") + return +} + +func (r *ResourcePipelineCriteria) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *pipelineCriteriaModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + clientResp, err := r.client.DeletePipelineCriteriaWithResponse(ctx, r.orgID, data.AppID.ValueString(), data.PipelineId.ValueString(), data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete pipeline criteria, got error: %s", err)) + return + } + switch clientResp.StatusCode() { + case http.StatusNoContent: + return + case http.StatusBadRequest: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete pipeline criteria, Humanitec returned bad request: %s", clientResp.Body)) + return + case http.StatusNotFound: + resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete missing pipeline criteria: %s", clientResp.Body)) + return + default: + resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Received unexpected status code when deleting pipeline criteria: %d, body: %s", clientResp.StatusCode(), clientResp.Body)) + return + } +} + +func (r *ResourcePipelineCriteria) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, "/") + if len(idParts) != 3 { + resp.Diagnostics.AddError("Unexpected Import Identifier", "expected a 3 part import id like //") + return + } + appId, pipelineId, criteriaId := idParts[0], idParts[1], idParts[2] + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_id"), appId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("pipeline_id"), pipelineId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), criteriaId)...) + return +} diff --git a/internal/provider/resource_pipeline_criteria_test.go b/internal/provider/resource_pipeline_criteria_test.go new file mode 100644 index 0000000..276f2a8 --- /dev/null +++ b/internal/provider/resource_pipeline_criteria_test.go @@ -0,0 +1,118 @@ +package provider + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestResourcePipelineCriteria(t *testing.T) { + // avoid conflict by giving apps a unique id + testUid := int(time.Now().UnixMilli()) + + // base config contains the "static" bits for this test that don't change + baseConfig := fmt.Sprintf(` +resource humanitec_application "app" { + id = "app%[1]d" + name = "App %[1]d" +} + +resource humanitec_pipeline "pip" { + app_id = humanitec_application.app.id + definition = <