🚛 load balancer resource (#17)

* 🚛 load balancer resource

* Fix whitespace in server resource
This commit is contained in:
Oscar Hermoso
2024-09-16 15:55:01 +08:00
committed by GitHub
parent 600e3f56e7
commit 44772aa55d
22 changed files with 1795 additions and 100 deletions
+39
View File
@@ -0,0 +1,39 @@
name: Terraform Provider Tests
on:
pull_request:
paths:
- '.github/workflows/test.yaml'
- '**.go'
permissions:
# Permission for checking out code
contents: read
jobs:
acceptance:
name: Acceptance Tests
runs-on: ubuntu-latest
environment: test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version-file: 'go.mod'
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: '1.5.*'
terraform_wrapper: false
- run: go test -v -cover ./...
env:
TF_ACC: '1'
BINARYLANE_API_TOKEN: ${{ secrets.BINARYLANE_API_TOKEN }}
# unit:
# name: Unit Tests
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - uses: actions/setup-go@v4
# with:
# go-version-file: 'go.mod'
# - run: go test -v -cover ./...
+16 -3
View File
@@ -131,7 +131,7 @@ Add a `Configure` method to the resource:
+ }
```
Add the new data source/resource to `provider.go`:
Add the new resource to `provider.go`:
```diff
return []func() resource.Resource{
@@ -204,12 +204,25 @@ Use the generated schema to define the data source schema.
- },
- },
- }
+ ds, err := convertResourceSchemaToDataSourceSchema(resources.ExampleResourceSchema(ctx))
+ ds, err := convertResourceSchemaToDataSourceSchema(
+ resources.ExampleResourceSchema(ctx)
+ AttributeConfig{
+ RequiredAttributes: &[]string{"id"},
+ },
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("Failed to convert resource schema to data source schema", err.Error())
+ return
+ }
+ resp.Schema = *ds
+ resp.Schema.Description = "TODO"
+ // resp.Schema.Description = "TODO"
}
```
Add the data source to `provider.go`:
```diff
return []func() datasource.DataSource{
# ...
+ NewExampleDataSource,
}
+1 -1
View File
@@ -24,8 +24,8 @@ Planned features:
- [ ] Alerts
- [x] SSH Keys
- [x] Virtual Private Cloud
- [x] Load Balancers
- [ ] Images
- [ ] Load Balancers
- [ ] DNS
- [x] Docs
- [x] Generated docs
+44
View File
@@ -0,0 +1,44 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_load_balancer Data Source - terraform-provider-binarylane"
subcategory: ""
description: |-
---
# binarylane_load_balancer (Data Source)
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `id` (Number) The ID of the load balancer to fetch.
### Read-Only
- `forwarding_rules` (List of Object) The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default "HTTP" only forwarding rule. (see [below for nested schema](#nestedatt--forwarding_rules))
- `health_check` (Object) The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules. (see [below for nested schema](#nestedatt--health_check))
- `name` (String) The hostname of the load balancer.
- `region` (String) Leave null to create an anycast load balancer.
- `server_ids` (List of Number) A list of server IDs to assign to this load balancer.
<a id="nestedatt--forwarding_rules"></a>
### Nested Schema for `forwarding_rules`
Read-Only:
- `entry_protocol` (String)
<a id="nestedatt--health_check"></a>
### Nested Schema for `health_check`
Read-Only:
- `path` (String)
- `protocol` (String)
+47
View File
@@ -0,0 +1,47 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_load_balancer Resource - terraform-provider-binarylane"
subcategory: ""
description: |-
---
# binarylane_load_balancer (Resource)
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The hostname of the load balancer.
### Optional
- `forwarding_rules` (Attributes List) The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default "HTTP" only forwarding rule. (see [below for nested schema](#nestedatt--forwarding_rules))
- `health_check` (Attributes) The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules. (see [below for nested schema](#nestedatt--health_check))
- `region` (String) Leave null to create an anycast load balancer.
- `server_ids` (List of Number) A list of server IDs to assign to this load balancer.
### Read-Only
- `id` (Number) The ID of the load balancer to fetch.
<a id="nestedatt--forwarding_rules"></a>
### Nested Schema for `forwarding_rules`
Required:
- `entry_protocol` (String) The protocol that traffic must match for this load balancer to forward traffic according to this rule.
<a id="nestedatt--health_check"></a>
### Nested Schema for `health_check`
Optional:
- `path` (String) Leave null to accept the default '/' path.
- `protocol` (String) Leave null to accept the default HTTP protocol.
+8 -1
View File
@@ -2,7 +2,14 @@
This WIP example shows how to create a Kubernetes cluster on Binary Lane.
Prior art:
## Prior art
- [MartinHodges/create_k8s](https://github.com/MartinHodges/create_k8s) repo and the [medium article](https://medium.com/@martin.hodges/creating-a-kubernetes-cluster-from-scratch-in-1-hour-using-automation-a25e387be547) that goes with it.
- https://github.com/inscapist/terraform-k3s-private-cloud
- https://github.com/schnerring/schnerring.github.io/blob/07d06fb40e3d01f2483a739f516ac4d711a5742c/content/blog/use-terraform-to-deploy-an-azure-kubernetes-service-aks-cluster-traefik-2-cert-manager-and-lets-encrypt-certificates/index.md
## Remaining TODOs
- [ ] Consider rewriting module to use `microk8s` instead of `k3s`
- [ ] Use `kustomize` instead of `helm`
- [ ] Use the `binarylane_load_balancer` resource instead of `kubectl port-forward` (or use the load balancer in another example)
+1 -1
View File
@@ -107,7 +107,7 @@ require (
golang.org/x/tools v0.22.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.66.0 // indirect
google.golang.org/grpc v1.66.2 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+2 -2
View File
@@ -395,8 +395,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+6 -1
View File
@@ -21,7 +21,7 @@ cat <<<$(jq '.paths["/account/keys/{key_id}"].delete.parameters[0].schema |= del
# Remove the "/paths/{image_id}" path because its duplicated by "/images/{image_id_or_slug}"
cat <<<$(jq 'del(.paths."/images/{image_id}")' $OPENAPI_FILE) >$OPENAPI_FILE
# Add x-oapi-codegen-extra-tags so Go structs can be reflected
# Add x-oapi-codegen-extra-tags so structs can be reflected
## RouteEntryRequest
cat <<<$(jq '.components.schemas.RouteEntryRequest.properties.destination += {"x-oapi-codegen-extra-tags": {"tfsdk": "destination"}}' $OPENAPI_FILE) >$OPENAPI_FILE
@@ -35,3 +35,8 @@ cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.action += {"x-o
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.destination_addresses += {"x-oapi-codegen-extra-tags": {"tfsdk": "destination_addresses"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.destination_ports += {"x-oapi-codegen-extra-tags": {"tfsdk": "destination_ports"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.source_addresses += {"x-oapi-codegen-extra-tags": {"tfsdk": "source_addresses"}}' $OPENAPI_FILE) >$OPENAPI_FILE
## Load Balancer
cat <<<$(jq '.components.schemas.CreateLoadBalancerRequest.properties.forwarding_rules += {"x-oapi-codegen-extra-tags": {"tfsdk": "forwarding_rules"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.ForwardingRule.properties.entry_protocol += {"x-oapi-codegen-extra-tags": {"tfsdk": "entry_protocol"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.HealthCheckProtocol |= del(.enum)' $OPENAPI_FILE) >$OPENAPI_FILE
+8 -7
View File
@@ -10057,7 +10057,10 @@
"$ref": "#/components/schemas/ForwardingRule"
},
"description": "The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default \"HTTP\" only forwarding rule.",
"nullable": true
"nullable": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "forwarding_rules"
}
},
"health_check": {
"allOf": [
@@ -10998,7 +11001,10 @@
"$ref": "#/components/schemas/LoadBalancerRuleProtocol"
}
],
"description": "The protocol that traffic must match for this load balancer to forward traffic according to this rule."
"description": "The protocol that traffic must match for this load balancer to forward traffic according to this rule.",
"x-oapi-codegen-extra-tags": {
"tfsdk": "entry_protocol"
}
}
}
},
@@ -11038,11 +11044,6 @@
}
},
"HealthCheckProtocol": {
"enum": [
"http",
"https",
"both"
],
"type": "string",
"description": "\n| Value | Description |\n| ----- | ----------- |\n| http | The health check will be performed via HTTP. |\n| https | The health check will be performed via HTTPS. |\n| both | The health check will be performed via both HTTP and HTTPS. Failing a health check on one protocol will remove the server from the pool of servers only for that protocol. |\n\n",
"x-enum-descriptions": [
+5 -12
View File
@@ -230,13 +230,6 @@ const (
EnableIpv6TypeEnableIpv6 EnableIpv6Type = "enable_ipv6"
)
// Defines values for HealthCheckProtocol.
const (
HealthCheckProtocolBoth HealthCheckProtocol = "both"
HealthCheckProtocolHttp HealthCheckProtocol = "http"
HealthCheckProtocolHttps HealthCheckProtocol = "https"
)
// Defines values for ImageQueryType.
const (
ImageQueryTypeBackup ImageQueryType = "backup"
@@ -265,8 +258,8 @@ const (
// Defines values for LoadBalancerRuleProtocol.
const (
LoadBalancerRuleProtocolHttp LoadBalancerRuleProtocol = "http"
LoadBalancerRuleProtocolHttps LoadBalancerRuleProtocol = "https"
Http LoadBalancerRuleProtocol = "http"
Https LoadBalancerRuleProtocol = "https"
)
// Defines values for LoadBalancerStatus.
@@ -1131,7 +1124,7 @@ type ConsoleResponse struct {
// CreateLoadBalancerRequest defines model for CreateLoadBalancerRequest.
type CreateLoadBalancerRequest struct {
// ForwardingRules The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default "HTTP" only forwarding rule.
ForwardingRules *[]ForwardingRule `json:"forwarding_rules"`
ForwardingRules *[]ForwardingRule `json:"forwarding_rules" tfsdk:"forwarding_rules"`
// HealthCheck The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules.
HealthCheck *HealthCheck `json:"health_check"`
@@ -1538,7 +1531,7 @@ type FailoverIpsResponse struct {
// ForwardingRule defines model for ForwardingRule.
type ForwardingRule struct {
// EntryProtocol The protocol that traffic must match for this load balancer to forward traffic according to this rule.
EntryProtocol LoadBalancerRuleProtocol `json:"entry_protocol"`
EntryProtocol LoadBalancerRuleProtocol `json:"entry_protocol" tfsdk:"entry_protocol"`
}
// ForwardingRulesRequest defines model for ForwardingRulesRequest.
@@ -1562,7 +1555,7 @@ type HealthCheck struct {
// | http | The health check will be performed via HTTP. |
// | https | The health check will be performed via HTTPS. |
// | both | The health check will be performed via both HTTP and HTTPS. Failing a health check on one protocol will remove the server from the pool of servers only for that protocol. |
type HealthCheckProtocol string
type HealthCheckProtocol = string
// Host defines model for Host.
type Host struct {
@@ -0,0 +1,104 @@
package provider
import (
"context"
"fmt"
"net/http"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework/datasource"
)
var (
_ datasource.DataSource = &loadBalancerDataSource{}
_ datasource.DataSourceWithConfigure = &loadBalancerDataSource{}
)
func NewLoadBalancerDataSource() datasource.DataSource {
return &loadBalancerDataSource{}
}
type loadBalancerDataSource struct {
bc *BinarylaneClient
}
type loadBalancerDataSourceModel struct {
resources.LoadBalancerModel
}
func (d *loadBalancerDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_load_balancer"
}
func (d *loadBalancerDataSource) Configure(
_ context.Context,
req datasource.ConfigureRequest,
resp *datasource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
bc, ok := req.ProviderData.(BinarylaneClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData))
return
}
d.bc = &bc
}
func (d *loadBalancerDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
ds, err := convertResourceSchemaToDataSourceSchema(
resources.LoadBalancerResourceSchema(ctx),
AttributeConfig{
RequiredAttributes: &[]string{"id"},
},
)
if err != nil {
resp.Diagnostics.AddError("Failed to convert resource schema to data source schema", err.Error())
return
}
resp.Schema = *ds
// resp.Schema.Description = "TODO"
}
func (d *loadBalancerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data loadBalancerDataSourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read API call logic
lbResp, err := d.bc.client.GetLoadBalancersLoadBalancerIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading load balancer",
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body),
)
return
}
diags := SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
+389
View File
@@ -0,0 +1,389 @@
package provider
import (
"context"
"fmt"
"net/http"
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework/attr"
"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/int64planmodifier"
"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/hashicorp/terraform-plugin-log/tflog"
)
var (
_ resource.Resource = &loadBalancerResource{}
_ resource.ResourceWithConfigure = &loadBalancerResource{}
_ resource.ResourceWithImportState = &loadBalancerResource{}
)
func NewLoadBalancerResource() resource.Resource {
return &loadBalancerResource{}
}
type loadBalancerResource struct {
bc *BinarylaneClient
}
type loadBalancerResourceModel struct {
resources.LoadBalancerModel
}
func (r *loadBalancerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_load_balancer"
}
func (d *loadBalancerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
bc, ok := req.ProviderData.(BinarylaneClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData),
)
return
}
d.bc = &bc
}
func (r *loadBalancerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resources.LoadBalancerResourceSchema(ctx)
// resp.Schema.Description = "TODO"
// Overrides
id := resp.Schema.Attributes["id"]
resp.Schema.Attributes["id"] = &schema.Int64Attribute{
Description: id.GetDescription(),
MarkdownDescription: id.GetMarkdownDescription(),
// read only
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
}
region := resp.Schema.Attributes["region"]
resp.Schema.Attributes["region"] = &schema.StringAttribute{
Description: region.GetDescription(),
MarkdownDescription: region.GetMarkdownDescription(),
Optional: true,
Computed: false, // region is not computed, defined at creation
PlanModifiers: []planmodifier.String{ // region is not allowed to be changed
stringplanmodifier.RequiresReplace(),
},
}
}
func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data loadBalancerResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Create API call logic
forwardingRules := []binarylane.ForwardingRule{}
diags := data.LoadBalancerModel.ForwardingRules.ElementsAs(ctx, &forwardingRules, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
serverIds := []int64{}
diags = data.ServerIds.ElementsAs(ctx, &serverIds, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
body := binarylane.CreateLoadBalancerRequest{
Name: data.Name.ValueString(),
ForwardingRules: &forwardingRules,
ServerIds: &serverIds,
}
if data.HealthCheck.Path.ValueStringPointer() != nil || data.HealthCheck.Protocol.ValueStringPointer() != nil {
body.HealthCheck = &binarylane.HealthCheck{
Path: data.HealthCheck.Path.ValueStringPointer(),
Protocol: data.HealthCheck.Protocol.ValueStringPointer(),
}
}
tflog.Info(ctx, fmt.Sprintf("Creating Load Balancer: name=%s", data.Name.ValueString()))
lbResp, err := r.bc.client.PostLoadBalancersWithResponse(ctx, body)
if err != nil {
tflog.Info(ctx, fmt.Sprintf("Attempted to create new load balancer: request=%+v", body))
resp.Diagnostics.AddError(
fmt.Sprintf("Error sending request to create load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusOK {
tflog.Info(ctx, fmt.Sprintf("Attempted to create new load balancer: request=%+v", body))
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating load balancer",
fmt.Sprintf("Received %s creating new load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body))
return
}
diags = SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data loadBalancerResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read API call logic
lbResp, err := r.bc.client.GetLoadBalancersLoadBalancerIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading load balancer",
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body),
)
return
}
diags := SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data loadBalancerResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Update API call logic
forwardingRules := &[]binarylane.ForwardingRule{}
diags := data.LoadBalancerModel.ForwardingRules.ElementsAs(ctx, forwardingRules, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
serverIds := &[]int64{}
diags = data.ServerIds.ElementsAs(ctx, serverIds, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, fmt.Sprintf("Updating Load Balancer: name=%s", data.Name.ValueString()))
body := binarylane.UpdateLoadBalancerRequest{
Name: data.Name.ValueString(),
ForwardingRules: forwardingRules,
HealthCheck: &binarylane.HealthCheck{
Path: data.HealthCheck.Path.ValueStringPointer(),
Protocol: data.HealthCheck.Protocol.ValueStringPointer(),
},
ServerIds: serverIds,
}
lbResp, err := r.bc.client.PutLoadBalancersLoadBalancerIdWithResponse(ctx, data.Id.ValueInt64(), body)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading load balancer",
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body),
)
return
}
diags = SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *loadBalancerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data loadBalancerResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Delete API call logic
tflog.Debug(ctx, fmt.Sprintf("Deleting Load Balancer: name=%s", data.Name.ValueString()))
lbResp, err := r.bc.client.DeleteLoadBalancersLoadBalancerIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error deleting load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusNoContent {
resp.Diagnostics.AddError(
"Unexpected HTTP status code deleting load balancer",
fmt.Sprintf("Received %s deleting load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body))
return
}
}
func (r *loadBalancerResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
// Import by ID
id, err := strconv.ParseInt(req.ID, 10, 32)
if err == nil {
diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(id))
resp.Diagnostics.Append(diags...)
return
}
// Import by name
var page int32 = 1
perPage := int32(200)
var loadBalancer binarylane.LoadBalancer
var nextPage bool = true
for nextPage { // Need to paginate because the API does not support filtering by name
params := binarylane.GetLoadBalancersParams{
Page: &page,
PerPage: &perPage,
}
lbResp, err := r.bc.client.GetLoadBalancersWithResponse(ctx, &params)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Error getting load balancer for import: name=%s", req.ID), err.Error())
return
}
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code getting load balancer for import",
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), req.ID,
lbResp.Body))
return
}
loadBalancers := *lbResp.JSON200.LoadBalancers
for _, lb := range loadBalancers {
if *lb.Name == req.ID {
loadBalancer = lb
nextPage = false
break
}
}
if lbResp.JSON200.Links == nil || lbResp.JSON200.Links.Pages == nil || lbResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
page++
}
if loadBalancer.Id == nil {
resp.Diagnostics.AddError(
"Could not find load balancer by name",
fmt.Sprintf("Error finding load balancer: name=%s", req.ID),
)
return
}
diags := resp.State.SetAttribute(ctx, path.Root("id"), *loadBalancer.Id)
resp.Diagnostics.Append(diags...)
}
func SetLoadBalancerModelState(ctx context.Context, data *resources.LoadBalancerModel, lb *binarylane.LoadBalancer) diag.Diagnostics {
var diags, diag diag.Diagnostics
data.Id = types.Int64Value(*lb.Id)
data.Name = types.StringValue(*lb.Name)
if lb.Region == nil {
data.Region = types.StringNull()
} else {
data.Region = types.StringValue(*lb.Region.Slug)
}
data.ServerIds, diags = types.ListValueFrom(ctx, types.Int64Type, lb.ServerIds)
if lb.HealthCheck == nil {
data.HealthCheck = resources.NewHealthCheckValueNull()
} else {
data.HealthCheck, diag = resources.NewHealthCheckValue(
resources.HealthCheckValue{}.AttributeTypes(ctx),
map[string]attr.Value{
"path": types.StringValue(*lb.HealthCheck.Path),
"protocol": types.StringValue(*lb.HealthCheck.Protocol),
},
)
diags.Append(diag...)
}
data.ForwardingRules, diag = types.ListValueFrom(ctx,
types.ObjectType{AttrTypes: resources.ForwardingRulesValue{}.AttributeTypes(ctx)},
lb.ForwardingRules,
)
diags.Append(diag...)
return diags
}
@@ -0,0 +1,86 @@
package provider
import (
"crypto/rand"
"encoding/base64"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestLoadBalancerResource(t *testing.T) {
// Must assign a password to the server or Binary Lane will send emails
pw_bytes := make([]byte, 12)
_, err := rand.Read(pw_bytes)
if err != nil {
t.Errorf("Failed to generate password: %s", err)
return
}
password := base64.URLEncoding.EncodeToString(pw_bytes)
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: providerConfig + `
resource "binarylane_server" "test" {
count = 2
name = "tf-test-lb-server-${count.index}"
region = "per"
image = "debian-12"
size = "std-min"
password = "` + password + `"
wait_for_create = 60
}
resource "binarylane_load_balancer" "test" {
name = "tf-test-lb"
server_ids = [binarylane_server.test.0.id, binarylane_server.test.1.id]
forwarding_rules = [{ entry_protocol = "http" }]
}
data "binarylane_load_balancer" "test" {
depends_on = [binarylane_load_balancer.test]
id = binarylane_load_balancer.test.id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify resource values
resource.TestCheckResourceAttrSet("binarylane_load_balancer.test", "id"),
resource.TestCheckResourceAttr("binarylane_load_balancer.test", "name", "tf-test-lb"),
resource.TestCheckNoResourceAttr("binarylane_load_balancer.test", "region"),
resource.TestCheckResourceAttr("binarylane_load_balancer.test", "server_ids.#", "2"),
resource.TestCheckResourceAttrPair("binarylane_load_balancer.test", "server_ids.0", "binarylane_server.test.0", "id"),
resource.TestCheckResourceAttrPair("binarylane_load_balancer.test", "server_ids.1", "binarylane_server.test.1", "id"),
resource.TestCheckResourceAttr("binarylane_load_balancer.test", "forwarding_rules.#", "1"),
resource.TestCheckResourceAttr("binarylane_load_balancer.test", "forwarding_rules.0.entry_protocol", "http"),
// Verify data source values
resource.TestCheckResourceAttrPair("data.binarylane_load_balancer.test", "id", "binarylane_load_balancer.test", "id"),
resource.TestCheckResourceAttr("data.binarylane_load_balancer.test", "name", "tf-test-lb"),
resource.TestCheckNoResourceAttr("data.binarylane_load_balancer.test", "region"),
resource.TestCheckResourceAttr("data.binarylane_load_balancer.test", "server_ids.#", "2"),
resource.TestCheckResourceAttrPair("data.binarylane_load_balancer.test", "server_ids.0", "binarylane_server.test.0", "id"),
resource.TestCheckResourceAttrPair("data.binarylane_load_balancer.test", "server_ids.1", "binarylane_server.test.1", "id"),
resource.TestCheckResourceAttr("data.binarylane_load_balancer.test", "forwarding_rules.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_load_balancer.test", "forwarding_rules.0.entry_protocol", "http"),
),
},
// Test import by ID
{
ResourceName: "binarylane_load_balancer.test",
ImportState: true,
ImportStateVerify: true,
},
// Test import by name
{
ResourceName: "binarylane_load_balancer.test",
ImportState: true,
ImportStateId: "tf-test-lb",
ImportStateVerify: true,
},
},
})
}
+2
View File
@@ -147,6 +147,7 @@ func (p *binarylaneProvider) DataSources(_ context.Context) []func() datasource.
NewSshKeyDataSource,
NewVpcDataSource,
NewVpcRouteEntriesDataSource,
NewLoadBalancerDataSource,
}
}
@@ -157,5 +158,6 @@ func (p *binarylaneProvider) Resources(_ context.Context) []func() resource.Reso
NewSshKeyResource,
NewVpcResource,
NewVpcRouteEntriesResource,
NewLoadBalancerResource,
}
}
+13 -9
View File
@@ -50,6 +50,7 @@ type serverModel struct {
PublicIpv4Addresses types.List `tfsdk:"public_ipv4_addresses"`
PrivateIPv4Addresses types.List `tfsdk:"private_ipv4_addresses"`
Permalink types.String `tfsdk:"permalink"`
Password types.String `tfsdk:"password"`
}
func (d *serverResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
@@ -92,15 +93,6 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
},
}
pw := resp.Schema.Attributes["password"]
resp.Schema.Attributes["password"] = &schema.StringAttribute{
Description: pw.GetDescription(),
MarkdownDescription: pw.GetMarkdownDescription(),
Optional: pw.IsOptional(),
Computed: false, // Computed must be false to allow server to be created without password
Sensitive: true, // Mark password as sensitive
}
backups := resp.Schema.Attributes["backups"]
resp.Schema.Attributes["backups"] = &schema.BoolAttribute{
Description: backups.GetDescription(),
@@ -152,6 +144,18 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
}
// Additional attributes
pwDescription :=
"If this is provided the specified or default remote user's account password will be set to this value. " +
"Only valid if the server supports password change actions. If omitted and the server supports password " +
"change actions a random password will be generated and emailed to the account email address."
resp.Schema.Attributes["password"] = &schema.StringAttribute{
Description: pwDescription,
MarkdownDescription: pwDescription,
Optional: true, // Password optional, if not set will be emailed to user
Computed: false, // Computed must be false to allow server to be created without password
Sensitive: true, // Mark password as sensitive
}
waitDescription := "The number of seconds to wait for the server to be created, after which, a timeout error will " +
"be reported. If `wait_seconds` is left empty or set to 0, Terraform will succeed without waiting for the " +
"server creation to complete."
@@ -91,12 +91,14 @@ echo "Hello World" > /var/tmp/output.txt
resource.TestCheckResourceAttrPair("data.binarylane_server.test", "permalink", "binarylane_server.test", "permalink"),
),
},
// Test import by ID
{
ResourceName: "binarylane_server.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"password", "ssh_keys", "user_data", "wait_for_create", "public_ipv4_count"},
},
// Test import by name
{
ResourceName: "binarylane_server.test",
ImportState: true,
+64 -44
View File
@@ -3,8 +3,8 @@ package provider
import (
"fmt"
"slices"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
d_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
r_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -24,110 +24,130 @@ func convertResourceSchemaToDataSourceSchema(rs r_schema.Schema, cfg AttributeCo
MarkdownDescription: rs.GetMarkdownDescription(),
DeprecationMessage: rs.GetDeprecationMessage(),
}
for name, attr := range rs.Attributes {
for name, attribute := range rs.Attributes {
if cfg.ExcludedAttributes != nil && slices.Contains(*cfg.ExcludedAttributes, name) {
continue
}
required := cfg.RequiredAttributes != nil && slices.Contains(*cfg.RequiredAttributes, name)
optional := cfg.OptionalAttributes != nil && slices.Contains(*cfg.OptionalAttributes, name)
switch attr.GetType() {
switch attribute.GetType() {
case types.BoolType:
ds.Attributes[name] = d_schema.BoolAttribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.DynamicType:
ds.Attributes[name] = d_schema.DynamicAttribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.Float32Type:
ds.Attributes[name] = d_schema.Float32Attribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.Float64Type:
ds.Attributes[name] = d_schema.Float64Attribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.Int32Type:
ds.Attributes[name] = d_schema.Int32Attribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.Int64Type:
ds.Attributes[name] = d_schema.Int64Attribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.NumberType:
ds.Attributes[name] = d_schema.NumberAttribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.StringType:
ds.Attributes[name] = d_schema.StringAttribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
default:
// Feel free to to raise a PR and remove this hack
if strings.HasPrefix(attr.GetType().String(), "types.ListType") {
if listType, isList := attribute.GetType().(types.ListType); isList {
ds.Attributes[name] = d_schema.ListAttribute{
ElementType: attr.GetType().(types.ListType).ElemType,
Description: attr.GetDescription(),
ElementType: listType.ElemType,
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
} else {
return nil, fmt.Errorf("failed to convert resource schema attribute to data source schema attribute: name=%s, type=%s", name, attr.GetType())
continue
}
if objType, isObject := attribute.(r_schema.SingleNestedAttribute); isObject {
attributeTypes := make(map[string]attr.Type, len(objType.GetAttributes()))
for name, attribute := range objType.GetAttributes() {
attributeTypes[name] = attribute.GetType()
}
ds.Attributes[name] = d_schema.ObjectAttribute{
CustomType: objType.CustomType,
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
AttributeTypes: attributeTypes,
}
continue
}
return nil, fmt.Errorf("failed to convert resource schema attribute to data source schema attribute: name=%s, type=%s", name, attribute.GetType())
}
}
@@ -0,0 +1,819 @@
// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
package resources
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"regexp"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)
func LoadBalancerResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"forwarding_rules": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"entry_protocol": schema.StringAttribute{
Required: true,
Description: "The protocol that traffic must match for this load balancer to forward traffic according to this rule.",
MarkdownDescription: "The protocol that traffic must match for this load balancer to forward traffic according to this rule.",
Validators: []validator.String{
stringvalidator.OneOf(
"http",
"https",
),
},
},
},
CustomType: ForwardingRulesType{
ObjectType: types.ObjectType{
AttrTypes: ForwardingRulesValue{}.AttributeTypes(ctx),
},
},
},
Optional: true,
Computed: true,
Description: "The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default \"HTTP\" only forwarding rule.",
MarkdownDescription: "The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default \"HTTP\" only forwarding rule.",
},
"health_check": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"path": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "Leave null to accept the default '/' path.",
MarkdownDescription: "Leave null to accept the default '/' path.",
Validators: []validator.String{
stringvalidator.RegexMatches(regexp.MustCompile("/[A-Za-z0-9/.?=&+%_-]*"), ""),
},
},
"protocol": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "Leave null to accept the default HTTP protocol.",
MarkdownDescription: "Leave null to accept the default HTTP protocol.",
},
},
CustomType: HealthCheckType{
ObjectType: types.ObjectType{
AttrTypes: HealthCheckValue{}.AttributeTypes(ctx),
},
},
Optional: true,
Computed: true,
Description: "The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules.",
MarkdownDescription: "The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules.",
},
"id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "The ID of the load balancer to fetch.",
MarkdownDescription: "The ID of the load balancer to fetch.",
},
"name": schema.StringAttribute{
Required: true,
Description: "The hostname of the load balancer.",
MarkdownDescription: "The hostname of the load balancer.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"region": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "Leave null to create an anycast load balancer.",
MarkdownDescription: "Leave null to create an anycast load balancer.",
},
"server_ids": schema.ListAttribute{
ElementType: types.Int64Type,
Optional: true,
Computed: true,
Description: "A list of server IDs to assign to this load balancer.",
MarkdownDescription: "A list of server IDs to assign to this load balancer.",
},
},
}
}
type LoadBalancerModel struct {
ForwardingRules types.List `tfsdk:"forwarding_rules"`
HealthCheck HealthCheckValue `tfsdk:"health_check"`
Id types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Region types.String `tfsdk:"region"`
ServerIds types.List `tfsdk:"server_ids"`
}
var _ basetypes.ObjectTypable = ForwardingRulesType{}
type ForwardingRulesType struct {
basetypes.ObjectType
}
func (t ForwardingRulesType) Equal(o attr.Type) bool {
other, ok := o.(ForwardingRulesType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t ForwardingRulesType) String() string {
return "ForwardingRulesType"
}
func (t ForwardingRulesType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
entryProtocolAttribute, ok := attributes["entry_protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`entry_protocol is missing from object`)
return nil, diags
}
entryProtocolVal, ok := entryProtocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`entry_protocol expected to be basetypes.StringValue, was: %T`, entryProtocolAttribute))
}
if diags.HasError() {
return nil, diags
}
return ForwardingRulesValue{
EntryProtocol: entryProtocolVal,
state: attr.ValueStateKnown,
}, diags
}
func NewForwardingRulesValueNull() ForwardingRulesValue {
return ForwardingRulesValue{
state: attr.ValueStateNull,
}
}
func NewForwardingRulesValueUnknown() ForwardingRulesValue {
return ForwardingRulesValue{
state: attr.ValueStateUnknown,
}
}
func NewForwardingRulesValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (ForwardingRulesValue, diag.Diagnostics) {
var diags diag.Diagnostics
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
ctx := context.Background()
for name, attributeType := range attributeTypes {
attribute, ok := attributes[name]
if !ok {
diags.AddError(
"Missing ForwardingRulesValue Attribute Value",
"While creating a ForwardingRulesValue value, a missing attribute value was detected. "+
"A ForwardingRulesValue must contain values for all attributes, even if null or unknown. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("ForwardingRulesValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid ForwardingRulesValue Attribute Type",
"While creating a ForwardingRulesValue value, an invalid attribute value was detected. "+
"A ForwardingRulesValue must use a matching attribute type for the value. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("ForwardingRulesValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("ForwardingRulesValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra ForwardingRulesValue Attribute Value",
"While creating a ForwardingRulesValue value, an extra attribute value was detected. "+
"A ForwardingRulesValue must not contain values beyond the expected attribute types. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Extra ForwardingRulesValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewForwardingRulesValueUnknown(), diags
}
entryProtocolAttribute, ok := attributes["entry_protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`entry_protocol is missing from object`)
return NewForwardingRulesValueUnknown(), diags
}
entryProtocolVal, ok := entryProtocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`entry_protocol expected to be basetypes.StringValue, was: %T`, entryProtocolAttribute))
}
if diags.HasError() {
return NewForwardingRulesValueUnknown(), diags
}
return ForwardingRulesValue{
EntryProtocol: entryProtocolVal,
state: attr.ValueStateKnown,
}, diags
}
func NewForwardingRulesValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) ForwardingRulesValue {
object, diags := NewForwardingRulesValue(attributeTypes, attributes)
if diags.HasError() {
// This could potentially be added to the diag package.
diagsStrings := make([]string, 0, len(diags))
for _, diagnostic := range diags {
diagsStrings = append(diagsStrings, fmt.Sprintf(
"%s | %s | %s",
diagnostic.Severity(),
diagnostic.Summary(),
diagnostic.Detail()))
}
panic("NewForwardingRulesValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t ForwardingRulesType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewForwardingRulesValueNull(), nil
}
if !in.Type().Equal(t.TerraformType(ctx)) {
return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
}
if !in.IsKnown() {
return NewForwardingRulesValueUnknown(), nil
}
if in.IsNull() {
return NewForwardingRulesValueNull(), nil
}
attributes := map[string]attr.Value{}
val := map[string]tftypes.Value{}
err := in.As(&val)
if err != nil {
return nil, err
}
for k, v := range val {
a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
if err != nil {
return nil, err
}
attributes[k] = a
}
return NewForwardingRulesValueMust(ForwardingRulesValue{}.AttributeTypes(ctx), attributes), nil
}
func (t ForwardingRulesType) ValueType(ctx context.Context) attr.Value {
return ForwardingRulesValue{}
}
var _ basetypes.ObjectValuable = ForwardingRulesValue{}
type ForwardingRulesValue struct {
EntryProtocol basetypes.StringValue `tfsdk:"entry_protocol"`
state attr.ValueState
}
func (v ForwardingRulesValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 1)
var val tftypes.Value
var err error
attrTypes["entry_protocol"] = basetypes.StringType{}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 1)
val, err = v.EntryProtocol.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["entry_protocol"] = val
if err := tftypes.ValidateValue(objectType, vals); err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
return tftypes.NewValue(objectType, vals), nil
case attr.ValueStateNull:
return tftypes.NewValue(objectType, nil), nil
case attr.ValueStateUnknown:
return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
default:
panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
}
}
func (v ForwardingRulesValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v ForwardingRulesValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v ForwardingRulesValue) String() string {
return "ForwardingRulesValue"
}
func (v ForwardingRulesValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
attributeTypes := map[string]attr.Type{
"entry_protocol": basetypes.StringType{},
}
if v.IsNull() {
return types.ObjectNull(attributeTypes), diags
}
if v.IsUnknown() {
return types.ObjectUnknown(attributeTypes), diags
}
objVal, diags := types.ObjectValue(
attributeTypes,
map[string]attr.Value{
"entry_protocol": v.EntryProtocol,
})
return objVal, diags
}
func (v ForwardingRulesValue) Equal(o attr.Value) bool {
other, ok := o.(ForwardingRulesValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.EntryProtocol.Equal(other.EntryProtocol) {
return false
}
return true
}
func (v ForwardingRulesValue) Type(ctx context.Context) attr.Type {
return ForwardingRulesType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v ForwardingRulesValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"entry_protocol": basetypes.StringType{},
}
}
var _ basetypes.ObjectTypable = HealthCheckType{}
type HealthCheckType struct {
basetypes.ObjectType
}
func (t HealthCheckType) Equal(o attr.Type) bool {
other, ok := o.(HealthCheckType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t HealthCheckType) String() string {
return "HealthCheckType"
}
func (t HealthCheckType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
pathAttribute, ok := attributes["path"]
if !ok {
diags.AddError(
"Attribute Missing",
`path is missing from object`)
return nil, diags
}
pathVal, ok := pathAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`path expected to be basetypes.StringValue, was: %T`, pathAttribute))
}
protocolAttribute, ok := attributes["protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`protocol is missing from object`)
return nil, diags
}
protocolVal, ok := protocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`protocol expected to be basetypes.StringValue, was: %T`, protocolAttribute))
}
if diags.HasError() {
return nil, diags
}
return HealthCheckValue{
Path: pathVal,
Protocol: protocolVal,
state: attr.ValueStateKnown,
}, diags
}
func NewHealthCheckValueNull() HealthCheckValue {
return HealthCheckValue{
state: attr.ValueStateNull,
}
}
func NewHealthCheckValueUnknown() HealthCheckValue {
return HealthCheckValue{
state: attr.ValueStateUnknown,
}
}
func NewHealthCheckValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (HealthCheckValue, diag.Diagnostics) {
var diags diag.Diagnostics
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
ctx := context.Background()
for name, attributeType := range attributeTypes {
attribute, ok := attributes[name]
if !ok {
diags.AddError(
"Missing HealthCheckValue Attribute Value",
"While creating a HealthCheckValue value, a missing attribute value was detected. "+
"A HealthCheckValue must contain values for all attributes, even if null or unknown. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("HealthCheckValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid HealthCheckValue Attribute Type",
"While creating a HealthCheckValue value, an invalid attribute value was detected. "+
"A HealthCheckValue must use a matching attribute type for the value. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("HealthCheckValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("HealthCheckValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra HealthCheckValue Attribute Value",
"While creating a HealthCheckValue value, an extra attribute value was detected. "+
"A HealthCheckValue must not contain values beyond the expected attribute types. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Extra HealthCheckValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewHealthCheckValueUnknown(), diags
}
pathAttribute, ok := attributes["path"]
if !ok {
diags.AddError(
"Attribute Missing",
`path is missing from object`)
return NewHealthCheckValueUnknown(), diags
}
pathVal, ok := pathAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`path expected to be basetypes.StringValue, was: %T`, pathAttribute))
}
protocolAttribute, ok := attributes["protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`protocol is missing from object`)
return NewHealthCheckValueUnknown(), diags
}
protocolVal, ok := protocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`protocol expected to be basetypes.StringValue, was: %T`, protocolAttribute))
}
if diags.HasError() {
return NewHealthCheckValueUnknown(), diags
}
return HealthCheckValue{
Path: pathVal,
Protocol: protocolVal,
state: attr.ValueStateKnown,
}, diags
}
func NewHealthCheckValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) HealthCheckValue {
object, diags := NewHealthCheckValue(attributeTypes, attributes)
if diags.HasError() {
// This could potentially be added to the diag package.
diagsStrings := make([]string, 0, len(diags))
for _, diagnostic := range diags {
diagsStrings = append(diagsStrings, fmt.Sprintf(
"%s | %s | %s",
diagnostic.Severity(),
diagnostic.Summary(),
diagnostic.Detail()))
}
panic("NewHealthCheckValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t HealthCheckType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewHealthCheckValueNull(), nil
}
if !in.Type().Equal(t.TerraformType(ctx)) {
return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
}
if !in.IsKnown() {
return NewHealthCheckValueUnknown(), nil
}
if in.IsNull() {
return NewHealthCheckValueNull(), nil
}
attributes := map[string]attr.Value{}
val := map[string]tftypes.Value{}
err := in.As(&val)
if err != nil {
return nil, err
}
for k, v := range val {
a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
if err != nil {
return nil, err
}
attributes[k] = a
}
return NewHealthCheckValueMust(HealthCheckValue{}.AttributeTypes(ctx), attributes), nil
}
func (t HealthCheckType) ValueType(ctx context.Context) attr.Value {
return HealthCheckValue{}
}
var _ basetypes.ObjectValuable = HealthCheckValue{}
type HealthCheckValue struct {
Path basetypes.StringValue `tfsdk:"path"`
Protocol basetypes.StringValue `tfsdk:"protocol"`
state attr.ValueState
}
func (v HealthCheckValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 2)
var val tftypes.Value
var err error
attrTypes["path"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["protocol"] = basetypes.StringType{}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 2)
val, err = v.Path.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["path"] = val
val, err = v.Protocol.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["protocol"] = val
if err := tftypes.ValidateValue(objectType, vals); err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
return tftypes.NewValue(objectType, vals), nil
case attr.ValueStateNull:
return tftypes.NewValue(objectType, nil), nil
case attr.ValueStateUnknown:
return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
default:
panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
}
}
func (v HealthCheckValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v HealthCheckValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v HealthCheckValue) String() string {
return "HealthCheckValue"
}
func (v HealthCheckValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
attributeTypes := map[string]attr.Type{
"path": basetypes.StringType{},
"protocol": basetypes.StringType{},
}
if v.IsNull() {
return types.ObjectNull(attributeTypes), diags
}
if v.IsUnknown() {
return types.ObjectUnknown(attributeTypes), diags
}
objVal, diags := types.ObjectValue(
attributeTypes,
map[string]attr.Value{
"path": v.Path,
"protocol": v.Protocol,
})
return objVal, diags
}
func (v HealthCheckValue) Equal(o attr.Value) bool {
other, ok := o.(HealthCheckValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.Path.Equal(other.Path) {
return false
}
if !v.Protocol.Equal(other.Protocol) {
return false
}
return true
}
func (v HealthCheckValue) Type(ctx context.Context) attr.Type {
return HealthCheckType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v HealthCheckValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"path": basetypes.StringType{},
"protocol": basetypes.StringType{},
}
}
@@ -37,12 +37,6 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
Description: "The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name.",
MarkdownDescription: "The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name.",
},
"password": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "If this is provided the specified or default remote user's account password will be set to this value. Only valid if the server supports password change actions. If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.\n",
MarkdownDescription: "If this is provided the specified or default remote user's account password will be set to this value. Only valid if the server supports password change actions. If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.\n",
},
"port_blocking": schema.BoolAttribute{
Optional: true,
Computed: true,
@@ -96,7 +90,6 @@ type ServerModel struct {
Id types.Int64 `tfsdk:"id"`
Image types.String `tfsdk:"image"`
Name types.String `tfsdk:"name"`
Password types.String `tfsdk:"password"`
PortBlocking types.Bool `tfsdk:"port_blocking"`
Region types.String `tfsdk:"region"`
Size types.String `tfsdk:"size"`
+118 -7
View File
@@ -3,6 +3,124 @@
"name": "binarylane"
},
"resources": [
{
"name": "load_balancer",
"schema": {
"attributes": [
{
"name": "forwarding_rules",
"list_nested": {
"computed_optional_required": "computed_optional",
"nested_object": {
"attributes": [
{
"name": "entry_protocol",
"string": {
"computed_optional_required": "required",
"description": "The protocol that traffic must match for this load balancer to forward traffic according to this rule.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.OneOf(\n\"http\",\n\"https\",\n)"
}
}
]
}
}
]
},
"description": "The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default \"HTTP\" only forwarding rule."
}
},
{
"name": "health_check",
"single_nested": {
"computed_optional_required": "computed_optional",
"attributes": [
{
"name": "path",
"string": {
"computed_optional_required": "computed_optional",
"description": "Leave null to accept the default '/' path.",
"validators": [
{
"custom": {
"imports": [
{
"path": "regexp"
},
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.RegexMatches(regexp.MustCompile(\"/[A-Za-z0-9/.?=\u0026+%_-]*\"), \"\")"
}
}
]
}
},
{
"name": "protocol",
"string": {
"computed_optional_required": "computed_optional",
"description": "Leave null to accept the default HTTP protocol."
}
}
],
"description": "The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules."
}
},
{
"name": "name",
"string": {
"computed_optional_required": "required",
"description": "The hostname of the load balancer.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.LengthAtLeast(1)"
}
}
]
}
},
{
"name": "region",
"string": {
"computed_optional_required": "computed_optional",
"description": "Leave null to create an anycast load balancer."
}
},
{
"name": "server_ids",
"list": {
"computed_optional_required": "computed_optional",
"element_type": {
"int64": {}
},
"description": "A list of server IDs to assign to this load balancer."
}
},
{
"name": "id",
"int64": {
"computed_optional_required": "computed_optional",
"description": "The ID of the load balancer to fetch."
}
}
]
}
},
{
"name": "server",
"schema": {
@@ -28,13 +146,6 @@
"description": "The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name."
}
},
{
"name": "password",
"string": {
"computed_optional_required": "computed_optional",
"description": "If this is provided the specified or default remote user's account password will be set to this value. Only valid if the server supports password change actions. If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.\n"
}
},
{
"name": "port_blocking",
"bool": {
+21 -5
View File
@@ -34,11 +34,6 @@ resources:
value if provided. Setting this to false has no effect.
image:
description: The slug of the selected operating system.
password:
description: >
If this is provided the specified or default remote user's account password will be set to this value.
Only valid if the server supports password change actions. If omitted and the server supports password
change actions a random password will be generated and emailed to the account email address.
ssh_keys:
description: >
This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as
@@ -50,6 +45,27 @@ resources:
- links
- server
- options
- password
load_balancer:
create:
path: /load_balancers
method: POST
read:
path: /load_balancers/{load_balancer_id}
method: GET
update:
path: /load_balancers/{load_balancer_id}
method: PUT
delete:
path: /load_balancers/{load_balancer_id}
method: DELETE
schema:
attributes:
aliases:
load_balancer_id: id
ignores:
- links
- load_balancer
server_firewall_rules:
create:
path: /servers/{server_id}/actions#ChangeAdvancedFirewallRules