Skip to content

Commit

Permalink
feat: add environment resource
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuszjenek committed May 15, 2024
1 parent 490929b commit 58a28a4
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 1 deletion.
1 change: 1 addition & 0 deletions examples/resources/humanitec_environment/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import humanitec_environment.example application_id/environment_id
6 changes: 6 additions & 0 deletions examples/resources/humanitec_environment/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resource "humanitec_environment" "example" {
app_id = "example-app"
id = "example"
name = "An example app"
type = "development"
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ func (p *HumanitecProvider) Resources(ctx context.Context) []func() resource.Res
NewResourceArtefactVersion,
NewResourceDefinitionCriteriaResource,
NewResourceDefinitionResource,
NewResourceEnvironment,
NewResourceEnvironmentType,
NewResourceEnvironmentTypeUser,
NewResourceKey,
Expand Down
298 changes: 298 additions & 0 deletions internal/provider/resource_environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
package provider

import (
"context"
"fmt"
"net/http"
"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 = &ResourceEnvironment{}
var _ resource.ResourceWithImportState = &ResourceEnvironment{}

func NewResourceEnvironment() resource.Resource {
return &ResourceEnvironment{}
}

// ResourceEnvironment defines the resource implementation.
type ResourceEnvironment struct {
client *humanitec.Client
orgID string
}

type EnvironmentModel struct {
AppID types.String `tfsdk:"app_id"`
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Type types.String `tfsdk:"type"`
FromDeployID types.String `tfsdk:"from_deploy_id"`
}

func (r *ResourceEnvironment) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_environment"
}

func (r *ResourceEnvironment) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "An Environment is a space where an instance of an Application can be deployed. Environments consist of a Kubernetes namespace and any shared Resources (as configured by relevant Matching Rules).",

Attributes: map[string]schema.Attribute{
"app_id": schema.StringAttribute{
MarkdownDescription: "The Application ID.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"id": schema.StringAttribute{
MarkdownDescription: "The ID the Environment is referenced as.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"name": schema.StringAttribute{
MarkdownDescription: "The Human-friendly name for the Environment.",
Required: true,
},
"type": schema.StringAttribute{
MarkdownDescription: "The Environment Type. This is used for organizing and managing Environments.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"from_deploy_id": schema.StringAttribute{
MarkdownDescription: "Defines the existing Deployment the new Environment will be based on.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}

func (r *ResourceEnvironment) 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 *ResourceEnvironment) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *EnvironmentModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

appID := data.AppID.ValueString()

var environment *client.EnvironmentResponse
createEnvironmentResp, err := r.client.CreateEnvironmentWithResponse(ctx, r.orgID, appID, client.EnvironmentDefinitionRequest{
Id: data.ID.ValueString(),
Name: data.Name.ValueString(),
Type: data.Type.ValueStringPointer(),
FromDeployId: data.FromDeployID.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create environment, got error: %s", err))
return
}
switch createEnvironmentResp.StatusCode() {
case http.StatusCreated:
environment = createEnvironmentResp.JSON201
case http.StatusBadRequest:
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create environment, Humanitec returned bad request: %s", createEnvironmentResp.Body))
return
case http.StatusNotFound:
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to create environment, environment not found: %s", createEnvironmentResp.Body))
return
default:
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to create environment unexpected status code: %d, body: %s", createEnvironmentResp.StatusCode(), createEnvironmentResp.Body))
return
}

parseEnvironmentResponse(appID, environment, data)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ResourceEnvironment) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *EnvironmentModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

appID := data.AppID.ValueString()
id := data.ID.ValueString()

var environment *client.EnvironmentResponse
getEnvironmentResp, err := r.client.GetEnvironmentWithResponse(ctx, r.orgID, appID, id)
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to get environment, got error: %s", err))
return
}
switch getEnvironmentResp.StatusCode() {
case http.StatusOK:
environment = getEnvironmentResp.JSON200
case http.StatusNotFound:
resp.Diagnostics.AddWarning("Environment not found", fmt.Sprintf("The environment (%s) was deleted outside Terraform", id))
resp.State.RemoveResource(ctx)
return
default:
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to get environment, unexpected status code: %d, body: %s", getEnvironmentResp.StatusCode(), getEnvironmentResp.Body))
return
}

parseEnvironmentResponse(appID, environment, data)

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ResourceEnvironment) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data, state *EnvironmentModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

appID := state.AppID.ValueString()
id := state.ID.ValueString()

var environment *client.EnvironmentResponse
updateEnvironmentResp, err := r.client.UpdateEnvironmentWithResponse(ctx, r.orgID, appID, id, client.UpdateEnvironmentJSONRequestBody{
Name: data.Name.ValueStringPointer(),
})
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update environment, got error: %s", err))
return
}
switch updateEnvironmentResp.StatusCode() {
case http.StatusOK:
environment = updateEnvironmentResp.JSON200
case http.StatusBadRequest:
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update environment, Humanitec returned bad request: %s", updateEnvironmentResp.Body))
return
case http.StatusNotFound:
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update environment, environment not found: %s", updateEnvironmentResp.Body))
return
case http.StatusPreconditionFailed:
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to update environment, the state of Terraform resource do not match resource in Humanitec: %s", updateEnvironmentResp.Body))
return
default:
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to update environment, unexpected status code: %d, body: %s", updateEnvironmentResp.StatusCode(), updateEnvironmentResp.Body))
return
}

parseEnvironmentResponse(appID, environment, data)

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ResourceEnvironment) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *EnvironmentModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

appID := data.AppID.ValueString()
id := data.ID.ValueString()

deleteEnvironmentResp, err := r.client.DeleteEnvironmentWithResponse(ctx, r.orgID, appID, id)
if err != nil {
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete environment, got error: %s", err))
return
}
switch deleteEnvironmentResp.StatusCode() {
case http.StatusNoContent, http.StatusAccepted:
// Do nothing
case http.StatusNotFound:
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete environment, environment not found: %s", deleteEnvironmentResp.Body))
return
case http.StatusPreconditionFailed:
resp.Diagnostics.AddError(HUM_CLIENT_ERR, fmt.Sprintf("Unable to delete environment, the state of Terraform resource do not match resource in Humanitec: %s", deleteEnvironmentResp.Body))
return
default:
resp.Diagnostics.AddError(HUM_API_ERR, fmt.Sprintf("Unable to delete environment, unexpected status code: %d, body: %s", deleteEnvironmentResp.StatusCode(), deleteEnvironmentResp.Body))
return
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ResourceEnvironment) 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: app_id/env_id. Got: %q", req.ID),
)
return
}
}

if len(idParts) == 2 {
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_id"), 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: app_id/env_id. Got: %q", req.ID),
)
return
}
}

func parseEnvironmentResponse(appID string, res *client.EnvironmentResponse, data *EnvironmentModel) {
data.AppID = types.StringValue(appID)
data.FromDeployID = types.StringValue(res.FromDeploy.Id)
data.ID = types.StringValue(res.Id)
data.Name = types.StringValue(res.Name)
data.Type = types.StringValue(res.Type)
}
Loading

0 comments on commit 58a28a4

Please sign in to comment.