Compare commits

..

7 Commits

Author SHA1 Message Date
Oscar Hermoso a0a227c88c Merge pull request #102 from an9wer/server-ipv6
6️⃣ Add "ipv6" attribute to servers
2025-09-29 09:54:22 +08:00
Oscar Hermoso f8edcd0e06 Add tests, update docs, bump OpenAPI schema version 2025-09-29 09:48:25 +08:00
an9wer 04552e0fcf Add 'ipv6' attribute to servers 2025-09-28 12:53:15 +09:30
dependabot[bot] 66fcb6f85e 🐹 Bump github.com/hashicorp/terraform-plugin-testing (#99)
Bumps the minor-patch group with 1 update: [github.com/hashicorp/terraform-plugin-testing](https://github.com/hashicorp/terraform-plugin-testing).


Updates `github.com/hashicorp/terraform-plugin-testing` from 1.13.2 to 1.13.3
- [Release notes](https://github.com/hashicorp/terraform-plugin-testing/releases)
- [Changelog](https://github.com/hashicorp/terraform-plugin-testing/blob/v1.13.3/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/terraform-plugin-testing/compare/v1.13.2...v1.13.3)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/terraform-plugin-testing
  dependency-version: 1.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 12:50:05 +08:00
dependabot[bot] 806bdebaf8 🐈 Bump goreleaser/goreleaser-action in the minor-patch group (#94)
Bumps the minor-patch group with 1 update: [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action).


Updates `goreleaser/goreleaser-action` from 6.3.0 to 6.4.0
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/9c156ee8a17a598857849441385a2041ef570552...e435ccd777264be153ace6237001ef4d979d3a7a)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 12:49:33 +08:00
dependabot[bot] dd06380c63 🐈 Bump actions/checkout from 4 to 5 (#95)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 12:49:03 +08:00
Oscar Hermoso 322a8fda80 👆 Update OpenAPI schema to 0.32.1 (#97) 2025-08-18 11:21:23 +08:00
20 changed files with 195 additions and 997 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Allow goreleaser to access older tag information.
fetch-depth: 0
@@ -38,7 +38,7 @@ jobs:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
with:
args: release --clean
env:
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
group: test
cancel-in-progress: true # would be nice if GH offered cancel-pending instead, https://github.com/orgs/community/discussions/63136
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
@@ -54,7 +54,7 @@ jobs:
# name: Unit Tests
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: actions/checkout@v5
# - uses: actions/setup-go@v5
# with:
# go-version-file: 'go.mod'
+1 -2
View File
@@ -24,8 +24,7 @@ provider_installation {
4. Build and test the provider
```sh
# Build with line numbers
go build -ldflags="-s -w" -o bin/terraform-provider-binarylane
go build -o bin/terraform-provider-binarylane
go install
cd examples/basic
terraform plan
+1 -1
View File
@@ -20,9 +20,9 @@ Planned features:
- [x] Cloud Init
- [x] Updating server properties
- [x] "Advanced Features"
- [x] IPV6
- [ ] Customised Backups
- [ ] Software
- [ ] IPV6
- [ ] Alerts
- [x] SSH Keys
- [x] Virtual Private Cloud
+3 -12
View File
@@ -33,8 +33,8 @@ 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.
- `ipv6` (Boolean) If `true` this will add a public and private IPv6 address to the server. By default, IPv6 is disabled.
- `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
@@ -44,7 +44,9 @@ data "binarylane_server" "example" {
- `permalink` (String) A randomly generated two-word identifier assigned to servers in regions that support this feature
- `port_blocking` (Boolean) Port blocking of outgoing connections for email, SSH and Remote Desktop (TCP ports 22, 25, and 3389) is enabled by default for all new servers. If this is false port blocking will be disabled. Disabling port blocking is only available to reviewed accounts.
- `private_ipv4_addresses` (List of String) The private IPv4 addresses assigned to the server.
- `private_ipv6_addresses` (List of String) The private IPv6 addresses assigned to the server.
- `public_ipv4_addresses` (List of String) The public IPv4 addresses assigned to the server.
- `public_ipv6_addresses` (List of String) The public IPv6 addresses assigned to the server.
- `region` (String) The slug of the selected region.
- `size` (String) The slug of the selected size.
- `source_and_destination_check` (Boolean) This attribute can only be set if your server also has a `vpc_id` attribute set. When enabled (which is `true` by default), your server will only be able to send or receive packets that are directly addressed to one of the IP addresses associated with the Cloud Server. Generally, this is desirable behaviour because it prevents IP conflicts and other hard-to-diagnose networking faults due to incorrect network configuration. When `source_and_destination_check` is `false`, your Cloud Server will be able to send and receive packets addressed to any server. This is typically used when you want to use your Cloud Server as a VPN endpoint, a NAT server to provide internet access, or IP forwarding.
@@ -67,14 +69,3 @@ 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.
+3 -18
View File
@@ -44,7 +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))
- `ipv6` (Boolean) If `true` this will add a public and private IPv6 address to the server. By default, IPv6 is disabled.
- `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
@@ -65,7 +65,9 @@ resource "binarylane_server" "example" {
- `password_change_supported` (Boolean) If this is true then the `password` attribute can be changed with Terraform. If this is false then the `password` attribute can only be replaced with a null/empty value, which will clear the root/administrator password allowing the password to be changed via the web console.
- `permalink` (String) A randomly generated two-word identifier assigned to servers in regions that support this feature
- `private_ipv4_addresses` (List of String) The private IPv4 addresses assigned to the server.
- `private_ipv6_addresses` (List of String) The private IPv6 addresses assigned to the server.
- `public_ipv4_addresses` (List of String) The public IPv4 addresses assigned to the server.
- `public_ipv6_addresses` (List of String) The public IPv6 addresses assigned to the server.
<a id="nestedatt--advanced_features"></a>
### Nested Schema for `advanced_features`
@@ -87,23 +89,6 @@ 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,11 +17,4 @@ 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
}]
}
+1 -1
View File
@@ -14,7 +14,7 @@ require (
github.com/hashicorp/terraform-plugin-framework-validators v0.18.0
github.com/hashicorp/terraform-plugin-go v0.28.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.13.2
github.com/hashicorp/terraform-plugin-testing v1.13.3
github.com/oapi-codegen/oapi-codegen/v2 v2.5.0
github.com/oapi-codegen/runtime v1.1.2
)
+2 -2
View File
@@ -163,8 +163,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA=
github.com/hashicorp/terraform-plugin-testing v1.13.2 h1:mSotG4Odl020vRjIenA3rggwo6Kg6XCKIwtRhYgp+/M=
github.com/hashicorp/terraform-plugin-testing v1.13.2/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE=
github.com/hashicorp/terraform-plugin-testing v1.13.3 h1:QLi/khB8Z0a5L54AfPrHukFpnwsGL8cwwswj4RZduCo=
github.com/hashicorp/terraform-plugin-testing v1.13.3/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE=
github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo=
github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
+10 -5
View File
@@ -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" tfsdk:"description"`
Description *string `json:"description"`
// Id The ID of this disk.
Id *int64 `json:"id,omitempty" tfsdk:"id"`
Id *int64 `json:"id,omitempty"`
// Primary A primary disk is treated differently from other disks.
Primary *bool `json:"primary,omitempty" tfsdk:"primary"`
Primary *bool `json:"primary,omitempty"`
// SizeGigabytes The size of the disk in GB.
SizeGigabytes *float64 `json:"size_gigabytes,omitempty" tfsdk:"size_gigabytes"`
SizeGigabytes *float64 `json:"size_gigabytes,omitempty"`
}
// DistributionFeature
@@ -1915,6 +1915,11 @@ type License struct {
// LicensedSoftware defines model for LicensedSoftware.
type LicensedSoftware struct {
// Incompatible Software that is incompatible with the server will be automatically removed at the next plan change.
// Servers may have incompatible software due to changes made by support. Software is not incompatible merely because it is disabled;
// disabled software may be retained by servers that already have it, incompatible software will be removed.
Incompatible bool `json:"incompatible"`
// LicenceCount The current licence count for the software.
LicenceCount int32 `json:"licence_count"`
@@ -2316,7 +2321,7 @@ type Resize struct {
// ChangeImage This may be left null to keep the current base image for the server. If this is provided the server disks will be destroyed and the server will be rebuilt from the selected image.
ChangeImage *ChangeImage `json:"change_image"`
// ChangeLicenses This may be left null to keep the current licenses for the server. If this is provided any licenses that are not included will be removed.
// ChangeLicenses This may be left null to keep the current licenses for the server. If this is provided any licenses that are not included will be removed. Any licences that are invalid will be removed regardless, see the documentation for `servers/{server_id}/software` for more information.
ChangeLicenses *ChangeLicenses `json:"change_licenses"`
// Options If this is null and the server has no selected size options the defaults for the size will be used. If this is null and the server has currently selected size options those will be preserved. If this is provided any option fields that are not included will be removed from the existing server.
+25 -3
View File
@@ -31,6 +31,8 @@ type serverDataModel struct {
resources.ServerModel
PublicIpv4Addresses types.List `tfsdk:"public_ipv4_addresses"`
PrivateIPv4Addresses types.List `tfsdk:"private_ipv4_addresses"`
PublicIpv6Addresses types.List `tfsdk:"public_ipv6_addresses"`
PrivateIpv6Addresses types.List `tfsdk:"private_ipv6_addresses"`
Permalink types.String `tfsdk:"permalink"`
Memory types.Int32 `tfsdk:"memory"`
Disk types.Int32 `tfsdk:"disk"`
@@ -109,6 +111,7 @@ func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
data.Region = types.StringValue(*serverResp.JSON200.Server.Region.Slug)
data.Size = types.StringValue(*serverResp.JSON200.Server.Size.Slug)
data.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
data.Ipv6 = types.BoolValue(len(serverResp.JSON200.Server.Networks.V6) > 0)
data.PortBlocking = types.BoolValue(serverResp.JSON200.Server.Networks.PortBlocking)
data.VpcId = types.Int64PointerValue(serverResp.JSON200.Server.VpcId)
data.Permalink = types.StringValue(*serverResp.JSON200.Server.Permalink)
@@ -162,12 +165,31 @@ 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)
publicIpv6Addresses := []string{}
privateIpv6Addresses := []string{}
for _, v6address := range serverResp.JSON200.Server.Networks.V6 {
if v6address.Type == "public" {
publicIpv6Addresses = append(publicIpv6Addresses, v6address.IpAddress)
} else {
privateIpv6Addresses = append(privateIpv6Addresses, v6address.IpAddress)
}
}
tfPublicIpv6Addresses, diag := types.ListValueFrom(ctx, types.StringType, publicIpv6Addresses)
diags.Append(diag...)
if diag.HasError() {
data.Disks = types.ListUnknown(data.Disks.ElementType(ctx))
data.PublicIpv6Addresses = types.ListUnknown(data.PublicIpv6Addresses.ElementType(ctx))
} else {
data.Disks = disks
data.PublicIpv6Addresses = tfPublicIpv6Addresses
}
tfPrivateIpv6Addresses, diag := types.ListValueFrom(ctx, types.StringType, privateIpv6Addresses)
diags.Append(diag...)
if diag.HasError() {
data.PrivateIpv6Addresses = types.ListUnknown(data.PrivateIpv6Addresses.ElementType(ctx))
} else {
data.PrivateIpv6Addresses = tfPrivateIpv6Addresses
}
// Get user data script
+112 -221
View File
@@ -256,6 +256,40 @@ func serverSchema(ctx context.Context) schema.Schema {
},
}
ipv6Description := "If `true` this will add a public and private IPv6 address to the server. By default, IPv6 is disabled."
s.Attributes["ipv6"] = schema.BoolAttribute{
Description: ipv6Description,
MarkdownDescription: ipv6Description,
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false), // Add default to ipv6
}
publicIpv6AddressesDescription := "The public IPv6 addresses assigned to the server."
s.Attributes["public_ipv6_addresses"] = schema.ListAttribute{
Description: publicIpv6AddressesDescription,
MarkdownDescription: publicIpv6AddressesDescription,
ElementType: types.StringType,
// read only
Optional: false,
Required: false,
Computed: true,
}
privateIpv6AddressesDescription := "The private IPv6 addresses assigned to the server."
s.Attributes["private_ipv6_addresses"] = schema.ListAttribute{
Description: privateIpv6AddressesDescription,
MarkdownDescription: privateIpv6AddressesDescription,
ElementType: types.StringType,
// read only
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
}
s.Attributes["permalink"] = schema.StringAttribute{
Description: "A randomly generated two-word identifier assigned to servers in regions that support this feature",
MarkdownDescription: "A randomly generated two-word identifier assigned to servers in regions that support this feature",
@@ -414,6 +448,16 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
)
}
if !plan.Ipv6.Equal(state.Ipv6) {
if plan.Ipv6.ValueBool() {
plan.PublicIpv6Addresses = types.ListUnknown(state.PublicIpv6Addresses.ElementType(ctx))
plan.PrivateIpv6Addresses = types.ListUnknown(state.PrivateIpv6Addresses.ElementType(ctx))
} else {
plan.PublicIpv6Addresses = types.ListNull(state.PublicIpv6Addresses.ElementType(ctx))
plan.PrivateIpv6Addresses = types.ListNull(state.PrivateIpv6Addresses.ElementType(ctx))
}
}
// Use state for unknown disk/memory values, as long as server size is the same
if (plan.Memory.IsNull() || plan.Memory.IsUnknown()) && plan.Size.Equal(state.Size) {
plan.Memory = state.Memory
@@ -421,13 +465,8 @@ 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) {
plan.AdvancedFeatures = state.AdvancedFeatures
} else {
if isAdvFeatChanged(&config.AdvancedFeatures, &state.AdvancedFeatures) {
advFeatResp, err := r.bc.client.GetServersServerIdAvailableAdvancedFeaturesWithResponse(ctx, state.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
@@ -532,6 +571,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
Ipv4Addresses: data.PublicIpv4Count.ValueInt32Pointer(),
},
Backups: data.Backups.ValueBoolPointer(),
Ipv6: data.Ipv6.ValueBoolPointer(),
}
if !data.Memory.IsNull() && !data.Memory.IsUnknown() {
@@ -589,6 +629,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
data.Region = types.StringValue(*serverResp.JSON200.Server.Region.Slug)
data.Size = types.StringValue(*serverResp.JSON200.Server.Size.Slug)
data.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
data.Ipv6 = types.BoolValue(len(serverResp.JSON200.Server.Networks.V6) > 0)
data.PortBlocking = types.BoolValue(serverResp.JSON200.Server.Networks.PortBlocking)
data.VpcId = types.Int64PointerValue(serverResp.JSON200.Server.VpcId)
data.Permalink = types.StringValue(*serverResp.JSON200.Server.Permalink)
@@ -633,14 +674,6 @@ 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() {
@@ -671,14 +704,8 @@ 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
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...)
}
diag := r.fetchServerResourceState(ctx, &data)
resp.Diagnostics.Append(diag...)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
@@ -909,6 +936,43 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
}
}
// Change ipv6
if !plan.Ipv6.Equal(state.Ipv6) {
ipv6Resp, err := r.bc.client.PostServersServerIdActionsChangeIpv6WithResponse(
ctx,
state.Id.ValueInt64(),
binarylane.PostServersServerIdActionsChangeIpv6JSONRequestBody{
Type: "change_ipv6",
Enabled: plan.Ipv6.ValueBool(),
},
)
if err != nil {
resp.Diagnostics.AddError("Error changing IPv6", err.Error())
return
}
err = r.waitForServerAction(ctx, state.Id.ValueInt64(), *ipv6Resp.JSON200.Action.Id)
if err != nil {
resp.Diagnostics.AddError("Error waiting for changing IPv6", err.Error())
return
}
state.Ipv6 = plan.Ipv6
state.PublicIpv6Addresses = plan.PublicIpv6Addresses
state.PrivateIpv6Addresses = plan.PrivateIpv6Addresses
if state.PublicIpv6Addresses.IsUnknown() || state.PrivateIpv6Addresses.IsUnknown() || listContainsUnknown(ctx, state.PublicIpv6Addresses) || listContainsUnknown(ctx, state.PrivateIpv6Addresses) {
refreshNeeded = true
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
}
// Rebuild operation
if !plan.SshKeys.Equal(state.SshKeys) || !plan.UserData.IsNull() && !plan.UserData.Equal(state.UserData) {
var rebuildReq *binarylane.PostServersServerIdActionsRebuildJSONRequestBody
@@ -1018,18 +1082,6 @@ 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() {
@@ -1254,181 +1306,6 @@ 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
@@ -1464,6 +1341,7 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
state.Memory = types.Int32Value(*serverResp.JSON200.Server.Memory)
state.Disk = types.Int32Value(*serverResp.JSON200.Server.Disk)
state.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
state.Ipv6 = types.BoolValue(len(serverResp.JSON200.Server.Networks.V6) > 0)
advFeat := *serverResp.JSON200.Server.AdvancedFeatures.EnabledAdvancedFeatures
state.AdvancedFeatures, diags = resources.NewAdvancedFeaturesValue(
@@ -1488,16 +1366,10 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
privateIpv4Addresses := []string{}
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
switch v4address.Type {
case "public":
if v4address.Type == "public" {
publicIpv4Addresses = append(publicIpv4Addresses, v4address.IpAddress)
case "private":
} else if v4address.Type == "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),
)
}
}
@@ -1519,12 +1391,31 @@ 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)
publicIpv6Addresses := []string{}
privateIpv6Addresses := []string{}
for _, v6address := range serverResp.JSON200.Server.Networks.V6 {
if v6address.Type == "public" {
publicIpv6Addresses = append(publicIpv6Addresses, v6address.IpAddress)
} else {
privateIpv6Addresses = append(privateIpv6Addresses, v6address.IpAddress)
}
}
tfPublicIpv6Addresses, diag := types.ListValueFrom(ctx, types.StringType, publicIpv6Addresses)
diags.Append(diag...)
if diag.HasError() {
state.Disks = types.ListUnknown(state.Disks.ElementType(ctx))
if diag.HasError() || tfPublicIpv6Addresses.IsNull() {
state.PublicIpv6Addresses = types.ListUnknown(state.PublicIpv6Addresses.ElementType(ctx))
} else {
state.Disks = disks
state.PublicIpv6Addresses = tfPublicIpv6Addresses
}
tfPrivateIpv6Addresses, diag := types.ListValueFrom(ctx, types.StringType, privateIpv6Addresses)
diags.Append(diag...)
if diag.HasError() || tfPrivateIpv6Addresses.IsNull() {
state.PrivateIpv6Addresses = types.ListUnknown(state.PrivateIpv6Addresses.ElementType(ctx))
} else {
state.PrivateIpv6Addresses = tfPrivateIpv6Addresses
}
return diags
+8 -89
View File
@@ -104,11 +104,8 @@ 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"),
resource.TestCheckResourceAttr("binarylane_server.test", "ipv6", "false"),
resource.TestCheckResourceAttr("binarylane_server.test", "public_ipv6_addresses.#", "0"),
// Verify data source values
resource.TestCheckResourceAttrPair("data.binarylane_server.test", "id", "binarylane_server.test", "id"),
@@ -135,11 +132,8 @@ 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"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "ipv6", "false"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "public_ipv6_addresses.#", "0"),
),
},
// Test import by ID
@@ -190,6 +184,7 @@ resource "binarylane_server" "test" {
advanced_features = {
emulated_hyperv = true
}
ipv6 = true
# source_and_destination_check = null # defaults to null
# backups = false # defaults to false
@@ -224,6 +219,9 @@ echo "Hello Whitespace" > /var/tmp/output.txt
`),
resource.TestCheckResourceAttr("binarylane_server.test", "ipv6", "true"),
resource.TestCheckResourceAttr("binarylane_server.test", "public_ipv6_addresses.#", "1"),
resource.TestCheckResourceAttr("binarylane_server.test", "private_ipv6_addresses.#", "1"),
),
},
// Change password testing (Cannot run at same time as Rebuild operation, so it has it's own test)
@@ -323,85 +321,6 @@ 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)
+7 -526
View File
@@ -94,42 +94,6 @@ 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,
@@ -141,6 +105,12 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
Description: "The slug of the selected operating system.",
MarkdownDescription: "The slug of the selected operating system.",
},
"ipv6": schema.BoolAttribute{
Optional: true,
Computed: true,
Description: "If true this will enable IPv6 for this server.",
MarkdownDescription: "If true this will enable IPv6 for this server.",
},
"name": schema.StringAttribute{
Optional: true,
Computed: true,
@@ -198,9 +168,9 @@ 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"`
Ipv6 types.Bool `tfsdk:"ipv6"`
Name types.String `tfsdk:"name"`
PortBlocking types.Bool `tfsdk:"port_blocking"`
Region types.String `tfsdk:"region"`
@@ -1028,492 +998,3 @@ 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{},
}
}
+11 -18
View File
@@ -8,7 +8,7 @@
"url": "https://support.binarylane.com.au",
"email": "support@binarylane.com.au"
},
"version": "0.32.1",
"version": "0.33.0",
"x-logo": {
"url": "https://www.binarylane.com.au/res/images/binarylane/logo.png",
"backgroundColor": "#000000",
@@ -7995,36 +7995,24 @@
"type": "integer",
"description": "The ID of this disk.",
"format": "int64",
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "id"
}
"readOnly": true
},
"size_gigabytes": {
"type": "number",
"description": "The size of the disk in GB.",
"format": "double",
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "size_gigabytes"
}
"readOnly": true
},
"description": {
"type": "string",
"description": "A description of this disk.",
"nullable": true,
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "description"
}
"readOnly": true
},
"primary": {
"type": "boolean",
"description": "A primary disk is treated differently from other disks.",
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "primary"
}
"readOnly": true
}
}
},
@@ -9348,6 +9336,7 @@
},
"LicensedSoftware": {
"required": [
"incompatible",
"licence_count",
"software"
],
@@ -9365,6 +9354,10 @@
"type": "integer",
"description": "The current licence count for the software.",
"format": "int32"
},
"incompatible": {
"type": "boolean",
"description": "Software that is incompatible with the server will be automatically removed at the next plan change.\nServers may have incompatible software due to changes made by support. Software is not incompatible merely because it is disabled;\ndisabled software may be retained by servers that already have it, incompatible software will be removed."
}
}
},
@@ -10280,7 +10273,7 @@
"$ref": "#/components/schemas/ChangeLicenses"
}
],
"description": "This may be left null to keep the current licenses for the server. If this is provided any licenses that are not included will be removed.",
"description": "This may be left null to keep the current licenses for the server. If this is provided any licenses that are not included will be removed. Any licences that are invalid will be removed regardless, see the documentation for `servers/{server_id}/software` for more information.",
"nullable": true
},
"pre_action_backup": {
+7 -39
View File
@@ -834,6 +834,13 @@
"description": "The slug of the selected operating system."
}
},
{
"name": "ipv6",
"bool": {
"computed_optional_required": "computed_optional",
"description": "If true this will enable IPv6 for this server."
}
},
{
"name": "name",
"string": {
@@ -1006,45 +1013,6 @@
}
]
}
},
{
"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."
}
}
]
}
-1
View File
@@ -40,7 +40,6 @@ resources:
default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to
disable deployment of default keys.
ignores:
- ipv6
- licenses
- links
- server
-39
View File
@@ -1,39 +0,0 @@
{
"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."
}
}
-3
View File
@@ -5,6 +5,3 @@ GENERATOR_CONFIG=$(dirname "$0")/../provider_code_spec.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
-6
View File
@@ -142,11 +142,5 @@ 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