Compare commits

...

2 Commits

Author SHA1 Message Date
Oscar Hermoso 32e9056957 💽 WIP disks 2025-08-17 17:22:26 +08:00
Oscar Hermoso 325b2e96ea 👆 Update OpenAPI schema to 0.32.1 2025-08-16 11:12:32 +08:00
21 changed files with 1160 additions and 108 deletions
+5
View File
@@ -45,3 +45,8 @@ jobs:
# GitHub sets the GITHUB_TOKEN secret automatically.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
- name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
run: gh release edit "${{ github.ref }}" --draft false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -1
View File
@@ -24,7 +24,8 @@ provider_installation {
4. Build and test the provider
```sh
go build -o bin/terraform-provider-binarylane
# Build with line numbers
go build -ldflags="-s -w" -o bin/terraform-provider-binarylane
go install
cd examples/basic
terraform plan
+6 -1
View File
@@ -39,7 +39,12 @@ data "binarylane_load_balancer" "example" {
Read-Only:
- `entry_protocol` (String) The protocol that traffic must match for the load balancer to forward it. Valid values are "http" and "https".
- `entry_protocol` (String) The protocol that traffic must match for this load balancer to forward traffic according to this rule.
| Value | Description |
| ----- | ----------- |
| http | The load balancer will forward HTTP traffic that matches this rule. |
| https | The load balancer will forward HTTPS traffic that matches this rule. |
<a id="nestedatt--health_check"></a>
+12
View File
@@ -33,6 +33,7 @@ data "binarylane_server" "example" {
- must be a multiple of 5
- \> 60 GB must be a multiple of 10
- \> 200 GB must be a multiple of 100
- `disks` (Attributes List) A list of the disks that are currently attached to the server. (see [below for nested schema](#nestedatt--disks))
- `image` (String) The slug of the selected operating system, such as `debian-12`. You can fetch a full list of images from the BinaryLane API.
- `memory` (Number) The total memory in MB for this server. Leave null to accept the default size. Valid values:
- must be a multiple of 128
@@ -66,3 +67,14 @@ Read-Only:
- `qemu_guest_agent` (Boolean)
- `uefi_boot` (Boolean)
- `unset_uuid` (Boolean)
<a id="nestedatt--disks"></a>
### Nested Schema for `disks`
Read-Only:
- `description` (String) A description of this disk.
- `id` (Number) The ID of this disk.
- `primary` (Boolean) A primary disk is treated differently from other disks.
- `size_gigabytes` (Number) The size of the disk in GB.
+6 -1
View File
@@ -53,7 +53,12 @@ resource "binarylane_load_balancer" "example" {
Required:
- `entry_protocol` (String) The protocol that traffic must match for the load balancer to forward it. Valid values are "http" and "https".
- `entry_protocol` (String) The protocol that traffic must match for this load balancer to forward traffic according to this rule.
| Value | Description |
| ----- | ----------- |
| http | The load balancer will forward HTTP traffic that matches this rule. |
| https | The load balancer will forward HTTPS traffic that matches this rule. |
<a id="nestedatt--health_check"></a>
+18
View File
@@ -44,6 +44,7 @@ resource "binarylane_server" "example" {
- must be a multiple of 5
- \> 60 GB must be a multiple of 10
- \> 200 GB must be a multiple of 100
- `disks` (Attributes List) A list of the disks that are currently attached to the server. (see [below for nested schema](#nestedatt--disks))
- `memory` (Number) The total memory in MB for this server. Leave null to accept the default size. Valid values:
- must be a multiple of 128
- \> 2048 MB must be a multiple of 1024
@@ -86,6 +87,23 @@ Read-Only:
- `uefi_boot` (Boolean) When this option is enabled the Cloud Server will use UEFI instead of legacy PC BIOS.
<a id="nestedatt--disks"></a>
### Nested Schema for `disks`
Required:
- `size_gigabytes` (Number) The size of the disk in GB.
Optional:
- `description` (String) A description of this disk.
Read-Only:
- `id` (Number) The ID of this disk.
- `primary` (Boolean) A primary disk is treated differently from other disks.
<a id="nestedatt--timeouts"></a>
### Nested Schema for `timeouts`
+7
View File
@@ -17,4 +17,11 @@ resource "binarylane_server" "example" {
size = "std-min" # 1 VPCU, 1 GB Memory, 20 GB NVME Storage, 1000 GB Data Transfer
public_ipv4_count = 1
# Password will be generated automatically and mailed to the account holder
disks = [{
description = "Primary Disk"
size_gigabytes = 10
}, {
description = "Secondary Disk"
size_gigabytes = 10
}]
}
+63 -26
View File
@@ -1189,10 +1189,10 @@ 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" tfsdk:"forwarding_rules"`
ForwardingRules *[]ForwardingRuleRequest `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"`
HealthCheck *HealthCheckRequest `json:"health_check"`
// Name The hostname of the load balancer.
Name string `json:"name"`
@@ -1356,16 +1356,16 @@ type DisableSelinuxType string
// Disk defines model for Disk.
type Disk struct {
// Description A description of this disk.
Description *string `json:"description"`
Description *string `json:"description" tfsdk:"description"`
// Id The ID of this disk.
Id *int64 `json:"id,omitempty"`
Id *int64 `json:"id,omitempty" tfsdk:"id"`
// Primary A primary disk is treated differently from other disks.
Primary *bool `json:"primary,omitempty"`
Primary *bool `json:"primary,omitempty" tfsdk:"primary"`
// SizeGigabytes The size of the disk in GB.
SizeGigabytes *float64 `json:"size_gigabytes,omitempty"`
SizeGigabytes *float64 `json:"size_gigabytes,omitempty" tfsdk:"size_gigabytes"`
}
// DistributionFeature
@@ -1603,14 +1603,50 @@ type ForwardingRule struct {
EntryProtocol LoadBalancerRuleProtocol `json:"entry_protocol" tfsdk:"entry_protocol"`
}
// ForwardingRuleRequest defines model for ForwardingRuleRequest.
type ForwardingRuleRequest struct {
// EntryProtocol The protocol that traffic must match for this load balancer to forward traffic according to this rule.
//
// | Value | Description |
// | ----- | ----------- |
// | http | The load balancer will forward HTTP traffic that matches this rule. |
// | https | The load balancer will forward HTTPS traffic that matches this rule. |
//
EntryProtocol LoadBalancerRuleProtocol `json:"entry_protocol" tfsdk:"entry_protocol"`
}
// ForwardingRulesRequest defines model for ForwardingRulesRequest.
type ForwardingRulesRequest struct {
// ForwardingRules The rules that control which traffic the load balancer will forward to servers in the pool.
ForwardingRules []ForwardingRule `json:"forwarding_rules"`
ForwardingRules []ForwardingRuleRequest `json:"forwarding_rules"`
}
// HealthCheck defines model for HealthCheck.
type HealthCheck struct {
// Path The path to the health check endpoint.
Path string `json:"path"`
// Protocol The protocol used for the health check.
//
// | Value | Description |
// | ----- | ----------- |
// | 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. |
//
Protocol HealthCheckProtocol `json:"protocol"`
}
// HealthCheckProtocol
// | Value | Description |
// | ----- | ----------- |
// | 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
// HealthCheckRequest defines model for HealthCheckRequest.
type HealthCheckRequest struct {
// Path Leave null to accept the default '/' path.
Path *string `json:"path"`
@@ -1625,14 +1661,6 @@ type HealthCheck struct {
Protocol *HealthCheckProtocol `json:"protocol"`
}
// HealthCheckProtocol
// | Value | Description |
// | ----- | ----------- |
// | 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
// Host defines model for Host.
type Host struct {
// DisplayName The name for this host. If this is a dedicated host this property will be empty.
@@ -1912,29 +1940,38 @@ type Links struct {
// LoadBalancer defines model for LoadBalancer.
type LoadBalancer struct {
// CreatedAt The date and time in ISO8601 format of the creation of the load balancer.
CreatedAt *time.Time `json:"created_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
// ForwardingRules The rules that control which traffic the load balancer will forward to servers in the pool.
ForwardingRules *[]ForwardingRule `json:"forwarding_rules,omitempty"`
ForwardingRules []ForwardingRule `json:"forwarding_rules"`
// HealthCheck The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer.
HealthCheck *HealthCheck `json:"health_check,omitempty" tfsdk:"health_check"`
HealthCheck HealthCheck `json:"health_check" tfsdk:"health_check"`
// Id The ID of the load balancer.
Id *int64 `json:"id,omitempty"`
Id int64 `json:"id"`
// Ip The IPv4 address of the load balancer.
Ip *string `json:"ip,omitempty"`
Ip string `json:"ip"`
// Name The hostname of the load balancer.
Name *string `json:"name,omitempty"`
Name string `json:"name"`
// Region The region the load balancer is located in. If this value is null the load balancer is an 'AnyCast' load balancer.
Region *Region `json:"region"`
// ServerIds The server IDs of the servers that are currently in the load balancer pool (regardless of their current 'health').
ServerIds *[]int64 `json:"server_ids,omitempty"`
Status *LoadBalancerStatus `json:"status,omitempty"`
ServerIds []int64 `json:"server_ids"`
// Status The current status of the load balancer.
//
// | Value | Description |
// | ----- | ----------- |
// | new | The load balancer is currently being built and is not ready to accept connections. |
// | active | The load balancer is available. |
// | errored | The load balancer is in an errored state. |
//
Status LoadBalancerStatus `json:"status"`
}
// LoadBalancerAvailabilityOption defines model for LoadBalancerAvailabilityOption.
@@ -1946,7 +1983,7 @@ type LoadBalancerAvailabilityOption struct {
PriceHourly *float64 `json:"price_hourly,omitempty"`
// PriceMonthly Monthly Price in AU$.
PriceMonthly *float64 `json:"price_monthly,omitempty"`
PriceMonthly float64 `json:"price_monthly"`
// Regions The slugs of regions where this load balancer option is available. If this is an Anycast load balancer option this will be null.
Regions *[]string `json:"regions"`
@@ -3048,10 +3085,10 @@ type UpdateDomainRecordRequest struct {
// UpdateLoadBalancerRequest defines model for UpdateLoadBalancerRequest.
type UpdateLoadBalancerRequest 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 *[]ForwardingRuleRequest `json:"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"`
HealthCheck *HealthCheckRequest `json:"health_check"`
// Name The hostname of the load balancer.
Name string `json:"name"`
+17 -29
View File
@@ -154,7 +154,7 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe
}
// Create API call logic
forwardingRules := []binarylane.ForwardingRule{}
forwardingRules := []binarylane.ForwardingRuleRequest{}
diags := data.LoadBalancerModel.ForwardingRules.ElementsAs(ctx, &forwardingRules, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@@ -175,7 +175,7 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe
}
if data.HealthCheck.Path.ValueStringPointer() != nil || data.HealthCheck.Protocol.ValueStringPointer() != nil {
body.HealthCheck = &binarylane.HealthCheck{
body.HealthCheck = &binarylane.HealthCheckRequest{
Path: data.HealthCheck.Path.ValueStringPointer(),
Protocol: data.HealthCheck.Protocol.ValueStringPointer(),
}
@@ -320,7 +320,7 @@ func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRe
}
// Update API call logic
forwardingRules := &[]binarylane.ForwardingRule{}
forwardingRules := &[]binarylane.ForwardingRuleRequest{}
diags := data.LoadBalancerModel.ForwardingRules.ElementsAs(ctx, forwardingRules, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@@ -339,7 +339,7 @@ func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRe
body := binarylane.UpdateLoadBalancerRequest{
Name: data.Name.ValueString(),
ForwardingRules: forwardingRules,
HealthCheck: &binarylane.HealthCheck{
HealthCheck: &binarylane.HealthCheckRequest{
Path: data.HealthCheck.Path.ValueStringPointer(),
Protocol: data.HealthCheck.Protocol.ValueStringPointer(),
},
@@ -442,7 +442,7 @@ func (r *loadBalancerResource) ImportState(
loadBalancers := *lbResp.JSON200.LoadBalancers
for _, lb := range loadBalancers {
if *lb.Name == req.ID {
if lb.Name == req.ID {
loadBalancer = lb
nextPage = false
break
@@ -456,24 +456,16 @@ func (r *loadBalancerResource) ImportState(
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)
diags := resp.State.SetAttribute(ctx, path.Root("id"), loadBalancer.Id)
resp.Diagnostics.Append(diags...)
}
func setLoadBalancerModelState(ctx context.Context, data *loadBalancerDataModel, lb *binarylane.LoadBalancer) diag.Diagnostics {
var diags, diag diag.Diagnostics
data.Id = types.Int64Value(*lb.Id)
data.Name = types.StringValue(*lb.Name)
data.Ip = types.StringValue(*lb.Ip)
data.Id = types.Int64Value(lb.Id)
data.Name = types.StringValue(lb.Name)
data.Ip = types.StringValue(lb.Ip)
if lb.Region == nil {
data.Region = types.StringNull()
@@ -483,18 +475,14 @@ func setLoadBalancerModelState(ctx context.Context, data *loadBalancerDataModel,
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.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)},
@@ -180,15 +180,15 @@ func init() {
loadBalancers := *listResp.JSON200.LoadBalancers
for _, lb := range loadBalancers {
if strings.HasPrefix(*lb.Name, "tf-test-") {
deleteResp, err := client.DeleteLoadBalancersLoadBalancerIdWithResponse(ctx, *lb.Id)
if strings.HasPrefix(lb.Name, "tf-test-") {
deleteResp, err := client.DeleteLoadBalancersLoadBalancerIdWithResponse(ctx, lb.Id)
if err != nil {
return fmt.Errorf("Error deleting load balancer %d for test sweep: %w", *lb.Id, err)
return fmt.Errorf("Error deleting load balancer %d for test sweep: %w", lb.Id, err)
}
if deleteResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting load balancer %d in test sweep: %s", deleteResp.StatusCode(), *lb.Id, deleteResp.Body)
return fmt.Errorf("Unexpected status %d deleting load balancer %d in test sweep: %s", deleteResp.StatusCode(), lb.Id, deleteResp.Body)
}
log.Println("Deleted load balancer during test sweep:", *lb.Id)
log.Println("Deleted load balancer during test sweep:", lb.Id)
}
}
if listResp.JSON200.Links == nil || listResp.JSON200.Links.Pages == nil || listResp.JSON200.Links.Pages.Next == nil {
+8
View File
@@ -162,6 +162,14 @@ func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
data.PrivateIPv4Addresses = tfPrivateIpv4Addresses
}
disks, diag := types.ListValueFrom(ctx, data.Disks.ElementType(ctx), serverResp.JSON200.Server.Disks)
diags.Append(diag...)
if diag.HasError() {
data.Disks = types.ListUnknown(data.Disks.ElementType(ctx))
} else {
data.Disks = disks
}
// Get user data script
userDataResp, err := d.bc.client.GetServersServerIdUserDataWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
+225 -5
View File
@@ -421,8 +421,13 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
if (plan.Disk.IsNull() || plan.Disk.IsUnknown()) && plan.Size.Equal(state.Size) {
plan.Disk = state.Disk
}
if (plan.Disks.IsNull() || plan.Disks.IsUnknown()) && plan.Size.Equal(state.Size) {
plan.Disks = state.Disks
}
if isAdvFeatChanged(&config.AdvancedFeatures, &state.AdvancedFeatures) {
if !isAdvFeatChanged(&config.AdvancedFeatures, &state.AdvancedFeatures) {
plan.AdvancedFeatures = state.AdvancedFeatures
} else {
advFeatResp, err := r.bc.client.GetServersServerIdAvailableAdvancedFeaturesWithResponse(ctx, state.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
@@ -628,6 +633,14 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
data.PrivateIPv4Addresses, diags = types.ListValueFrom(ctx, types.StringType, privateIpv4Addresses)
resp.Diagnostics.Append(diags...)
disks, diags := types.ListValueFrom(ctx, data.Disks.ElementType(ctx), serverResp.JSON200.Server.Disks)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
data.Disks = types.ListUnknown(data.Disks.ElementType(ctx))
} else {
data.Disks = disks
}
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
if resp.Diagnostics.HasError() {
@@ -658,8 +671,14 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
// One extra read to check the final state of enabled_advanced_features, needed because
// some flags (like "cloud-init") are not set until the server is fully created. See #13
diag := r.fetchServerResourceState(ctx, &data)
resp.Diagnostics.Append(diag...)
diags = r.fetchServerResourceState(ctx, &data)
resp.Diagnostics.Append(diags...)
// Update disks
if !config.Disks.IsNull() && !config.Disks.Equal(data.Disks) {
diags = r.updateDisks(ctx, &data, config.Disks)
resp.Diagnostics.Append(diags...)
}
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
@@ -999,6 +1018,18 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
}
}
// Update disks
if !config.Disks.IsNull() && !config.Disks.Equal(plan.Disks) {
diags := r.updateDisks(ctx, &state, config.Disks)
resp.Diagnostics.Append(diags...)
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
}
// Enable or disable backups
if !plan.Backups.Equal(state.Backups) {
if plan.Backups.ValueBool() {
@@ -1223,6 +1254,181 @@ func (r *serverResource) updateSourceDestCheck(
return nil
}
func (r *serverResource) updateDisks(ctx context.Context, state *serverResourceModel, disks types.List) diag.Diagnostics {
var diags diag.Diagnostics
if state.Disk.IsUnknown() || state.Disk.IsNull() {
diags.AddError("Error updating disks", "Unable to update disks because the `disk` attribute is unknown or null.")
return diags
}
if state.Disks.IsUnknown() || state.Disks.IsNull() {
diags.AddError("Error updating disks", "Unable to update disks because the `disks` attribute is unknown or null.")
return diags
}
if state.Disks.Equal(disks) {
tflog.Debug(ctx, "No changes to disks, skipping update")
return diags
}
var current []resources.DisksValue
diags.Append(state.Disks.ElementsAs(ctx, &current, false)...)
if diags.HasError() {
return diags
}
var desired []resources.DisksValue
diags.Append(disks.ElementsAs(ctx, &desired, false)...)
if diags.HasError() {
return diags
}
// 1. Check that desired disks fit within max VM disk size
var totalDesired float64
for _, d := range desired {
totalDesired += d.SizeGigabytes.ValueFloat64()
}
if int32(totalDesired) > state.Disk.ValueInt32() {
diags.AddError("Error updating disks",
fmt.Sprintf("Total desired disk size %d GB exceeds maximum allowed %d GB",
int32(totalDesired), state.Disk.ValueInt32()))
return diags
}
// Build maps for diffing
currentMap := make(map[int64]resources.DisksValue)
for _, d := range current {
currentMap[d.Id.ValueInt64()] = d
}
desiredMap := make(map[int64]resources.DisksValue)
for _, d := range desired {
// Only disks with IDs are "existing". New ones will have Id.IsNull()
if !d.Id.IsNull() && !d.Id.IsUnknown() {
desiredMap[d.Id.ValueInt64()] = d
}
}
// Remove unused disks
for id := range currentMap {
if _, ok := desiredMap[id]; !ok {
tflog.Debug(ctx, "Deleting disk", map[string]any{"disk_id": id})
resp, err := r.bc.client.PostServersServerIdActionsDeleteDiskWithResponse(ctx, state.Id.ValueInt64(), binarylane.DeleteDisk{
DiskId: id,
Type: "delete_disk",
})
if err != nil {
diags.AddError("Error deleting disk", err.Error())
return diags
}
// WIP
// binarylane_server.example: Modifying... [name=tf-example-basic]
// ╷
// │ Error: Unexpected HTTP status code deleting disk
// │
// │ with binarylane_server.example,
// │ on main.tf line 13, in resource "binarylane_server" "example":
// │ 13: resource "binarylane_server" "example" {
// │
// │ Received 400 Bad Request deleting disk: server_id=523165, disk_id=1. Details:
// │ {"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors
// │ occurred","status":400,"errors":{"disk_id":["May not delete primary disk."]},"message":"May not delete
// │ primary disk."}
if resp.StatusCode() != http.StatusOK {
diags.AddError("Unexpected HTTP status code deleting disk",
fmt.Sprintf("Received %s deleting disk: server_id=%s, disk_id=%d. Details: %s",
resp.Status(), state.Id.String(), id, resp.Body))
return diags
}
err = r.waitForServerAction(ctx, state.Id.ValueInt64(), *resp.JSON200.Action.Id)
if err != nil {
diags.AddError("Error waiting for disks to be deleted", err.Error())
return diags
}
}
}
// Downsize disks
for id, desiredDisk := range desiredMap {
if currentDisk, ok := currentMap[id]; ok {
curSize := int32(currentDisk.SizeGigabytes.ValueFloat64())
newSize := int32(desiredDisk.SizeGigabytes.ValueFloat64())
if newSize < curSize {
tflog.Debug(ctx, "Downsizing disk", map[string]any{"disk_id": id, "from": curSize, "to": newSize})
resp, err := r.bc.client.PostServersServerIdActionsResizeDiskWithResponse(ctx, state.Id.ValueInt64(), binarylane.ResizeDisk{
DiskId: id,
SizeGigabytes: newSize,
Type: "resize_disk",
})
if err != nil {
diags.AddError("Error downsizing disk", err.Error())
return diags
}
if resp.StatusCode() != http.StatusOK {
diags.AddError("Unexpected HTTP status code downsizing disks",
fmt.Sprintf("Received %s downsizing disks: server_id=%s, disk_id=%d. Details: %s",
resp.Status(), state.Id.String(), id, resp.Body))
return diags
}
err = r.waitForServerAction(ctx, state.Id.ValueInt64(), *resp.JSON200.Action.Id)
if err != nil {
diags.AddError("Error waiting for disks to downsize", err.Error())
return diags
}
}
}
}
// Upsize disks
for id, desiredDisk := range desiredMap {
if currentDisk, ok := currentMap[id]; ok {
curSize := int32(currentDisk.SizeGigabytes.ValueFloat64())
newSize := int32(desiredDisk.SizeGigabytes.ValueFloat64())
if newSize > curSize {
tflog.Debug(ctx, "Upsizing disk", map[string]any{"disk_id": id, "from": curSize, "to": newSize})
resp, err := r.bc.client.PostServersServerIdActionsResizeDiskWithResponse(ctx, state.Id.ValueInt64(), binarylane.ResizeDisk{
DiskId: id,
SizeGigabytes: newSize,
Type: "resize_disk",
})
if err != nil {
diags.AddError("Error upsizing disk", err.Error())
return diags
}
err = r.waitForServerAction(ctx, state.Id.ValueInt64(), *resp.JSON200.Action.Id)
if err != nil {
diags.AddError("Error waiting for new disks to upsize", err.Error())
return diags
}
}
}
}
// 5. Add new disks (those with no ID in desired)
for _, desiredDisk := range desired {
if desiredDisk.Id.IsNull() || desiredDisk.Id.IsUnknown() {
desc := desiredDisk.Description.ValueStringPointer()
tflog.Debug(ctx, "Creating new disk", map[string]any{"size": desiredDisk.SizeGigabytes.ValueFloat64()})
resp, err := r.bc.client.PostServersServerIdActionsAddDiskWithResponse(ctx, state.Id.ValueInt64(), binarylane.AddDisk{
Description: desc,
SizeGigabytes: int32(desiredDisk.SizeGigabytes.ValueFloat64()),
Type: "add_disk",
})
if err != nil {
diags.AddError("Error creating disk", err.Error())
return diags
}
err = r.waitForServerAction(ctx, state.Id.ValueInt64(), *resp.JSON200.Action.Id)
if err != nil {
diags.AddError("Error waiting for new disk to be added", err.Error())
return diags
}
}
}
return diags
}
func (r *serverResource) fetchServerResourceState(ctx context.Context, state *serverResourceModel) diag.Diagnostics {
var diags diag.Diagnostics
@@ -1282,10 +1488,16 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
privateIpv4Addresses := []string{}
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
if v4address.Type == "public" {
switch v4address.Type {
case "public":
publicIpv4Addresses = append(publicIpv4Addresses, v4address.IpAddress)
} else if v4address.Type == "private" {
case "private":
privateIpv4Addresses = append(privateIpv4Addresses, v4address.IpAddress)
default:
diags.AddError(
"Unexpected IP address type",
fmt.Sprintf("Received unexpected IP address type '%s' for server: id=%d, name=%s", v4address.Type, *serverResp.JSON200.Server.Id, *serverResp.JSON200.Server.Name),
)
}
}
@@ -1307,6 +1519,14 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
state.PrivateIPv4Addresses = tfPrivateIpv4Addresses
}
disks, diag := types.ListValueFrom(ctx, state.Disks.ElementType(ctx), serverResp.JSON200.Server.Disks)
diags.Append(diag...)
if diag.HasError() {
state.Disks = types.ListUnknown(state.Disks.ElementType(ctx))
} else {
state.Disks = disks
}
return diags
}
+89
View File
@@ -104,6 +104,11 @@ echo "Hello World" > /var/tmp/output.txt
resource.TestCheckResourceAttr("binarylane_server.test", "advanced_features.cloud_init", "true"),
resource.TestCheckResourceAttr("binarylane_server.test", "advanced_features.qemu_guest_agent", "false"),
resource.TestCheckResourceAttr("binarylane_server.test", "advanced_features.uefi_boot", "false"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.#", "1"),
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.id"),
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.description"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.primary", "true"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.size_gigabytes", "20"),
// Verify data source values
resource.TestCheckResourceAttrPair("data.binarylane_server.test", "id", "binarylane_server.test", "id"),
@@ -130,6 +135,11 @@ echo "Hello World" > /var/tmp/output.txt
resource.TestCheckResourceAttr("data.binarylane_server.test", "advanced_features.cloud_init", "true"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "advanced_features.qemu_guest_agent", "false"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "advanced_features.uefi_boot", "false"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.#", "1"),
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "disks.0.id"),
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "disks.0.description"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.0.primary", "true"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.0.size_gigabytes", "20"),
),
},
// Test import by ID
@@ -313,6 +323,85 @@ resource "binarylane_server" "test" {
})
}
func TestServerResourceDisks(t *testing.T) {
// Must assign a password to the server or Binary Lane will send emails
password := GenerateTestPassword(t)
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: providerConfig + `
resource "binarylane_server" "test" {
name = "tf-test-server-resource"
region = "per"
image = "debian-12"
size = "std-min"
password = "` + password + `"
public_ipv4_count = 0
disks = [
{
description = "Primary Disk"
size_gigabytes = 9
},
{
description = "Secondary Disk"
size_gigabytes = 2
},
{
description = "Tertiary Disk"
size_gigabytes = 4
}
]
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("binarylane_server.test", "disks.#", "2"),
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.id"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.description", "Renamed Primary Disk"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.primary", "true"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.size_gigabytes", "10"),
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.1.id"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.description", "Secondary Disk"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.primary", "false"),
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.size_gigabytes", "5"),
),
},
// Test import by ID
{
ResourceName: "binarylane_server.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"password", "ssh_keys", "timeouts"},
},
// Test import by ID
{
Config: providerConfig + `
resource "binarylane_server" "test" {
name = "tf-test-server-resource"
region = "per"
image = "debian-12"
size = "std-min"
password = "` + password + `"
public_ipv4_count = 0
disks = [
{
description = "Renamed Primary Disk"
size_gigabytes = 10
},
{
description = "Secondary Disk"
size_gigabytes = 10
}
]
}
`,
},
},
})
}
func GenerateTestPassword(t *testing.T) string {
t.Helper()
pwBytes := make([]byte, 12)
@@ -26,8 +26,8 @@ func LoadBalancerResourceSchema(ctx context.Context) schema.Schema {
Attributes: map[string]schema.Attribute{
"entry_protocol": schema.StringAttribute{
Required: true,
Description: "The protocol that traffic must match for the load balancer to forward it. Valid values are \"http\" and \"https\".",
MarkdownDescription: "The protocol that traffic must match for the load balancer to forward it. Valid values are \"http\" and \"https\".",
Description: "The protocol that traffic must match for this load balancer to forward traffic according to this rule.\n\n| Value | Description |\n| ----- | ----------- |\n| http | The load balancer will forward HTTP traffic that matches this rule. |\n| https | The load balancer will forward HTTPS traffic that matches this rule. |\n\n",
MarkdownDescription: "The protocol that traffic must match for this load balancer to forward traffic according to this rule.\n\n| Value | Description |\n| ----- | ----------- |\n| http | The load balancer will forward HTTP traffic that matches this rule. |\n| https | The load balancer will forward HTTPS traffic that matches this rule. |\n\n",
Validators: []validator.String{
stringvalidator.OneOf(
"http",
+526
View File
@@ -94,6 +94,42 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
Description: "If true this will enable two daily backups for the server. `options.daily_backups` will override this value if provided. Setting this to false has no effect.\n",
MarkdownDescription: "If true this will enable two daily backups for the server. `options.daily_backups` will override this value if provided. Setting this to false has no effect.\n",
},
"disks": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"description": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "A description of this disk.",
MarkdownDescription: "A description of this disk.",
},
"id": schema.Int64Attribute{
Computed: true,
Description: "The ID of this disk.",
MarkdownDescription: "The ID of this disk.",
},
"primary": schema.BoolAttribute{
Computed: true,
Description: "A primary disk is treated differently from other disks.",
MarkdownDescription: "A primary disk is treated differently from other disks.",
},
"size_gigabytes": schema.Float64Attribute{
Required: true,
Description: "The size of the disk in GB.",
MarkdownDescription: "The size of the disk in GB.",
},
},
CustomType: DisksType{
ObjectType: types.ObjectType{
AttrTypes: DisksValue{}.AttributeTypes(ctx),
},
},
},
Optional: true,
Computed: true,
Description: "A list of the disks that are currently attached to the server.",
MarkdownDescription: "A list of the disks that are currently attached to the server.",
},
"id": schema.Int64Attribute{
Optional: true,
Computed: true,
@@ -162,6 +198,7 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
type ServerModel struct {
AdvancedFeatures AdvancedFeaturesValue `tfsdk:"advanced_features"`
Backups types.Bool `tfsdk:"backups"`
Disks types.List `tfsdk:"disks"`
Id types.Int64 `tfsdk:"id"`
Image types.String `tfsdk:"image"`
Name types.String `tfsdk:"name"`
@@ -991,3 +1028,492 @@ func (v AdvancedFeaturesValue) AttributeTypes(ctx context.Context) map[string]at
"unset_uuid": basetypes.BoolType{},
}
}
var _ basetypes.ObjectTypable = DisksType{}
type DisksType struct {
basetypes.ObjectType
}
func (t DisksType) Equal(o attr.Type) bool {
other, ok := o.(DisksType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t DisksType) String() string {
return "DisksType"
}
func (t DisksType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
descriptionAttribute, ok := attributes["description"]
if !ok {
diags.AddError(
"Attribute Missing",
`description is missing from object`)
return nil, diags
}
descriptionVal, ok := descriptionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute))
}
idAttribute, ok := attributes["id"]
if !ok {
diags.AddError(
"Attribute Missing",
`id is missing from object`)
return nil, diags
}
idVal, ok := idAttribute.(basetypes.Int64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute))
}
primaryAttribute, ok := attributes["primary"]
if !ok {
diags.AddError(
"Attribute Missing",
`primary is missing from object`)
return nil, diags
}
primaryVal, ok := primaryAttribute.(basetypes.BoolValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`primary expected to be basetypes.BoolValue, was: %T`, primaryAttribute))
}
sizeGigabytesAttribute, ok := attributes["size_gigabytes"]
if !ok {
diags.AddError(
"Attribute Missing",
`size_gigabytes is missing from object`)
return nil, diags
}
sizeGigabytesVal, ok := sizeGigabytesAttribute.(basetypes.Float64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`size_gigabytes expected to be basetypes.Float64Value, was: %T`, sizeGigabytesAttribute))
}
if diags.HasError() {
return nil, diags
}
return DisksValue{
Description: descriptionVal,
Id: idVal,
Primary: primaryVal,
SizeGigabytes: sizeGigabytesVal,
state: attr.ValueStateKnown,
}, diags
}
func NewDisksValueNull() DisksValue {
return DisksValue{
state: attr.ValueStateNull,
}
}
func NewDisksValueUnknown() DisksValue {
return DisksValue{
state: attr.ValueStateUnknown,
}
}
func NewDisksValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (DisksValue, 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 DisksValue Attribute Value",
"While creating a DisksValue value, a missing attribute value was detected. "+
"A DisksValue 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("DisksValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid DisksValue Attribute Type",
"While creating a DisksValue value, an invalid attribute value was detected. "+
"A DisksValue 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("DisksValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("DisksValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra DisksValue Attribute Value",
"While creating a DisksValue value, an extra attribute value was detected. "+
"A DisksValue 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 DisksValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewDisksValueUnknown(), diags
}
descriptionAttribute, ok := attributes["description"]
if !ok {
diags.AddError(
"Attribute Missing",
`description is missing from object`)
return NewDisksValueUnknown(), diags
}
descriptionVal, ok := descriptionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute))
}
idAttribute, ok := attributes["id"]
if !ok {
diags.AddError(
"Attribute Missing",
`id is missing from object`)
return NewDisksValueUnknown(), diags
}
idVal, ok := idAttribute.(basetypes.Int64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute))
}
primaryAttribute, ok := attributes["primary"]
if !ok {
diags.AddError(
"Attribute Missing",
`primary is missing from object`)
return NewDisksValueUnknown(), diags
}
primaryVal, ok := primaryAttribute.(basetypes.BoolValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`primary expected to be basetypes.BoolValue, was: %T`, primaryAttribute))
}
sizeGigabytesAttribute, ok := attributes["size_gigabytes"]
if !ok {
diags.AddError(
"Attribute Missing",
`size_gigabytes is missing from object`)
return NewDisksValueUnknown(), diags
}
sizeGigabytesVal, ok := sizeGigabytesAttribute.(basetypes.Float64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`size_gigabytes expected to be basetypes.Float64Value, was: %T`, sizeGigabytesAttribute))
}
if diags.HasError() {
return NewDisksValueUnknown(), diags
}
return DisksValue{
Description: descriptionVal,
Id: idVal,
Primary: primaryVal,
SizeGigabytes: sizeGigabytesVal,
state: attr.ValueStateKnown,
}, diags
}
func NewDisksValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) DisksValue {
object, diags := NewDisksValue(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("NewDisksValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t DisksType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewDisksValueNull(), 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 NewDisksValueUnknown(), nil
}
if in.IsNull() {
return NewDisksValueNull(), 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 NewDisksValueMust(DisksValue{}.AttributeTypes(ctx), attributes), nil
}
func (t DisksType) ValueType(ctx context.Context) attr.Value {
return DisksValue{}
}
var _ basetypes.ObjectValuable = DisksValue{}
type DisksValue struct {
Description basetypes.StringValue `tfsdk:"description"`
Id basetypes.Int64Value `tfsdk:"id"`
Primary basetypes.BoolValue `tfsdk:"primary"`
SizeGigabytes basetypes.Float64Value `tfsdk:"size_gigabytes"`
state attr.ValueState
}
func (v DisksValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 4)
var val tftypes.Value
var err error
attrTypes["description"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["id"] = basetypes.Int64Type{}.TerraformType(ctx)
attrTypes["primary"] = basetypes.BoolType{}.TerraformType(ctx)
attrTypes["size_gigabytes"] = basetypes.Float64Type{}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 4)
val, err = v.Description.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["description"] = val
val, err = v.Id.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["id"] = val
val, err = v.Primary.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["primary"] = val
val, err = v.SizeGigabytes.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["size_gigabytes"] = 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 DisksValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v DisksValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v DisksValue) String() string {
return "DisksValue"
}
func (v DisksValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
attributeTypes := map[string]attr.Type{
"description": basetypes.StringType{},
"id": basetypes.Int64Type{},
"primary": basetypes.BoolType{},
"size_gigabytes": basetypes.Float64Type{},
}
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{
"description": v.Description,
"id": v.Id,
"primary": v.Primary,
"size_gigabytes": v.SizeGigabytes,
})
return objVal, diags
}
func (v DisksValue) Equal(o attr.Value) bool {
other, ok := o.(DisksValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.Description.Equal(other.Description) {
return false
}
if !v.Id.Equal(other.Id) {
return false
}
if !v.Primary.Equal(other.Primary) {
return false
}
if !v.SizeGigabytes.Equal(other.SizeGigabytes) {
return false
}
return true
}
func (v DisksValue) Type(ctx context.Context) attr.Type {
return DisksType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v DisksValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"description": basetypes.StringType{},
"id": basetypes.Int64Type{},
"primary": basetypes.BoolType{},
"size_gigabytes": basetypes.Float64Type{},
}
}
+78 -35
View File
@@ -8,7 +8,7 @@
"url": "https://support.binarylane.com.au",
"email": "support@binarylane.com.au"
},
"version": "0.32.0",
"version": "0.32.1",
"x-logo": {
"url": "https://www.binarylane.com.au/res/images/binarylane/logo.png",
"backgroundColor": "#000000",
@@ -7590,7 +7590,7 @@
"forwarding_rules": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ForwardingRule"
"$ref": "#/components/schemas/ForwardingRuleRequest"
},
"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,
@@ -7601,7 +7601,7 @@
"health_check": {
"allOf": [
{
"$ref": "#/components/schemas/HealthCheck"
"$ref": "#/components/schemas/HealthCheckRequest"
}
],
"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.",
@@ -7995,24 +7995,36 @@
"type": "integer",
"description": "The ID of this disk.",
"format": "int64",
"readOnly": true
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "id"
}
},
"size_gigabytes": {
"type": "number",
"description": "The size of the disk in GB.",
"format": "double",
"readOnly": true
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "size_gigabytes"
}
},
"description": {
"type": "string",
"description": "A description of this disk.",
"nullable": true,
"readOnly": true
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "description"
}
},
"primary": {
"type": "boolean",
"description": "A primary disk is treated differently from other disks.",
"readOnly": true
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "primary"
}
}
}
},
@@ -8542,6 +8554,25 @@
}
}
},
"ForwardingRuleRequest": {
"required": [
"entry_protocol"
],
"type": "object",
"properties": {
"entry_protocol": {
"allOf": [
{
"$ref": "#/components/schemas/LoadBalancerRuleProtocol"
}
],
"description": "The protocol that traffic must match for this load balancer to forward traffic according to this rule.\n\n| Value | Description |\n| ----- | ----------- |\n| http | The load balancer will forward HTTP traffic that matches this rule. |\n| https | The load balancer will forward HTTPS traffic that matches this rule. |\n\n",
"x-oapi-codegen-extra-tags": {
"tfsdk": "entry_protocol"
}
}
}
},
"ForwardingRulesRequest": {
"required": [
"forwarding_rules"
@@ -8551,13 +8582,44 @@
"forwarding_rules": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ForwardingRule"
"$ref": "#/components/schemas/ForwardingRuleRequest"
},
"description": "The rules that control which traffic the load balancer will forward to servers in the pool."
}
}
},
"HealthCheck": {
"required": [
"path",
"protocol"
],
"type": "object",
"properties": {
"protocol": {
"allOf": [
{
"$ref": "#/components/schemas/HealthCheckProtocol"
}
],
"description": "The protocol used for the health check.\n\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"
},
"path": {
"minLength": 1,
"type": "string",
"description": "The path to the health check endpoint."
}
}
},
"HealthCheckProtocol": {
"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": [
"The health check will be performed via HTTP.",
"The health check will be performed via HTTPS.",
"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."
]
},
"HealthCheckRequest": {
"type": "object",
"properties": {
"protocol": {
@@ -8577,15 +8639,6 @@
}
}
},
"HealthCheckProtocol": {
"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": [
"The health check will be performed via HTTP.",
"The health check will be performed via HTTPS.",
"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."
]
},
"Host": {
"required": [
"display_name"
@@ -9380,21 +9433,18 @@
"type": "integer",
"description": "The ID of the load balancer.",
"format": "int64",
"readOnly": true,
"x-cli-format": 1
},
"name": {
"minLength": 1,
"type": "string",
"description": "The hostname of the load balancer.",
"readOnly": true,
"x-cli-format": 2
},
"ip": {
"minLength": 1,
"type": "string",
"description": "The IPv4 address of the load balancer.",
"readOnly": true,
"x-cli-format": 4
},
"status": {
@@ -9403,21 +9453,19 @@
"$ref": "#/components/schemas/LoadBalancerStatus"
}
],
"readOnly": true
"description": "The current status of the load balancer.\n\n| Value | Description |\n| ----- | ----------- |\n| new | The load balancer is currently being built and is not ready to accept connections. |\n| active | The load balancer is available. |\n| errored | The load balancer is in an errored state. |\n\n"
},
"created_at": {
"type": "string",
"description": "The date and time in ISO8601 format of the creation of the load balancer.",
"format": "date-time",
"readOnly": true
"format": "date-time"
},
"forwarding_rules": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ForwardingRule"
},
"description": "The rules that control which traffic the load balancer will forward to servers in the pool.",
"readOnly": true
"description": "The rules that control which traffic the load balancer will forward to servers in the pool."
},
"health_check": {
"allOf": [
@@ -9426,7 +9474,6 @@
}
],
"description": "The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer.",
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "health_check"
}
@@ -9439,7 +9486,6 @@
],
"description": "The region the load balancer is located in. If this value is null the load balancer is an 'AnyCast' load balancer.",
"nullable": true,
"readOnly": true,
"x-cli-format": 3
},
"server_ids": {
@@ -9448,8 +9494,7 @@
"type": "integer",
"format": "int64"
},
"description": "The server IDs of the servers that are currently in the load balancer pool (regardless of their current 'health').",
"readOnly": true
"description": "The server IDs of the servers that are currently in the load balancer pool (regardless of their current 'health')."
}
}
},
@@ -9467,8 +9512,7 @@
"type": "string"
},
"description": "The slugs of regions where this load balancer option is available. If this is an Anycast load balancer option this will be null.",
"nullable": true,
"readOnly": true
"nullable": true
},
"anycast": {
"type": "boolean",
@@ -9478,8 +9522,7 @@
"price_monthly": {
"type": "number",
"description": "Monthly Price in AU$.",
"format": "double",
"readOnly": true
"format": "double"
},
"price_hourly": {
"type": "number",
@@ -12290,7 +12333,7 @@
"forwarding_rules": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ForwardingRule"
"$ref": "#/components/schemas/ForwardingRuleRequest"
},
"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
@@ -12298,7 +12341,7 @@
"health_check": {
"allOf": [
{
"$ref": "#/components/schemas/HealthCheck"
"$ref": "#/components/schemas/HealthCheckRequest"
}
],
"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.",
+40 -1
View File
@@ -712,7 +712,7 @@
"name": "entry_protocol",
"string": {
"computed_optional_required": "required",
"description": "The protocol that traffic must match for the load balancer to forward it. Valid values are \"http\" and \"https\".",
"description": "The protocol that traffic must match for this load balancer to forward traffic according to this rule.\n\n| Value | Description |\n| ----- | ----------- |\n| http | The load balancer will forward HTTP traffic that matches this rule. |\n| https | The load balancer will forward HTTPS traffic that matches this rule. |\n\n",
"validators": [
{
"custom": {
@@ -1006,6 +1006,45 @@
}
]
}
},
{
"name": "disks",
"list_nested": {
"computed_optional_required": "computed_optional",
"nested_object": {
"attributes": [
{
"name": "description",
"string": {
"computed_optional_required": "computed_optional",
"description": "A description of this disk."
}
},
{
"name": "id",
"int64": {
"computed_optional_required": "computed",
"description": "The ID of this disk."
}
},
{
"name": "primary",
"bool": {
"computed_optional_required": "computed",
"description": "A primary disk is treated differently from other disks."
}
},
{
"name": "size_gigabytes",
"float64": {
"computed_optional_required": "required",
"description": "The size of the disk in GB."
}
}
]
},
"description": "A list of the disks that are currently attached to the server."
}
}
]
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "disks",
"list_nested": {
"computed_optional_required": "computed_optional",
"nested_object": {
"attributes": [
{
"name": "description",
"string": {
"computed_optional_required": "computed_optional",
"description": "A description of this disk."
}
},
{
"name": "id",
"int64": {
"computed_optional_required": "computed",
"description": "The ID of this disk."
}
},
{
"name": "primary",
"bool": {
"computed_optional_required": "computed",
"description": "A primary disk is treated differently from other disks."
}
},
{
"name": "size_gigabytes",
"float64": {
"computed_optional_required": "required",
"description": "The size of the disk in GB."
}
}
]
},
"description": "A list of the disks that are currently attached to the server."
}
}
+5 -2
View File
@@ -2,6 +2,9 @@
GENERATOR_CONFIG=$(dirname "$0")/../provider_code_spec.json
# Advanced features configuration
ADVANCED_FEATURES_CONFIG=$(dirname "$0")/data/advanced_features.json
# Server configuration
ADVANCED_FEATURES_CONFIG=$(dirname "$0")/data/server_advanced_features.json
cat <<<$(jq --tab --slurpfile adv_feat_cfg $ADVANCED_FEATURES_CONFIG '.resources[1].schema.attributes |= . + $adv_feat_cfg' $GENERATOR_CONFIG) >$GENERATOR_CONFIG
DISKS_CONFIG=$(dirname "$0")/data/server_disks.json
cat <<<$(jq --tab --slurpfile disks_cfg $DISKS_CONFIG '.resources[1].schema.attributes |= . + $disks_cfg' $GENERATOR_CONFIG) >$GENERATOR_CONFIG
+7
View File
@@ -59,6 +59,7 @@ cat <<<$(jq '.components.schemas.AdvancedFirewallRuleRequest.properties.source_a
## 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.ForwardingRuleRequest.properties.entry_protocol += {"x-oapi-codegen-extra-tags": {"tfsdk": "entry_protocol"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.LoadBalancer.properties.health_check += {"x-oapi-codegen-extra-tags": {"tfsdk": "health_check"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.HealthCheckProtocol |= del(.enum)' $OPENAPI_FILE) >$OPENAPI_FILE
@@ -141,5 +142,11 @@ cat <<<$(jq '.components.schemas.SizeType.properties.slug += {"x-oapi-codegen-ex
cat <<<$(jq '.components.schemas.SizeType.properties.name += {"x-oapi-codegen-extra-tags": {"tfsdk": "name"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.SizeType.properties.description += {"x-oapi-codegen-extra-tags": {"tfsdk": "description"}}' $OPENAPI_FILE) >$OPENAPI_FILE
## Disks
cat <<<$(jq '.components.schemas.Disk.properties.id += {"x-oapi-codegen-extra-tags": {"tfsdk": "id"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.Disk.properties.description += {"x-oapi-codegen-extra-tags": {"tfsdk": "description"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.Disk.properties.primary += {"x-oapi-codegen-extra-tags": {"tfsdk": "primary"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.Disk.properties.size_gigabytes += {"x-oapi-codegen-extra-tags": {"tfsdk": "size_gigabytes"}}' $OPENAPI_FILE) >$OPENAPI_FILE
# Edit description here because it's hard to override nested schema properties
cat <<<$(jq '.components.schemas.ForwardingRule.properties.entry_protocol.description = "The protocol that traffic must match for the load balancer to forward it. Valid values are \"http\" and \"https\"."' $OPENAPI_FILE) >$OPENAPI_FILE