Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add environment resource #86

Merged
merged 1 commit into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
301 changes: 301 additions & 0 deletions internal/provider/resource_environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
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
}
}

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
Loading