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 16, 2024
1 parent 490929b commit ae86b03
Show file tree
Hide file tree
Showing 11 changed files with 445 additions and 6 deletions.
44 changes: 44 additions & 0 deletions docs/resources/environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "humanitec_environment Resource - terraform-provider-humanitec"
subcategory: ""
description: |-
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).
---

# humanitec_environment (Resource)

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).

## Example Usage

```terraform
resource "humanitec_environment" "example" {
app_id = "example-app"
id = "example"
name = "An example environment"
type = "development"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `app_id` (String) The Application ID.
- `id` (String) The ID the Environment is referenced as.
- `name` (String) The Human-friendly name for the Environment.
- `type` (String) The Environment Type. This is used for organizing and managing Environments.

### Optional

- `from_deploy_id` (String) Defines the existing Deployment the new Environment will be based on.

## Import

Import is supported using the following syntax:

```shell
terraform import humanitec_environment.example application_id/environment_id
```
2 changes: 1 addition & 1 deletion docs/resources/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ An entity or individual who has access to the Humanitec platform.

- `name` (String) The name the user goes by.
- `role` (String) The role that the service user should have on the organization it is created in. Could be `member`, `artefactContributor`, `manager` or `administrator`.
- `type` (String) The type of the account. Could be `user`, `service` or `system`.
- `type` (String) The type of the account. Only users type `service` can be managed by Terraform.

### Optional

Expand Down
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 environment"
type = "development"
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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-20240429100802-283cee98d746
github.com/humanitec/humanitec-go-autogen v0.0.0-20240516095603-93b078e55cd9
github.com/stretchr/testify v1.9.0
sigs.k8s.io/yaml v1.4.0
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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-20240429100802-283cee98d746 h1:im9qs2bH2xPz8zoCC/yUUrXqhN5wDVv/TbFgQZZD8MQ=
github.com/humanitec/humanitec-go-autogen v0.0.0-20240429100802-283cee98d746/go.mod h1:WqItJ/MhAHcjP7LIhIt2/NrgXeXRbLuxvXlin7qY0j4=
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/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=
Expand Down
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
304 changes: 304 additions & 0 deletions internal/provider/resource_environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
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.",
Optional: true,
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.ValueStringPointer(),
})
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) {
var fromDeployId *string
if res.FromDeploy != nil {
fromDeployId = &res.FromDeploy.Id
}

data.FromDeployID = types.StringPointerValue(fromDeployId)
data.AppID = types.StringValue(appID)
data.ID = types.StringValue(res.Id)
data.Name = types.StringValue(res.Name)
data.Type = types.StringValue(res.Type)
}
Loading

0 comments on commit ae86b03

Please sign in to comment.