From 44772aa55dcd6bfcc39c04498283cb5d4742dcad Mon Sep 17 00:00:00 2001 From: Oscar Hermoso Date: Mon, 16 Sep 2024 15:55:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=9B=20load=20balancer=20resource=20(#1?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚛 load balancer resource * Fix whitespace in server resource --- .github/workflows/test.yaml | 39 + CONTRIBUTING.md | 19 +- README.md | 2 +- docs/data-sources/load_balancer.md | 44 + docs/resources/load_balancer.md | 47 + examples/k3s/README.md | 9 +- go.mod | 2 +- go.sum | 4 +- internal/binarylane/fetch-openapi.sh | 7 +- internal/binarylane/openapi.json | 15 +- internal/binarylane/types_gen.go | 17 +- .../provider/load_balancer_data_source.go | 104 +++ internal/provider/load_balancer_resource.go | 389 +++++++++ .../provider/load_balancer_resource_test.go | 86 ++ internal/provider/provider.go | 2 + internal/provider/server_resource.go | 22 +- internal/provider/server_resource_test.go | 2 + internal/provider/util.go | 108 ++- .../resources/load_balancer_resource_gen.go | 819 ++++++++++++++++++ internal/resources/server_resource_gen.go | 7 - provider_code_spec.json | 125 ++- provider_gen_config.yml | 26 +- 22 files changed, 1795 insertions(+), 100 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 docs/data-sources/load_balancer.md create mode 100644 docs/resources/load_balancer.md create mode 100644 internal/provider/load_balancer_data_source.go create mode 100644 internal/provider/load_balancer_resource.go create mode 100644 internal/provider/load_balancer_resource_test.go create mode 100644 internal/resources/load_balancer_resource_gen.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..ed1f0e9 --- /dev/null +++ b/.github/workflows/test.yaml @@ -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 ./... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eedf6a3..101ac20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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, + } diff --git a/README.md b/README.md index 3e1d74f..7a4af82 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/data-sources/load_balancer.md b/docs/data-sources/load_balancer.md new file mode 100644 index 0000000..16732c1 --- /dev/null +++ b/docs/data-sources/load_balancer.md @@ -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 + +### 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. + + +### Nested Schema for `forwarding_rules` + +Read-Only: + +- `entry_protocol` (String) + + + +### Nested Schema for `health_check` + +Read-Only: + +- `path` (String) +- `protocol` (String) diff --git a/docs/resources/load_balancer.md b/docs/resources/load_balancer.md new file mode 100644 index 0000000..09b6fd8 --- /dev/null +++ b/docs/resources/load_balancer.md @@ -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 + +### 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. + + +### 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. + + + +### 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. diff --git a/examples/k3s/README.md b/examples/k3s/README.md index 28f85fc..d641f31 100644 --- a/examples/k3s/README.md +++ b/examples/k3s/README.md @@ -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) diff --git a/go.mod b/go.mod index 7b2e717..3d19c47 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 0d70a6a..5621dbd 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/binarylane/fetch-openapi.sh b/internal/binarylane/fetch-openapi.sh index 06569bf..23569e0 100755 --- a/internal/binarylane/fetch-openapi.sh +++ b/internal/binarylane/fetch-openapi.sh @@ -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 diff --git a/internal/binarylane/openapi.json b/internal/binarylane/openapi.json index 3854ca4..9e4bd46 100644 --- a/internal/binarylane/openapi.json +++ b/internal/binarylane/openapi.json @@ -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": [ diff --git a/internal/binarylane/types_gen.go b/internal/binarylane/types_gen.go index daaee9c..70c5df1 100644 --- a/internal/binarylane/types_gen.go +++ b/internal/binarylane/types_gen.go @@ -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 { diff --git a/internal/provider/load_balancer_data_source.go b/internal/provider/load_balancer_data_source.go new file mode 100644 index 0000000..cda41fc --- /dev/null +++ b/internal/provider/load_balancer_data_source.go @@ -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)...) +} diff --git a/internal/provider/load_balancer_resource.go b/internal/provider/load_balancer_resource.go new file mode 100644 index 0000000..6b11c2d --- /dev/null +++ b/internal/provider/load_balancer_resource.go @@ -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, ¶ms) + 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 +} diff --git a/internal/provider/load_balancer_resource_test.go b/internal/provider/load_balancer_resource_test.go new file mode 100644 index 0000000..c797666 --- /dev/null +++ b/internal/provider/load_balancer_resource_test.go @@ -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, + }, + }, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f2e0791..45e9f91 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -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, } } diff --git a/internal/provider/server_resource.go b/internal/provider/server_resource.go index d147b59..310f1bc 100644 --- a/internal/provider/server_resource.go +++ b/internal/provider/server_resource.go @@ -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." diff --git a/internal/provider/server_resource_test.go b/internal/provider/server_resource_test.go index 79e1e21..3db0a6c 100644 --- a/internal/provider/server_resource_test.go +++ b/internal/provider/server_resource_test.go @@ -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, diff --git a/internal/provider/util.go b/internal/provider/util.go index a847da0..b5fa74b 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -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()) } } diff --git a/internal/resources/load_balancer_resource_gen.go b/internal/resources/load_balancer_resource_gen.go new file mode 100644 index 0000000..b3c9e95 --- /dev/null +++ b/internal/resources/load_balancer_resource_gen.go @@ -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{}, + } +} diff --git a/internal/resources/server_resource_gen.go b/internal/resources/server_resource_gen.go index 0d99a21..997e685 100644 --- a/internal/resources/server_resource_gen.go +++ b/internal/resources/server_resource_gen.go @@ -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"` diff --git a/provider_code_spec.json b/provider_code_spec.json index cdcdec3..b7e1f39 100644 --- a/provider_code_spec.json +++ b/provider_code_spec.json @@ -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": { diff --git a/provider_gen_config.yml b/provider_gen_config.yml index 3c837b0..d63ea8a 100644 --- a/provider_gen_config.yml +++ b/provider_gen_config.yml @@ -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