forked from mirrors/terraform-provider-binarylane
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 725fa71248 | |||
| 6970691546 | |||
| d8ca224107 | |||
| d168435ed6 |
@@ -29,10 +29,11 @@ data "binarylane_server" "example" {
|
||||
|
||||
- `advanced_features` (Object) (see [below for nested schema](#nestedatt--advanced_features))
|
||||
- `backups` (Boolean) If `true` this will enable two daily backups for the server. By default, backups are disabled.
|
||||
- `disk` (Number) The total storage in GB for this server. Leave null to accept the default for the size Valid values:
|
||||
- `disk` (Number) The total storage in GB for this server. Leave null to accept the default for the size. Valid values:
|
||||
- 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) The disks attached to this server. Leave null to let Binary Lane manage the server's disks. If specified, every disk must be listed, including the primary: exactly one entry must have `primary = true`, and its `id` and `description` are assigned by Binary Lane. (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:
|
||||
@@ -71,3 +72,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 label for this disk. Assigned by Binary Lane for the primary disk, so must be left null when `primary = true`. Changing this will destroy the disk and create a new one, losing any data on it. Disks that share a description are matched in the order they are listed.
|
||||
- `id` (Number) The ID of this disk.
|
||||
- `primary` (Boolean) If `true` this is the primary disk, which the operating system is installed on. Exactly one disk must be the primary.
|
||||
- `size_gigabytes` (Number) The size of the disk in GB. The sum across all disks cannot exceed `disk`, and any remainder is left unallocated.
|
||||
|
||||
@@ -20,6 +20,14 @@ resource "binarylane_server" "example" {
|
||||
size = "std-min" # 1 VPCU, 1 GB Memory, 20 GB NVME Storage, 1000 GB Data Transfer
|
||||
public_ipv4_count = 1
|
||||
|
||||
# To manage multiple disks, every disk must be listed, including the primary
|
||||
# disk = 40
|
||||
# disks = [
|
||||
# { primary = true, size_gigabytes = 20 },
|
||||
# { description = "data1", size_gigabytes = 10 },
|
||||
# { description = "data2", size_gigabytes = 10 },
|
||||
# ]
|
||||
|
||||
# Accepts a cloud-init script or cloud-config YAML file to configure the server
|
||||
# See more: https://cloudinit.readthedocs.io/en/latest/explanation/format.html#user-data-script
|
||||
user_data = file("./init.sh")
|
||||
@@ -40,10 +48,11 @@ resource "binarylane_server" "example" {
|
||||
|
||||
- `advanced_features` (Attributes) (see [below for nested schema](#nestedatt--advanced_features))
|
||||
- `backups` (Boolean) If `true` this will enable two daily backups for the server. By default, backups are disabled.
|
||||
- `disk` (Number) The total storage in GB for this server. Leave null to accept the default for the size Valid values:
|
||||
- `disk` (Number) The total storage in GB for this server. Leave null to accept the default for the size. Valid values:
|
||||
- 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) The disks attached to this server. Leave null to let Binary Lane manage the server's disks. If specified, every disk must be listed, including the primary: exactly one entry must have `primary = true`, and its `id` and `description` are assigned by Binary Lane. (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
|
||||
@@ -91,6 +100,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. The sum across all disks cannot exceed `disk`, and any remainder is left unallocated.
|
||||
|
||||
Optional:
|
||||
|
||||
- `description` (String) A label for this disk. Assigned by Binary Lane for the primary disk, so must be left null when `primary = true`. Changing this will destroy the disk and create a new one, losing any data on it. Disks that share a description are matched in the order they are listed.
|
||||
- `primary` (Boolean) If `true` this is the primary disk, which the operating system is installed on. Exactly one disk must be the primary.
|
||||
|
||||
Read-Only:
|
||||
|
||||
- `id` (Number) The ID of this disk.
|
||||
|
||||
|
||||
<a id="nestedatt--timeouts"></a>
|
||||
### Nested Schema for `timeouts`
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ resource "binarylane_server" "example" {
|
||||
size = "std-min" # 1 VPCU, 1 GB Memory, 20 GB NVME Storage, 1000 GB Data Transfer
|
||||
public_ipv4_count = 1
|
||||
|
||||
# To manage multiple disks, every disk must be listed, including the primary
|
||||
# disk = 40
|
||||
# disks = [
|
||||
# { primary = true, size_gigabytes = 20 },
|
||||
# { description = "data1", size_gigabytes = 10 },
|
||||
# { description = "data2", size_gigabytes = 10 },
|
||||
# ]
|
||||
|
||||
# Accepts a cloud-init script or cloud-config YAML file to configure the server
|
||||
# See more: https://cloudinit.readthedocs.io/en/latest/explanation/format.html#user-data-script
|
||||
user_data = file("./init.sh")
|
||||
|
||||
@@ -31,7 +31,7 @@ require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
|
||||
@@ -34,8 +34,8 @@ github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
|
||||
@@ -4106,6 +4106,22 @@ func NewGetLoadBalancersRequest(server string, params *GetLoadBalancersParams) (
|
||||
if params != nil {
|
||||
queryValues := queryURL.Query()
|
||||
|
||||
if params.Name != nil {
|
||||
|
||||
if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil {
|
||||
return nil, err
|
||||
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for k, v := range parsed {
|
||||
for _, v2 := range v {
|
||||
queryValues.Add(k, v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if params.Page != nil {
|
||||
|
||||
if queryFrag, err := runtime.StyleParamWithOptions("form", true, "page", *params.Page, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int32"}); err != nil {
|
||||
|
||||
@@ -2164,16 +2164,16 @@ type DisableSelinuxType string
|
||||
// Disk defines model for Disk.
|
||||
type Disk struct {
|
||||
// Description A description of this disk.
|
||||
Description *string `json:"description,omitempty"`
|
||||
Description *string `json:"description,omitempty" tfsdk:"description"`
|
||||
|
||||
// Id The ID of this disk.
|
||||
Id int64 `json:"id"`
|
||||
Id int64 `json:"id" tfsdk:"id"`
|
||||
|
||||
// Primary A primary disk is treated differently from other disks.
|
||||
Primary bool `json:"primary"`
|
||||
Primary bool `json:"primary" tfsdk:"primary"`
|
||||
|
||||
// SizeGigabytes The size of the disk in GB.
|
||||
SizeGigabytes float64 `json:"size_gigabytes"`
|
||||
SizeGigabytes float64 `json:"size_gigabytes" tfsdk:"size_gigabytes"`
|
||||
}
|
||||
|
||||
// DistributionFeature
|
||||
@@ -4093,7 +4093,7 @@ type GetAccountKeysParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4108,7 +4108,7 @@ type GetActionsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4120,7 +4120,7 @@ type GetCustomersMyInvoicesParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4129,7 +4129,7 @@ type GetDataUsagesCurrentParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4138,7 +4138,7 @@ type GetDomainsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4183,7 +4183,7 @@ type GetDomainsDomainNameRecordsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4234,7 +4234,7 @@ type GetImagesParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4246,10 +4246,13 @@ type GetImagesImageIdOrSlugParamsImageIdOrSlug1 = string
|
||||
|
||||
// GetLoadBalancersParams defines parameters for GetLoadBalancers.
|
||||
type GetLoadBalancersParams struct {
|
||||
// Name Providing a name restricts the results to load balancers with this name (case insensitive).
|
||||
Name *string `form:"name,omitempty" json:"name,omitempty"`
|
||||
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4276,7 +4279,7 @@ type GetRegionsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4285,7 +4288,7 @@ type GetReverseNamesIpv6Params struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4315,7 +4318,7 @@ type GetSamplesetsServerIdParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4342,7 +4345,7 @@ type GetServersParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4360,7 +4363,7 @@ type GetServersServerIdActionsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4372,7 +4375,7 @@ type GetServersServerIdBackupsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4384,7 +4387,7 @@ type GetServersServerIdKernelsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4393,7 +4396,7 @@ type GetServersServerIdSnapshotsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4402,7 +4405,7 @@ type GetServersServerIdSoftwareParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4417,7 +4420,7 @@ type GetSizesParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4426,7 +4429,7 @@ type GetSoftwareParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4435,7 +4438,7 @@ type GetSoftwareOperatingSystemOperatingSystemIdOrSlugParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4450,7 +4453,7 @@ type GetVpcsParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4480,7 +4483,7 @@ type GetVpcsVpcIdMembersParams struct {
|
||||
// Page The selected page. Page numbering starts at 1
|
||||
Page *int32 `form:"page,omitempty" json:"page,omitempty"`
|
||||
|
||||
// PerPage The number of results to show per page.
|
||||
// PerPage The number of results to show per page. Use 0 to return only the total count.
|
||||
PerPage *int32 `form:"per_page,omitempty" json:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -140,11 +140,10 @@ func loadBalancerSchema(ctx context.Context) schema.Schema {
|
||||
func (r *loadBalancerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = loadBalancerSchema(ctx)
|
||||
|
||||
resp.Schema.Attributes["timeouts"] =
|
||||
timeouts.Attributes(ctx, timeouts.Opts{
|
||||
Create: true,
|
||||
Update: true,
|
||||
})
|
||||
resp.Schema.Attributes["timeouts"] = timeouts.Attributes(ctx, timeouts.Opts{
|
||||
Create: true,
|
||||
Update: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
@@ -404,49 +403,35 @@ func (r *loadBalancerResource) ImportState(
|
||||
}
|
||||
|
||||
// Import by name
|
||||
var page int32 = 1
|
||||
perPage := int32(200)
|
||||
var loadBalancer binarylane.LoadBalancer
|
||||
var nextPage bool = true
|
||||
|
||||
for nextPage { // Need to paginate because the API does not support filtering by name
|
||||
params := binarylane.GetLoadBalancersParams{
|
||||
Page: &page,
|
||||
PerPage: &perPage,
|
||||
}
|
||||
|
||||
lbResp, err := r.bc.client.GetLoadBalancersWithResponse(ctx, ¶ms)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(fmt.Sprintf("Error getting load balancer for import: name=%s", req.ID), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if lbResp.StatusCode() != http.StatusOK {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected HTTP status code getting load balancer for import",
|
||||
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), req.ID,
|
||||
lbResp.Body))
|
||||
return
|
||||
}
|
||||
|
||||
loadBalancers := lbResp.JSON200.LoadBalancers
|
||||
for _, lb := range loadBalancers {
|
||||
if lb.Name == req.ID {
|
||||
loadBalancer = lb
|
||||
nextPage = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if lbResp.JSON200.Links == nil || lbResp.JSON200.Links.Pages.Next == nil {
|
||||
nextPage = false
|
||||
break
|
||||
}
|
||||
|
||||
page++
|
||||
params := binarylane.GetLoadBalancersParams{
|
||||
Name: &req.ID,
|
||||
}
|
||||
|
||||
diags := resp.State.SetAttribute(ctx, path.Root("id"), loadBalancer.Id)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
lbResp, err := r.bc.client.GetLoadBalancersWithResponse(ctx, ¶ms)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(fmt.Sprintf("Error getting load balancer for import: name=%s", req.ID), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if lbResp.StatusCode() != http.StatusOK {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected HTTP status code getting load balancer for import",
|
||||
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), req.ID,
|
||||
lbResp.Body))
|
||||
return
|
||||
}
|
||||
|
||||
for _, lb := range lbResp.JSON200.LoadBalancers {
|
||||
if lb.Name == req.ID {
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), lb.Id)...)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.Diagnostics.AddError(
|
||||
"Load balancer not found",
|
||||
fmt.Sprintf("No load balancer named %q was found to import.", req.ID),
|
||||
)
|
||||
}
|
||||
|
||||
func setLoadBalancerModelState(ctx context.Context, data *loadBalancerDataModel, lb *binarylane.LoadBalancer) diag.Diagnostics {
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestLoadBalancerResource(t *testing.T) {
|
||||
resource "binarylane_server" "test" {
|
||||
count = 2
|
||||
name = "tf-test-lb-server-${count.index}"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
password = "` + password + `"
|
||||
@@ -94,7 +94,7 @@ data "binarylane_load_balancer" "test" {
|
||||
resource "binarylane_server" "test" {
|
||||
count = 2
|
||||
name = "tf-test-lb-server-${count.index}"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
password = "` + password + `"
|
||||
|
||||
@@ -15,6 +15,24 @@ provider "binarylane" {
|
||||
# End of provider config
|
||||
|
||||
`
|
||||
|
||||
// planOnlyProviderConfig configures the provider with a placeholder token, for tests that
|
||||
// only ever plan and so never reach the API. providerConfig leaves the token to
|
||||
// BINARYLANE_API_TOKEN, which those tests would otherwise need set to run at all.
|
||||
planOnlyProviderConfig = `
|
||||
provider "binarylane" {
|
||||
api_token = "placeholder-never-used"
|
||||
}
|
||||
|
||||
# End of provider config
|
||||
|
||||
`
|
||||
|
||||
// testRegion is the BinaryLane region acceptance tests create servers in.
|
||||
// TEMPORARY: set to "mel" because "per" is intermittently returning "Unable to find a
|
||||
// suitable host for the requested server configuration". Revert to "per" once that's
|
||||
// resolved.
|
||||
testRegion = "mel"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -118,6 +118,8 @@ func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
|
||||
data.Permalink = types.StringValue(*serverResp.JSON200.Server.Permalink)
|
||||
data.Memory = types.Int32Value(serverResp.JSON200.Server.Memory)
|
||||
data.Disk = types.Int32Value(serverResp.JSON200.Server.Disk)
|
||||
data.Disks, diag = serverDisksToList(ctx, serverResp.JSON200.Server.Disks, data.Disks)
|
||||
diags.Append(diag...)
|
||||
data.SourceAndDestinationCheck = types.BoolPointerValue(serverResp.JSON200.Server.Networks.SourceAndDestinationCheck)
|
||||
data.SeparatePrivateNetworkInterface = types.BoolPointerValue(serverResp.JSON200.Server.Networks.SeparatePrivateNetworkInterface)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestServerFirewallRulesResource(t *testing.T) {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-fw-rules"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
password = "` + password + `"
|
||||
@@ -90,7 +90,7 @@ data "binarylane_server_firewall_rules" "test" {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-fw-rules"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
password = "` + password + `"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -355,8 +357,8 @@ func serverSchema(ctx context.Context) schema.Schema {
|
||||
},
|
||||
}
|
||||
|
||||
diskDescription := `The total storage in GB for this server. Leave null to accept the default for the size`
|
||||
diskValidValues := "Valid values must be a multiple of 5. If the value is greater than 60 GB, it must be a multiple of 10. " +
|
||||
diskDescription := "The total storage in GB for this server. Leave null to accept the default for the size."
|
||||
diskValidValues := " Valid values must be a multiple of 5. If the value is greater than 60 GB, it must be a multiple of 10. " +
|
||||
"if the value is greater than 200 GB, it must be a multiple of 100. "
|
||||
diskValidValuesMarkdown := ` Valid values:
|
||||
- must be a multiple of 5
|
||||
@@ -423,6 +425,21 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
|
||||
return
|
||||
}
|
||||
|
||||
// `disks` includes the primary, so more than one element means an additional disk.
|
||||
warnDisksMayRaceUserData := func() {
|
||||
if plan.UserData.ValueString() == "" || plan.Disks.IsNull() || plan.Disks.IsUnknown() || len(plan.Disks.Elements()) < 2 {
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddAttributeWarning(
|
||||
path.Root("disks"),
|
||||
"Additional disks may race with user_data on first boot",
|
||||
"Additional disks are attached after the server boots into a fresh OS install (on create, or a "+
|
||||
"rebuild triggered by this plan), which can interrupt `user_data` (cloud-init) while it is "+
|
||||
"still running on first boot. cloud-init only runs user_data once per instance, so any "+
|
||||
"scripts that did not complete before the disk operations will not re-run on the next boot.",
|
||||
)
|
||||
}
|
||||
|
||||
if plan.SourceAndDestinationCheck.IsUnknown() {
|
||||
if plan.VpcId.IsNull() {
|
||||
plan.SourceAndDestinationCheck = types.BoolNull()
|
||||
@@ -451,6 +468,8 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
|
||||
}
|
||||
|
||||
if req.State.Raw.IsNull() {
|
||||
warnDisksMayRaceUserData()
|
||||
|
||||
// Creation plan, no further modification needed
|
||||
return
|
||||
}
|
||||
@@ -498,6 +517,7 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
|
||||
strings.Join(attrsRequiringRebuild, ", "),
|
||||
),
|
||||
)
|
||||
warnDisksMayRaceUserData()
|
||||
}
|
||||
|
||||
if !plan.Ipv6.Equal(state.Ipv6) {
|
||||
@@ -508,6 +528,11 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
|
||||
plan.PublicIpv6Addresses = types.ListNull(state.PublicIpv6Addresses.ElementType(ctx))
|
||||
plan.PrivateIpv6Addresses = types.ListNull(state.PrivateIpv6Addresses.ElementType(ctx))
|
||||
}
|
||||
} else {
|
||||
// Computed list attributes aren't carried forward from state by default, so without
|
||||
// this these would plan as unknown on every apply, even when ipv6 didn't change.
|
||||
plan.PublicIpv6Addresses = state.PublicIpv6Addresses
|
||||
plan.PrivateIpv6Addresses = state.PrivateIpv6Addresses
|
||||
}
|
||||
|
||||
// Use state for unknown disk/memory values, as long as server size is the same
|
||||
@@ -518,6 +543,32 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
|
||||
plan.Disk = state.Disk
|
||||
}
|
||||
|
||||
// A configured `disks` is planned by the attribute's own plan modifier, which matches each
|
||||
// disk with the one it corresponds to in prior state. Left unset, the disks are Binary Lane's
|
||||
// to manage, and what happens to them depends on `disk` — resolved just above, after that
|
||||
// plan modifier has already run.
|
||||
if config.Disks.IsNull() || config.Disks.IsUnknown() {
|
||||
// Computed list attributes aren't carried forward from state by default, so without this
|
||||
// `disks` would plan as unknown on every apply, even when nothing about it changed.
|
||||
plan.Disks = state.Disks
|
||||
|
||||
if state.Disks.IsNull() || state.Disks.IsUnknown() {
|
||||
plan.Disks = types.ListUnknown(resources.DisksValue{}.Type(ctx))
|
||||
} else if len(state.Disks.Elements()) == 1 && !plan.Disk.Equal(state.Disk) {
|
||||
// A resize resizes the primary disk to fill the new total, but only when it is the
|
||||
// server's only disk: with an additional disk it leaves every disk alone.
|
||||
plan.Disks = types.ListUnknown(resources.DisksValue{}.Type(ctx))
|
||||
|
||||
statePrimary, _, diags := splitDisks(ctx, state.Disks)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if statePrimary != nil && !plan.Disk.IsUnknown() &&
|
||||
float64(plan.Disk.ValueInt32()) < statePrimary.SizeGigabytes {
|
||||
resp.Diagnostics.Append(resources.DiskShrinkWarning(path.Root("disk"), "The primary disk",
|
||||
int64(statePrimary.SizeGigabytes), int64(plan.Disk.ValueInt32())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isAdvFeatChanged(&config.AdvancedFeatures, &state.AdvancedFeatures) {
|
||||
advFeatResp, err := r.bc.client.GetServersServerIdAvailableAdvancedFeaturesWithResponse(ctx, state.Id.ValueInt64())
|
||||
if err != nil {
|
||||
@@ -579,6 +630,11 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Computed object attributes aren't carried forward from state by default, so without
|
||||
// this `advanced_features` would plan as unknown on every apply, even when none of its
|
||||
// writable fields changed.
|
||||
plan.AdvancedFeatures = state.AdvancedFeatures
|
||||
}
|
||||
|
||||
// Save data into Terraform state
|
||||
@@ -627,6 +683,14 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
|
||||
SeparatePrivateNetworkInterface: data.SeparatePrivateNetworkInterface.ValueBoolPointer(),
|
||||
}
|
||||
|
||||
// The planned `disks` is unknown when it isn't configured, so the disks to create are read
|
||||
// from config, which is always known.
|
||||
configPrimary, configAdditional, diskDiags := splitDisks(ctx, config.Disks)
|
||||
resp.Diagnostics.Append(diskDiags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if !data.Memory.IsNull() && !data.Memory.IsUnknown() {
|
||||
body.Options.Memory = data.Memory.ValueInt32Pointer()
|
||||
}
|
||||
@@ -691,6 +755,8 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
|
||||
data.PasswordChangeSupported = types.BoolValue(serverResp.JSON200.Server.PasswordChangeSupported)
|
||||
data.Memory = types.Int32Value(serverResp.JSON200.Server.Memory)
|
||||
data.Disk = types.Int32Value(serverResp.JSON200.Server.Disk)
|
||||
data.Disks, diags = serverDisksToList(ctx, serverResp.JSON200.Server.Disks, config.Disks)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
plannedSourceDestCheck := data.SourceAndDestinationCheck
|
||||
serverRespSourceDestCheck := types.BoolPointerValue(serverResp.JSON200.Server.Networks.SourceAndDestinationCheck)
|
||||
data.SourceAndDestinationCheck = serverRespSourceDestCheck
|
||||
@@ -770,6 +836,25 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
|
||||
data.SourceAndDestinationCheck = plannedSourceDestCheck
|
||||
}
|
||||
|
||||
// Carve the additional disks out of the server's total: it is created with a single primary
|
||||
// disk spanning all of it, so shrink that to make room, as Binary Lane's own UI does
|
||||
// (https://support.binarylane.com.au/support/solutions/articles/11000133468). If cloud-init's
|
||||
// growpart has already expanded the filesystem to fill the original primary, this can
|
||||
// corrupt it, and a disk can only be resized in place.
|
||||
if configPrimary != nil {
|
||||
finalDisks, err := r.reconcileServerDisks(
|
||||
ctx, data.Id.ValueInt64(), serverResp.JSON200.Server.Disks,
|
||||
configPrimary.SizeGigabytes, configAdditional, true,
|
||||
)
|
||||
data.Disks, diags = serverDisksToList(ctx, finalDisks, config.Disks)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating additional disks", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -817,7 +902,6 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
|
||||
}
|
||||
|
||||
func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var diag diag.Diagnostics
|
||||
var config, plan, state serverResourceModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
@@ -960,28 +1044,68 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
|
||||
}
|
||||
}
|
||||
|
||||
// The disks to reconcile come from config, which describes every disk when `disks` is set.
|
||||
// Left unset, the disks are Binary Lane's to manage and there is nothing to reconcile.
|
||||
configPrimary, configAdditional, diags := splitDisks(ctx, config.Disks)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
planDiskKnown := !plan.Disk.IsNull() && !plan.Disk.IsUnknown()
|
||||
diskChanged := planDiskKnown && !plan.Disk.Equal(state.Disk)
|
||||
sizeChanged := !plan.Size.Equal(state.Size)
|
||||
|
||||
// A `disk` or `size` change resizes the server, which can resize the primary disk with it, so
|
||||
// the disks are reconciled back to config after one of those too.
|
||||
reconcileDisks := configPrimary != nil &&
|
||||
(!plan.Disks.Equal(state.Disks) || diskChanged || sizeChanged)
|
||||
|
||||
persistDisks := func(disks []binarylane.Disk) {
|
||||
if disks == nil {
|
||||
return
|
||||
}
|
||||
var diags diag.Diagnostics
|
||||
state.Disks, diags = serverDisksToList(ctx, disks, plan.Disks)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
// Free space before the resize below, which is where space to grow or add a disk comes from.
|
||||
var currentDisks []binarylane.Disk
|
||||
if reconcileDisks {
|
||||
var err error
|
||||
currentDisks, err = r.reconcileServerDisks(
|
||||
ctx, state.Id.ValueInt64(), nil, configPrimary.SizeGigabytes, configAdditional, false)
|
||||
persistDisks(currentDisks)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reconciling server disks", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Resize operation
|
||||
if !plan.Size.Equal(state.Size) ||
|
||||
!plan.Memory.IsNull() && !plan.Memory.IsUnknown() && !plan.Memory.Equal(state.Memory) ||
|
||||
!plan.Disk.IsNull() && !plan.Disk.IsUnknown() && !plan.Disk.Equal(state.Disk) ||
|
||||
memoryChanged := !plan.Memory.IsNull() && !plan.Memory.IsUnknown() && !plan.Memory.Equal(state.Memory)
|
||||
if sizeChanged || memoryChanged ||
|
||||
!plan.Image.Equal(state.Image) ||
|
||||
!plan.PublicIpv4Count.Equal(state.PublicIpv4Count) {
|
||||
!plan.PublicIpv4Count.Equal(state.PublicIpv4Count) ||
|
||||
diskChanged {
|
||||
|
||||
resizeReq := &binarylane.PostServersServerIdActionsResizeJSONRequestBody{
|
||||
Type: "resize",
|
||||
Options: &binarylane.ChangeSizeOptionsRequest{},
|
||||
}
|
||||
|
||||
if !plan.Size.Equal(state.Size) ||
|
||||
!plan.Memory.IsNull() && !plan.Memory.IsUnknown() && !plan.Memory.Equal(state.Memory) ||
|
||||
!plan.Disk.IsNull() && !plan.Disk.IsUnknown() && !plan.Disk.Equal(state.Disk) {
|
||||
|
||||
resizeReq.Size = plan.Size.ValueStringPointer()
|
||||
if sizeChanged || memoryChanged || diskChanged {
|
||||
if sizeChanged {
|
||||
resizeReq.Size = plan.Size.ValueStringPointer()
|
||||
}
|
||||
if !plan.Memory.IsUnknown() && !plan.Memory.IsNull() {
|
||||
resizeReq.Options.Memory = plan.Memory.ValueInt32Pointer()
|
||||
}
|
||||
if !plan.Disk.IsNull() && !plan.Disk.IsUnknown() {
|
||||
resizeReq.Options.Disk = plan.Disk.ValueInt32Pointer()
|
||||
if planDiskKnown {
|
||||
disk := plan.Disk.ValueInt32()
|
||||
resizeReq.Options.Disk = &disk
|
||||
}
|
||||
state.Size = plan.Size
|
||||
state.Memory = plan.Memory
|
||||
@@ -1042,6 +1166,9 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
|
||||
return
|
||||
}
|
||||
|
||||
// The resize can resize the primary disk, so anything read before it is stale.
|
||||
currentDisks = nil
|
||||
|
||||
if state.PublicIpv4Addresses.IsUnknown() || listContainsUnknown(ctx, state.PublicIpv4Addresses) || state.Memory.IsNull() || state.Memory.IsUnknown() {
|
||||
refreshNeeded = true
|
||||
}
|
||||
@@ -1053,6 +1180,18 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
|
||||
}
|
||||
}
|
||||
|
||||
// Grow the primary back to its target size, then grow and add the rest, now that the resize
|
||||
// has made room. currentDisks is nil if that resize ran, so they are re-read.
|
||||
if reconcileDisks {
|
||||
finalDisks, err := r.reconcileServerDisks(
|
||||
ctx, state.Id.ValueInt64(), currentDisks, configPrimary.SizeGigabytes, configAdditional, true)
|
||||
persistDisks(finalDisks)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reconciling server disks", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Change IPv6
|
||||
if !plan.Ipv6.Equal(state.Ipv6) {
|
||||
ipv6Resp, err := r.bc.client.PostServersServerIdActionsChangeIpv6WithResponse(
|
||||
@@ -1174,7 +1313,6 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
|
||||
resp.Diagnostics.AddError("Error updating advanced features", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.Append(diag...)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
@@ -1508,6 +1646,9 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
|
||||
state.SeparatePrivateNetworkInterface = types.BoolPointerValue(serverResp.JSON200.Server.Networks.SeparatePrivateNetworkInterface)
|
||||
state.Memory = types.Int32Value(serverResp.JSON200.Server.Memory)
|
||||
state.Disk = types.Int32Value(serverResp.JSON200.Server.Disk)
|
||||
var readDiags diag.Diagnostics
|
||||
state.Disks, readDiags = serverDisksToList(ctx, serverResp.JSON200.Server.Disks, state.Disks)
|
||||
diags.Append(readDiags...)
|
||||
state.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
|
||||
state.Ipv6 = types.BoolValue(len(serverResp.JSON200.Server.Networks.V6) > 0)
|
||||
|
||||
@@ -1706,3 +1847,290 @@ func (r *serverResource) updateAdvancedFeatures(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// serverDisksToList converts the server's disks into the `disks` attribute value, ordered to
|
||||
// match `order` — the value Terraform already holds, either planned or from prior state. A
|
||||
// list's elements must stay where they were planned, so a configuration can list disks in any
|
||||
// order as long as this keeps to it.
|
||||
func serverDisksToList(ctx context.Context, apiDisks []binarylane.Disk, order types.List) (types.List, diag.Diagnostics) {
|
||||
var diags diag.Diagnostics
|
||||
|
||||
var ordered []resources.DisksValue
|
||||
if !order.IsNull() && !order.IsUnknown() {
|
||||
diags.Append(order.ElementsAs(ctx, &ordered, true)...)
|
||||
}
|
||||
orderKeys := resources.DiskKeyer{}
|
||||
rank := make(map[resources.DiskKey]int, len(ordered))
|
||||
for i, d := range ordered {
|
||||
rank[orderKeys.Of(d.Primary.ValueBool(), d.Description.ValueString())] = i
|
||||
}
|
||||
|
||||
primaryFirst := func(d binarylane.Disk) int {
|
||||
if d.Primary {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// Sort into a stable order first, to key the disks in and to fall back on.
|
||||
disks := slices.Clone(apiDisks)
|
||||
slices.SortStableFunc(disks, func(a, b binarylane.Disk) int {
|
||||
return cmp.Or(cmp.Compare(primaryFirst(a), primaryFirst(b)), cmp.Compare(a.Id, b.Id))
|
||||
})
|
||||
|
||||
// Disks the order doesn't mention keep that order, after those it does.
|
||||
diskKeys := resources.DiskKeyer{}
|
||||
rankById := make(map[int64]int, len(disks))
|
||||
for _, d := range disks {
|
||||
r, ranked := rank[diskKeys.Of(d.Primary, diskDescription(d))]
|
||||
if !ranked {
|
||||
r = len(rank)
|
||||
}
|
||||
rankById[d.Id] = r
|
||||
}
|
||||
slices.SortStableFunc(disks, func(a, b binarylane.Disk) int {
|
||||
return cmp.Compare(rankById[a.Id], rankById[b.Id])
|
||||
})
|
||||
|
||||
list, listDiags := types.ListValueFrom(ctx, resources.DisksValue{}.Type(ctx), disks)
|
||||
diags.Append(listDiags...)
|
||||
return list, diags
|
||||
}
|
||||
|
||||
// splitDisks divides a `disks` list into its primary entry and the additional disks, as the API's
|
||||
// own type: binarylane.Disk carries tfsdk tags for exactly the attributes `disks` has, so the
|
||||
// values reflect straight into it. The attributes Binary Lane assigns are null in a configuration
|
||||
// and come back as their zero value. primary is nil when the list is null or unknown, or has no
|
||||
// entry with `primary = true`.
|
||||
func splitDisks(ctx context.Context, list types.List) (primary *binarylane.Disk, additional []binarylane.Disk, diags diag.Diagnostics) {
|
||||
if list.IsNull() || list.IsUnknown() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
var disks []binarylane.Disk
|
||||
diags = list.ElementsAs(ctx, &disks, true)
|
||||
if diags.HasError() {
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
for i, d := range disks {
|
||||
if d.Primary && primary == nil {
|
||||
primary = &disks[i]
|
||||
continue
|
||||
}
|
||||
additional = append(additional, d)
|
||||
}
|
||||
return primary, additional, diags
|
||||
}
|
||||
|
||||
// reconcileServerDisks brings the server's disks in line with desiredPrimary and
|
||||
// desiredAdditional, and returns them as they stand afterwards, re-read if anything changed —
|
||||
// including after a failure, since partially applied disk changes are not rolled back. current
|
||||
// is the disks as last read, or nil to read them here.
|
||||
//
|
||||
// Deleting and shrinking only free space, so they always run. Growing the primary, growing a
|
||||
// disk and adding one all claim space the server's total may not have yet, so they wait for
|
||||
// growAndAdd: Update calls this before its resize action with growAndAdd false and after it
|
||||
// with true, while Create has no resize of its own and always passes true.
|
||||
func (r *serverResource) reconcileServerDisks(
|
||||
ctx context.Context,
|
||||
serverId int64,
|
||||
current []binarylane.Disk,
|
||||
desiredPrimary float64,
|
||||
desiredAdditional []binarylane.Disk,
|
||||
growAndAdd bool,
|
||||
) ([]binarylane.Disk, error) {
|
||||
if current == nil {
|
||||
var err error
|
||||
if current, err = r.fetchServerDisks(ctx, serverId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
mutated, err := r.applyDiskChanges(ctx, serverId, current, desiredPrimary, desiredAdditional, growAndAdd)
|
||||
if !mutated {
|
||||
return current, err
|
||||
}
|
||||
disks, fetchErr := r.fetchServerDisks(ctx, serverId)
|
||||
if fetchErr != nil {
|
||||
return current, errors.Join(err, fetchErr)
|
||||
}
|
||||
return disks, err
|
||||
}
|
||||
|
||||
// applyDiskChanges makes the changes reconcileServerDisks needs, reporting whether it attempted
|
||||
// any.
|
||||
func (r *serverResource) applyDiskChanges(
|
||||
ctx context.Context,
|
||||
serverId int64,
|
||||
current []binarylane.Disk,
|
||||
desiredPrimary float64,
|
||||
desiredAdditional []binarylane.Disk,
|
||||
growAndAdd bool,
|
||||
) (mutated bool, err error) {
|
||||
desiredKeys := make([]resources.DiskKey, len(desiredAdditional))
|
||||
desiredSizes := make(map[resources.DiskKey]float64, len(desiredAdditional))
|
||||
keyer := resources.DiskKeyer{}
|
||||
for i, d := range desiredAdditional {
|
||||
desiredKeys[i] = keyer.Of(false, diskDescription(d))
|
||||
desiredSizes[desiredKeys[i]] = d.SizeGigabytes
|
||||
}
|
||||
|
||||
// Keyed by id order, which is the order the desired disks were planned against.
|
||||
byId := slices.Clone(current)
|
||||
slices.SortStableFunc(byId, func(a, b binarylane.Disk) int { return cmp.Compare(a.Id, b.Id) })
|
||||
|
||||
var primary *binarylane.Disk
|
||||
currentKeys := make(map[int64]resources.DiskKey, len(current))
|
||||
currentByKey := make(map[resources.DiskKey]binarylane.Disk, len(current))
|
||||
keyer = resources.DiskKeyer{}
|
||||
for i, d := range byId {
|
||||
if d.Primary {
|
||||
primary = &byId[i]
|
||||
continue
|
||||
}
|
||||
key := keyer.Of(false, diskDescription(d))
|
||||
currentKeys[d.Id] = key
|
||||
currentByKey[key] = d
|
||||
}
|
||||
|
||||
// Delete disks that are no longer wanted and shrink those that are getting smaller.
|
||||
for _, d := range current {
|
||||
if d.Primary {
|
||||
continue
|
||||
}
|
||||
desired, wanted := desiredSizes[currentKeys[d.Id]]
|
||||
switch {
|
||||
case !wanted:
|
||||
mutated = true
|
||||
if err := r.deleteServerDisk(ctx, serverId, d.Id); err != nil {
|
||||
return true, err
|
||||
}
|
||||
case desired < d.SizeGigabytes:
|
||||
mutated = true
|
||||
if err := r.resizeServerDisk(ctx, serverId, d.Id, desired); err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// The primary shrinks before a server-level resize and grows after it, never both.
|
||||
if primary != nil && (desiredPrimary < primary.SizeGigabytes ||
|
||||
(growAndAdd && desiredPrimary > primary.SizeGigabytes)) {
|
||||
mutated = true
|
||||
if err := r.resizeServerDisk(ctx, serverId, primary.Id, desiredPrimary); err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
|
||||
if !growAndAdd {
|
||||
return mutated, nil
|
||||
}
|
||||
|
||||
for i, d := range desiredAdditional {
|
||||
if existing, ok := currentByKey[desiredKeys[i]]; ok {
|
||||
if d.SizeGigabytes > existing.SizeGigabytes {
|
||||
mutated = true
|
||||
if err := r.resizeServerDisk(ctx, serverId, existing.Id, d.SizeGigabytes); err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
mutated = true
|
||||
if err := r.addServerDisk(ctx, serverId, diskDescription(d), d.SizeGigabytes); err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
return mutated, nil
|
||||
}
|
||||
|
||||
// diskDescription returns a disk's description, which is unset for a disk added without one.
|
||||
func diskDescription(d binarylane.Disk) string {
|
||||
if d.Description == nil {
|
||||
return ""
|
||||
}
|
||||
return *d.Description
|
||||
}
|
||||
|
||||
func (r *serverResource) fetchServerDisks(ctx context.Context, serverId int64) ([]binarylane.Disk, error) {
|
||||
resp, err := r.bc.client.GetServersServerIdWithResponse(ctx, serverId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching server disks: server_id=%d, error: %w", serverId, err)
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected HTTP status code fetching server disks: server_id=%d, status=%s, body: %s", serverId, resp.Status(), resp.Body)
|
||||
}
|
||||
return resp.JSON200.Server.Disks, nil
|
||||
}
|
||||
|
||||
func (r *serverResource) resizeServerDisk(ctx context.Context, serverId int64, diskId int64, sizeGigabytes float64) error {
|
||||
tflog.Info(ctx, fmt.Sprintf("Resizing disk: server_id=%d, disk_id=%d, size_gigabytes=%g", serverId, diskId, sizeGigabytes))
|
||||
|
||||
resizeResp, err := r.bc.client.PostServersServerIdActionsResizeDiskWithResponse(ctx, serverId, binarylane.ResizeDisk{
|
||||
Type: binarylane.ResizeDiskTypeResizeDisk,
|
||||
DiskId: diskId,
|
||||
SizeGigabytes: int32(sizeGigabytes),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resizing disk: server_id=%d, disk_id=%d, error: %w", serverId, diskId, err)
|
||||
}
|
||||
if resizeResp.StatusCode() != http.StatusOK {
|
||||
return fmt.Errorf("unexpected HTTP status code resizing disk: server_id=%d, disk_id=%d, details: %s", serverId, diskId, resizeResp.Body)
|
||||
}
|
||||
|
||||
err = r.waitForServerAction(ctx, serverId, resizeResp.JSON200.Action.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resizing disk: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *serverResource) deleteServerDisk(ctx context.Context, serverId int64, diskId int64) error {
|
||||
tflog.Info(ctx, fmt.Sprintf("Deleting disk: server_id=%d, disk_id=%d", serverId, diskId))
|
||||
|
||||
deleteResp, err := r.bc.client.PostServersServerIdActionsDeleteDiskWithResponse(ctx, serverId, binarylane.DeleteDisk{
|
||||
Type: binarylane.DeleteDiskTypeDeleteDisk,
|
||||
DiskId: diskId,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting disk: server_id=%d, disk_id=%d, error: %w", serverId, diskId, err)
|
||||
}
|
||||
if deleteResp.StatusCode() != http.StatusOK {
|
||||
return fmt.Errorf("unexpected HTTP status code deleting disk: server_id=%d, disk_id=%d, details: %s", serverId, diskId, deleteResp.Body)
|
||||
}
|
||||
|
||||
err = r.waitForServerAction(ctx, serverId, deleteResp.JSON200.Action.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting disk: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *serverResource) addServerDisk(ctx context.Context, serverId int64, description string, sizeGigabytes float64) error {
|
||||
tflog.Info(ctx, fmt.Sprintf("Adding disk: server_id=%d, description=%s, size_gigabytes=%g", serverId, description, sizeGigabytes))
|
||||
|
||||
addDisk := binarylane.AddDisk{
|
||||
Type: binarylane.AddDiskTypeAddDisk,
|
||||
SizeGigabytes: int32(sizeGigabytes),
|
||||
}
|
||||
if description != "" {
|
||||
addDisk.Description = &description
|
||||
}
|
||||
|
||||
addResp, err := r.bc.client.PostServersServerIdActionsAddDiskWithResponse(ctx, serverId, addDisk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding disk: server_id=%d, description=%s, error: %w", serverId, description, err)
|
||||
}
|
||||
if addResp.StatusCode() != http.StatusOK {
|
||||
return fmt.Errorf("unexpected HTTP status code adding disk: server_id=%d, description=%s, details: %s", serverId, description, addResp.Body)
|
||||
}
|
||||
|
||||
err = r.waitForServerAction(ctx, serverId, addResp.JSON200.Action.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding disk: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ func TestServerResource(t *testing.T) {
|
||||
sshPublicKeyInitial := GenerateTestPublicKey(t)
|
||||
sshPublicKeyUpdated := GenerateTestPublicKey(t)
|
||||
|
||||
// Captured while "zeta" still exists, then checked once it is gone and the disks after it
|
||||
// have shifted down a position.
|
||||
var alphaDiskId, betaDiskId string
|
||||
|
||||
resource.ParallelTest(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
@@ -49,7 +53,7 @@ resource "binarylane_ssh_key" "updated" {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-resource"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-11"
|
||||
size = "std-min"
|
||||
memory = 1152
|
||||
@@ -77,11 +81,16 @@ data "binarylane_server" "test" {
|
||||
// Verify resource values
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "name", "tf-test-server-resource"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "region", "per"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "region", testRegion),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "image", "debian-11"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "size", "std-min"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "memory", "1152"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disk", "20"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.#", "1"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.primary", "true"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.description", "SYSTEM"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.size_gigabytes", "20"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "vpc_id"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "vpc_ipv4_address"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "public_ipv4_count", "1"),
|
||||
@@ -114,7 +123,7 @@ echo "Hello World" > /var/tmp/output.txt
|
||||
// Verify data source values
|
||||
resource.TestCheckResourceAttrPair("data.binarylane_server.test", "id", "binarylane_server.test", "id"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "name", "tf-test-server-resource"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "region", "per"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "region", testRegion),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "image", "debian-11"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "size", "std-min"),
|
||||
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "vpc_id"),
|
||||
@@ -125,6 +134,11 @@ echo "Hello World" > /var/tmp/output.txt
|
||||
`),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "memory", "1152"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disk", "20"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.#", "1"),
|
||||
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "disks.0.id"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.0.primary", "true"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.0.description", "SYSTEM"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.0.size_gigabytes", "20"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "backups", "true"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "port_blocking", "false"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "source_and_destination_check", "false"),
|
||||
@@ -180,10 +194,16 @@ resource "binarylane_ssh_key" "updated" {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-resource-2"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-1vcpu"
|
||||
disk = "45"
|
||||
size = "std-min"
|
||||
disk = 40
|
||||
disks = [
|
||||
{ primary = true, size_gigabytes = 25 },
|
||||
{ description = "zeta", size_gigabytes = 5 },
|
||||
{ description = "alpha", size_gigabytes = 5 },
|
||||
{ description = "beta", size_gigabytes = 5 },
|
||||
]
|
||||
password = "` + password1 + `"
|
||||
vpc_id = null
|
||||
public_ipv4_count = 0
|
||||
@@ -207,9 +227,34 @@ EOT
|
||||
`,
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "name", "tf-test-server-resource-2"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "size", "std-1vcpu"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "memory", "2048"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disk", "45"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "size", "std-min"),
|
||||
// `memory` is unset here and `size` is unchanged, so it carries forward
|
||||
// the 1152 set in the previous step.
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "memory", "1152"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disk", "40"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.#", "4"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.primary", "true"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.description", "SYSTEM"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.size_gigabytes", "25"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.1.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.description", "zeta"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.size_gigabytes", "5"),
|
||||
resource.TestCheckResourceAttrWith("binarylane_server.test", "disks.2.id", func(value string) error {
|
||||
alphaDiskId = value
|
||||
return nil
|
||||
}),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.description", "alpha"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.size_gigabytes", "5"),
|
||||
resource.TestCheckResourceAttrWith("binarylane_server.test", "disks.3.id", func(value string) error {
|
||||
betaDiskId = value
|
||||
return nil
|
||||
}),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.description", "beta"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.size_gigabytes", "5"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "public_ipv4_count", "0"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "public_ipv4_addresses.#", "0"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "image", "debian-12"),
|
||||
@@ -254,10 +299,16 @@ resource "binarylane_ssh_key" "updated" {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-resource-2"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-1vcpu"
|
||||
disk = "45"
|
||||
size = "std-min"
|
||||
disk = 35
|
||||
disks = [
|
||||
{ primary = true, size_gigabytes = 15 },
|
||||
{ description = "alpha", size_gigabytes = 10 },
|
||||
{ description = "beta", size_gigabytes = 5 },
|
||||
{ description = "delta", size_gigabytes = 5 },
|
||||
]
|
||||
password = "` + password2 + `"
|
||||
vpc_id = null
|
||||
public_ipv4_count = 0
|
||||
@@ -278,6 +329,288 @@ EOT
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "name", "tf-test-server-resource-2"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "password", password2),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disk", "35"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.#", "4"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.primary", "true"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.description", "SYSTEM"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.size_gigabytes", "15"),
|
||||
// Removing "zeta" shifts these down a position, but each keeps the disk it
|
||||
// had: they are matched to prior state by description, not by index.
|
||||
resource.TestCheckResourceAttrWith("binarylane_server.test", "disks.1.id", func(value string) error {
|
||||
if value != alphaDiskId {
|
||||
return fmt.Errorf("disk \"alpha\" has id %s, want %s", value, alphaDiskId)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.description", "alpha"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.size_gigabytes", "10"),
|
||||
resource.TestCheckResourceAttrWith("binarylane_server.test", "disks.2.id", func(value string) error {
|
||||
if value != betaDiskId {
|
||||
return fmt.Errorf("disk \"beta\" has id %s, want %s", value, betaDiskId)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.description", "beta"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.size_gigabytes", "5"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.3.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.description", "delta"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.size_gigabytes", "5"),
|
||||
),
|
||||
},
|
||||
// Dropping `disks` leaves the server's disks alone: growing the total here only
|
||||
// adds unallocated space, since the primary is not the only disk.
|
||||
{
|
||||
Config: providerConfig + `
|
||||
resource "binarylane_ssh_key" "updated" {
|
||||
name = "tf-test-server-resource-updated"
|
||||
public_key = "` + sshPublicKeyUpdated + `"
|
||||
default = true
|
||||
}
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-resource-2"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
disk = 50
|
||||
password = "` + password2 + `"
|
||||
vpc_id = null
|
||||
public_ipv4_count = 0
|
||||
ssh_keys = [binarylane_ssh_key.updated.id]
|
||||
|
||||
user_data = <<EOT
|
||||
#cloud-config
|
||||
echo "Hello Whitespace" > /var/tmp/output.txt
|
||||
|
||||
|
||||
EOT
|
||||
}
|
||||
`,
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disk", "50"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.#", "4"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.primary", "true"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.description", "SYSTEM"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.size_gigabytes", "15"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.1.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.description", "alpha"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.size_gigabytes", "10"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.2.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.description", "beta"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.size_gigabytes", "5"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.3.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.description", "delta"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.3.size_gigabytes", "5"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TestServerResourceDisksValidation exercises the plan-time `disks` validators. Every case only
|
||||
// plans a fresh server, so no infrastructure is created.
|
||||
func TestServerResourceDisksValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
// attributes are added to an otherwise valid server resource.
|
||||
attributes string
|
||||
// expectErr is the error the plan must fail with, or empty if it must succeed.
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "no primary disk",
|
||||
attributes: `disks = [{ description = "data1", size_gigabytes = 10 }]`,
|
||||
expectErr: "Missing primary disk",
|
||||
},
|
||||
{
|
||||
name: "empty disks list",
|
||||
attributes: `disks = []`,
|
||||
expectErr: "Missing primary disk",
|
||||
},
|
||||
{
|
||||
name: "two primary disks",
|
||||
attributes: `disks = [
|
||||
{ primary = true, size_gigabytes = 20 },
|
||||
{ primary = true, size_gigabytes = 10 },
|
||||
]`,
|
||||
expectErr: "Multiple primary disks",
|
||||
},
|
||||
{
|
||||
// The primary disk's description is assigned by Binary Lane, not by the user.
|
||||
name: "primary disk with a description",
|
||||
attributes: `disks = [{ primary = true, description = "SYSTEM", size_gigabytes = 20 }]`,
|
||||
expectErr: "Primary disk description is server-assigned",
|
||||
},
|
||||
{
|
||||
name: "disks exceeding the total",
|
||||
attributes: `disk = 45
|
||||
disks = [
|
||||
{ primary = true, size_gigabytes = 30 },
|
||||
{ description = "data1", size_gigabytes = 20 },
|
||||
]`,
|
||||
expectErr: "Disks exceed total disk allocation",
|
||||
},
|
||||
{
|
||||
name: "disk below the minimum",
|
||||
attributes: `disk = 10`,
|
||||
expectErr: "Attribute disk",
|
||||
},
|
||||
{
|
||||
// Binary Lane allows duplicate descriptions, and so does this provider.
|
||||
name: "duplicate descriptions",
|
||||
attributes: `disks = [
|
||||
{ primary = true, size_gigabytes = 20 },
|
||||
{ description = "data", size_gigabytes = 5 },
|
||||
{ description = "data", size_gigabytes = 10 },
|
||||
]`,
|
||||
},
|
||||
{
|
||||
// Also valid with room to spare, leaving the difference unallocated.
|
||||
name: "disks filling the total exactly",
|
||||
attributes: `disk = 40
|
||||
disks = [
|
||||
{ primary = true, size_gigabytes = 30 },
|
||||
{ description = "data1", size_gigabytes = 10 },
|
||||
]`,
|
||||
},
|
||||
{
|
||||
name: "disks with the total left unset",
|
||||
attributes: `disks = [
|
||||
{ primary = true, size_gigabytes = 30 },
|
||||
{ description = "data1", size_gigabytes = 10 },
|
||||
]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
step := resource.TestStep{
|
||||
Config: planOnlyProviderConfig + fmt.Sprintf(`
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-disks-validation"
|
||||
region = %q
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
%s
|
||||
}
|
||||
`, testRegion, tt.attributes),
|
||||
PlanOnly: true,
|
||||
ExpectNonEmptyPlan: true,
|
||||
}
|
||||
if tt.expectErr != "" {
|
||||
step.ExpectError = regexp.MustCompile(tt.expectErr)
|
||||
}
|
||||
resource.UnitTest(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{step},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerResourceCreateWithDisks(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{
|
||||
{
|
||||
Config: providerConfig + `
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-create-disks"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
password = "` + password + `"
|
||||
disk = 40
|
||||
disks = [
|
||||
{ primary = true, size_gigabytes = 20 },
|
||||
{ description = "data", size_gigabytes = 10 },
|
||||
{ description = "data", size_gigabytes = 5 },
|
||||
]
|
||||
}
|
||||
`,
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
// 5 GB of the total is left unallocated.
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disk", "40"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.#", "3"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.primary", "true"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.description", "SYSTEM"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.size_gigabytes", "20"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.1.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.description", "data"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.size_gigabytes", "10"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.2.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.description", "data"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.size_gigabytes", "5"),
|
||||
),
|
||||
},
|
||||
// The same disks in a different order, which is not a change.
|
||||
{
|
||||
Config: providerConfig + `
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-create-disks"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
password = "` + password + `"
|
||||
disk = 40
|
||||
disks = [
|
||||
{ description = "data", size_gigabytes = 10 },
|
||||
{ primary = true, size_gigabytes = 20 },
|
||||
{ description = "data", size_gigabytes = 5 },
|
||||
]
|
||||
}
|
||||
|
||||
data "binarylane_server" "test" {
|
||||
depends_on = [binarylane_server.test]
|
||||
|
||||
id = binarylane_server.test.id
|
||||
}
|
||||
`,
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
// The resource keeps the order the configuration lists the disks in
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.#", "3"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.0.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.0.description", "data"),
|
||||
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.primary", "true"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.description", "SYSTEM"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.1.size_gigabytes", "20"),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "disks.2.id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.primary", "false"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.description", "data"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "disks.2.size_gigabytes", "5"),
|
||||
|
||||
// The data source has no order of its own, so it lists the primary disk
|
||||
// first and the rest by id
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.#", "3"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.0.primary", "true"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.0.description", "SYSTEM"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.0.size_gigabytes", "20"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.1.primary", "false"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.1.description", "data"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.1.size_gigabytes", "10"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.2.primary", "false"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.2.description", "data"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "disks.2.size_gigabytes", "5"),
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -297,7 +630,7 @@ func TestServerResourceRename(t *testing.T) {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-rename-1"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-11"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
@@ -310,7 +643,7 @@ resource "binarylane_server" "test" {
|
||||
Config: providerConfig + `
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-rename-2"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-11"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
@@ -319,7 +652,7 @@ resource "binarylane_server" "test" {
|
||||
`,
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "name", "tf-test-server-rename-2"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "region", "per"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "region", testRegion),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "image", "debian-11"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "size", "std-min"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "password", password),
|
||||
@@ -341,7 +674,7 @@ func TestServerVpcIpv4Change(t *testing.T) {
|
||||
Config: providerConfig + `
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-vpcipv4-1"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-11"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
@@ -367,7 +700,7 @@ resource "binarylane_vpc" "test2" {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-vpcipv4-1"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-11"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
@@ -407,7 +740,7 @@ resource "binarylane_vpc" "test2" {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-vpcipv4-1"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-11"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
@@ -447,7 +780,7 @@ resource "binarylane_vpc" "test2" {
|
||||
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-test-server-vpcipv4-1"
|
||||
region = "per"
|
||||
region = "` + testRegion + `"
|
||||
image = "debian-11"
|
||||
size = "std-min"
|
||||
public_ipv4_count = 0
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
// DiskKey identifies a disk across plans: the primary by being the primary, whose description
|
||||
// Binary Lane assigns, and every other disk by its description. Binary Lane allows two disks to
|
||||
// share a description, so they are also numbered within it: the second "data" disk configured is
|
||||
// the second "data" disk on the server.
|
||||
type DiskKey struct {
|
||||
primary bool
|
||||
description string
|
||||
ordinal int
|
||||
}
|
||||
|
||||
// DiskKeyer numbers disks as it keys them, so both sides of a match must be keyed in the same
|
||||
// order: id order for the server's own disks.
|
||||
type DiskKeyer map[string]int
|
||||
|
||||
func (k DiskKeyer) Of(primary bool, description string) DiskKey {
|
||||
if primary {
|
||||
return DiskKey{primary: true}
|
||||
}
|
||||
ordinal := k[description]
|
||||
k[description]++
|
||||
return DiskKey{description: description, ordinal: ordinal}
|
||||
}
|
||||
|
||||
// DiskShrinkWarning warns that shrinking a disk may corrupt the filesystem on it.
|
||||
func DiskShrinkWarning(attrPath path.Path, label string, from int64, to int64) diag.Diagnostic {
|
||||
return diag.NewAttributeWarningDiagnostic(
|
||||
attrPath,
|
||||
fmt.Sprintf("%s shrink may corrupt the filesystem", label),
|
||||
fmt.Sprintf(
|
||||
"%s is being reduced from %d GB to %d GB. Binary Lane resizes the block device in place and does not touch the "+
|
||||
"filesystem on it, so the filesystem(s) (and any partitions) on it must first be shrunk to fit within %d GB. "+
|
||||
"Back up the server before applying, and make sure there's enough free space to shrink the filesystem to that size.",
|
||||
label, from, to, to,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// disksPlanModifier is referenced by name from the "disks" attribute in
|
||||
// scripts/data/server_disks.json, the same way disksValidator is.
|
||||
//
|
||||
// Terraform builds the proposed plan for a list by merging prior state into the configuration
|
||||
// element by element, so removing a disk hands the disks after it the id of their predecessor.
|
||||
// This plans the list from the configuration instead, taking each disk's server-assigned values
|
||||
// from the disk it matches in prior state rather than from whatever sits at the same index.
|
||||
type disksPlanModifier struct{}
|
||||
|
||||
func (m disksPlanModifier) Description(ctx context.Context) string {
|
||||
return "Matches each disk with the one it corresponds to in prior state, rather than with whatever is at the same index."
|
||||
}
|
||||
|
||||
func (m disksPlanModifier) MarkdownDescription(ctx context.Context) string {
|
||||
return m.Description(ctx)
|
||||
}
|
||||
|
||||
func (m disksPlanModifier) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) {
|
||||
// Nothing to plan when the resource is being destroyed.
|
||||
if req.Plan.Raw.IsNull() {
|
||||
return
|
||||
}
|
||||
|
||||
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
|
||||
// An unset `disks` is whatever Binary Lane gives the server, which isn't known until the
|
||||
// server exists. Once it does, the resource's ModifyPlan plans it instead: what happens
|
||||
// to the disks then depends on `disk`, the server's total, which it only resolves after
|
||||
// attribute plan modifiers like this one have run.
|
||||
if req.State.Raw.IsNull() {
|
||||
resp.PlanValue = types.ListUnknown(DisksValue{}.Type(ctx))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var configured, current []DisksValue
|
||||
resp.Diagnostics.Append(req.ConfigValue.ElementsAs(ctx, &configured, true)...)
|
||||
if !req.StateValue.IsNull() && !req.StateValue.IsUnknown() {
|
||||
resp.Diagnostics.Append(req.StateValue.ElementsAs(ctx, ¤t, true)...)
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Key prior state in id order, the order the server's own disks are keyed in wherever they
|
||||
// are matched. serverDisksToList already leaves state in an order that agrees with it, but
|
||||
// this does not have to rely on that: get it wrong and disks that share a description are
|
||||
// paired with the wrong disk, and the plan promises a resize of one while the apply resizes
|
||||
// another.
|
||||
slices.SortStableFunc(current, func(a, b DisksValue) int {
|
||||
return cmp.Compare(a.Id.ValueInt64(), b.Id.ValueInt64())
|
||||
})
|
||||
|
||||
stateKeys := DiskKeyer{}
|
||||
priorByKey := make(map[DiskKey]DisksValue, len(current))
|
||||
for _, d := range current {
|
||||
priorByKey[stateKeys.Of(d.Primary.ValueBool(), d.Description.ValueString())] = d
|
||||
}
|
||||
|
||||
configKeys := DiskKeyer{}
|
||||
elements := make([]attr.Value, 0, len(configured))
|
||||
for _, cd := range configured {
|
||||
// Additional disks may leave `primary` unset, which means false.
|
||||
isPrimary := cd.Primary.ValueBool()
|
||||
prior, matched := priorByKey[configKeys.Of(isPrimary, cd.Description.ValueString())]
|
||||
|
||||
// `id` and the primary's `description` are assigned by Binary Lane, so both are unknown
|
||||
// until there is a disk in state to take them from.
|
||||
id, description := types.Int64Unknown(), cd.Description
|
||||
if isPrimary {
|
||||
description = types.StringUnknown()
|
||||
}
|
||||
if matched {
|
||||
if !prior.Id.IsNull() && !prior.Id.IsUnknown() {
|
||||
id = prior.Id
|
||||
}
|
||||
if isPrimary && !prior.Description.IsNull() && !prior.Description.IsUnknown() {
|
||||
description = prior.Description
|
||||
}
|
||||
if !cd.SizeGigabytes.IsUnknown() && cd.SizeGigabytes.ValueInt64() < prior.SizeGigabytes.ValueInt64() {
|
||||
label := fmt.Sprintf("Disk %q", cd.Description.ValueString())
|
||||
if isPrimary {
|
||||
label = "The primary disk"
|
||||
}
|
||||
resp.Diagnostics.Append(DiskShrinkWarning(
|
||||
req.Path, label, prior.SizeGigabytes.ValueInt64(), cd.SizeGigabytes.ValueInt64()))
|
||||
}
|
||||
}
|
||||
|
||||
disk, diskDiags := NewDisksValue(
|
||||
DisksValue{}.AttributeTypes(ctx),
|
||||
map[string]attr.Value{
|
||||
"id": id,
|
||||
"description": description,
|
||||
"primary": types.BoolValue(isPrimary),
|
||||
"size_gigabytes": cd.SizeGigabytes,
|
||||
},
|
||||
)
|
||||
resp.Diagnostics.Append(diskDiags...)
|
||||
elements = append(elements, disk)
|
||||
}
|
||||
|
||||
planned, diags := types.ListValue(DisksValue{}.Type(ctx), elements)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if !diags.HasError() {
|
||||
resp.PlanValue = planned
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
// disksValidator is referenced by name from the "disks" attribute in
|
||||
// scripts/data/server_disks.json, which extend-provider.sh splices into the generated schema. It
|
||||
// lives here rather than in internal/provider, which internal/resources cannot import.
|
||||
type disksValidator struct{}
|
||||
|
||||
func (v disksValidator) Description(ctx context.Context) string {
|
||||
return "Requires exactly one `disks` entry to have `primary = true` (with no `description` set) whenever `disks` is configured, and `sum(disks[*].size_gigabytes)` to not exceed `disk`."
|
||||
}
|
||||
|
||||
func (v disksValidator) MarkdownDescription(ctx context.Context) string {
|
||||
return v.Description(ctx)
|
||||
}
|
||||
|
||||
func (v disksValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
|
||||
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
var disks []DisksValue
|
||||
resp.Diagnostics.Append(req.ConfigValue.ElementsAs(ctx, &disks, false)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
primaries, primaryUnknown := 0, false
|
||||
sum, sumKnown := int64(0), true
|
||||
for i, d := range disks {
|
||||
switch {
|
||||
case d.Primary.IsUnknown():
|
||||
primaryUnknown = true
|
||||
case d.Primary.ValueBool():
|
||||
primaries++
|
||||
if !d.Description.IsNull() && !d.Description.IsUnknown() {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
req.Path.AtListIndex(i).AtName("description"),
|
||||
"Primary disk description is server-assigned",
|
||||
"The primary disk's `description` is always assigned by Binary Lane (typically \"SYSTEM\"); leave it unset on the entry with `primary = true`.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if d.SizeGigabytes.IsNull() || d.SizeGigabytes.IsUnknown() {
|
||||
sumKnown = false
|
||||
continue
|
||||
}
|
||||
sum += d.SizeGigabytes.ValueInt64()
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if !primaryUnknown {
|
||||
if primaries == 0 {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
req.Path,
|
||||
"Missing primary disk",
|
||||
"When `disks` is specified, it must fully describe every disk, including the primary — add an entry with `primary = true`.",
|
||||
)
|
||||
return
|
||||
}
|
||||
if primaries > 1 {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
req.Path,
|
||||
"Multiple primary disks",
|
||||
"Only one entry in `disks` can have `primary = true`.",
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var disk types.Int32
|
||||
resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("disk"), &disk)...)
|
||||
if resp.Diagnostics.HasError() || !sumKnown || disk.IsNull() || disk.IsUnknown() {
|
||||
return
|
||||
}
|
||||
if sum > int64(disk.ValueInt32()) {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
req.Path,
|
||||
"Disks exceed total disk allocation",
|
||||
fmt.Sprintf(
|
||||
"The sum of `disks[*].size_gigabytes` is %d GB, which exceeds `disk` (%d GB total). "+
|
||||
"`sum(disks[*].size_gigabytes)` must not exceed `disk`.",
|
||||
sum, disk.ValueInt32(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ package resources
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
@@ -94,6 +96,55 @@ 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 label for this disk. Assigned by Binary Lane for the primary disk, so must be left null when `primary = true`. Changing this will destroy the disk and create a new one, losing any data on it. Disks that share a description are matched in the order they are listed.",
|
||||
MarkdownDescription: "A label for this disk. Assigned by Binary Lane for the primary disk, so must be left null when `primary = true`. Changing this will destroy the disk and create a new one, losing any data on it. Disks that share a description are matched in the order they are listed.",
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
},
|
||||
},
|
||||
"id": schema.Int64Attribute{
|
||||
Computed: true,
|
||||
Description: "The ID of this disk.",
|
||||
MarkdownDescription: "The ID of this disk.",
|
||||
},
|
||||
"primary": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "If `true` this is the primary disk, which the operating system is installed on. Exactly one disk must be the primary.",
|
||||
MarkdownDescription: "If `true` this is the primary disk, which the operating system is installed on. Exactly one disk must be the primary.",
|
||||
},
|
||||
"size_gigabytes": schema.Int64Attribute{
|
||||
Required: true,
|
||||
Description: "The size of the disk in GB. The sum across all disks cannot exceed `disk`, and any remainder is left unallocated.",
|
||||
MarkdownDescription: "The size of the disk in GB. The sum across all disks cannot exceed `disk`, and any remainder is left unallocated.",
|
||||
Validators: []validator.Int64{
|
||||
int64validator.AtLeast(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
CustomType: DisksType{
|
||||
ObjectType: types.ObjectType{
|
||||
AttrTypes: DisksValue{}.AttributeTypes(ctx),
|
||||
},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "The disks attached to this server. Leave null to let Binary Lane manage the server's disks. If specified, every disk must be listed, including the primary: exactly one entry must have `primary = true`, and its `id` and `description` are assigned by Binary Lane.",
|
||||
MarkdownDescription: "The disks attached to this server. Leave null to let Binary Lane manage the server's disks. If specified, every disk must be listed, including the primary: exactly one entry must have `primary = true`, and its `id` and `description` are assigned by Binary Lane.",
|
||||
PlanModifiers: []planmodifier.List{
|
||||
disksPlanModifier{},
|
||||
},
|
||||
Validators: []validator.List{
|
||||
disksValidator{},
|
||||
},
|
||||
},
|
||||
"id": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
@@ -180,6 +231,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"`
|
||||
Ipv6 types.Bool `tfsdk:"ipv6"`
|
||||
@@ -1012,3 +1064,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.Int64Value)
|
||||
|
||||
if !ok {
|
||||
diags.AddError(
|
||||
"Attribute Wrong Type",
|
||||
fmt.Sprintf(`size_gigabytes expected to be basetypes.Int64Value, 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.Int64Value)
|
||||
|
||||
if !ok {
|
||||
diags.AddError(
|
||||
"Attribute Wrong Type",
|
||||
fmt.Sprintf(`size_gigabytes expected to be basetypes.Int64Value, 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.Int64Value `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.Int64Type{}.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.Int64Type{},
|
||||
}
|
||||
|
||||
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.Int64Type{},
|
||||
}
|
||||
}
|
||||
|
||||
+92
-72
@@ -8,7 +8,7 @@
|
||||
"url": "https://support.binarylane.com.au/support/home",
|
||||
"email": "support@binarylane.com.au"
|
||||
},
|
||||
"version": "0.39.1",
|
||||
"version": "0.40.0",
|
||||
"x-logo": {
|
||||
"url": "https://www.binarylane.com.au/res/images/binarylane/logo.png",
|
||||
"backgroundColor": "#000000",
|
||||
@@ -111,12 +111,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -277,12 +277,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -401,12 +401,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -517,12 +517,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -730,12 +730,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -1029,12 +1029,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -1292,12 +1292,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -1492,6 +1492,14 @@
|
||||
],
|
||||
"summary": "List all Load Balancers",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Providing a name restricts the results to load balancers with this name (case insensitive).",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
@@ -1508,12 +1516,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -1534,7 +1542,7 @@
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "Curl",
|
||||
"source": "curl -X GET \"https://api.binarylane.com.au/v2/load_balancers?page=${page}&per_page=${per_page}\" \\\n-H \"Authorization: Bearer ${APITOKEN}\""
|
||||
"source": "curl -X GET \"https://api.binarylane.com.au/v2/load_balancers?name=${name}&page=${page}&per_page=${per_page}\" \\\n-H \"Authorization: Bearer ${APITOKEN}\""
|
||||
}
|
||||
],
|
||||
"x-cli-command": "load-balancer list"
|
||||
@@ -1808,12 +1816,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -1864,12 +1872,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -2056,12 +2064,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -2122,12 +2130,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -2495,12 +2503,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -2729,12 +2737,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -2847,12 +2855,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -2914,12 +2922,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -3048,12 +3056,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -3201,12 +3209,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -3295,12 +3303,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -3368,12 +3376,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -3603,12 +3611,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -3721,12 +3729,12 @@
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"schema": {
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "The number of results to show per page.",
|
||||
"description": "The number of results to show per page. Use 0 to return only the total count.",
|
||||
"format": "int32",
|
||||
"default": 20
|
||||
}
|
||||
@@ -7998,21 +8006,33 @@
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "The ID of this disk.",
|
||||
"format": "int64"
|
||||
"format": "int64",
|
||||
"x-oapi-codegen-extra-tags": {
|
||||
"tfsdk": "id"
|
||||
}
|
||||
},
|
||||
"size_gigabytes": {
|
||||
"type": "number",
|
||||
"description": "The size of the disk in GB.",
|
||||
"format": "double"
|
||||
"format": "double",
|
||||
"x-oapi-codegen-extra-tags": {
|
||||
"tfsdk": "size_gigabytes"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A description of this disk.",
|
||||
"nullable": true
|
||||
"nullable": true,
|
||||
"x-oapi-codegen-extra-tags": {
|
||||
"tfsdk": "description"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean",
|
||||
"description": "A primary disk is treated differently from other disks."
|
||||
"description": "A primary disk is treated differently from other disks.",
|
||||
"x-oapi-codegen-extra-tags": {
|
||||
"tfsdk": "primary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1037,6 +1037,88 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disks",
|
||||
"list_nested": {
|
||||
"computed_optional_required": "computed_optional",
|
||||
"description": "The disks attached to this server. Leave null to let Binary Lane manage the server's disks. If specified, every disk must be listed, including the primary: exactly one entry must have `primary = true`, and its `id` and `description` are assigned by Binary Lane.",
|
||||
"nested_object": {
|
||||
"attributes": [
|
||||
{
|
||||
"name": "id",
|
||||
"int64": {
|
||||
"computed_optional_required": "computed",
|
||||
"description": "The ID of this disk."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"string": {
|
||||
"computed_optional_required": "computed_optional",
|
||||
"description": "A label for this disk. Assigned by Binary Lane for the primary disk, so must be left null when `primary = true`. Changing this will destroy the disk and create a new one, losing any data on it. Disks that share a description are matched in the order they are listed.",
|
||||
"validators": [
|
||||
{
|
||||
"custom": {
|
||||
"imports": [
|
||||
{
|
||||
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
}
|
||||
],
|
||||
"schema_definition": "stringvalidator.LengthAtLeast(1)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "primary",
|
||||
"bool": {
|
||||
"computed_optional_required": "computed_optional",
|
||||
"description": "If `true` this is the primary disk, which the operating system is installed on. Exactly one disk must be the primary."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size_gigabytes",
|
||||
"int64": {
|
||||
"computed_optional_required": "required",
|
||||
"description": "The size of the disk in GB. The sum across all disks cannot exceed `disk`, and any remainder is left unallocated.",
|
||||
"validators": [
|
||||
{
|
||||
"custom": {
|
||||
"imports": [
|
||||
{
|
||||
"path": "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
||||
}
|
||||
],
|
||||
"schema_definition": "int64validator.AtLeast(1)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"validators": [
|
||||
{
|
||||
"custom": {
|
||||
"schema_definition": "disksValidator{}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"plan_modifiers": [
|
||||
{
|
||||
"custom": {
|
||||
"imports": [
|
||||
{
|
||||
"path": "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
}
|
||||
],
|
||||
"schema_definition": "disksPlanModifier{}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "disks",
|
||||
"list_nested": {
|
||||
"computed_optional_required": "computed_optional",
|
||||
"description": "The disks attached to this server. Leave null to let Binary Lane manage the server's disks. If specified, every disk must be listed, including the primary: exactly one entry must have `primary = true`, and its `id` and `description` are assigned by Binary Lane.",
|
||||
"nested_object": {
|
||||
"attributes": [
|
||||
{
|
||||
"name": "id",
|
||||
"int64": {
|
||||
"computed_optional_required": "computed",
|
||||
"description": "The ID of this disk."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"string": {
|
||||
"computed_optional_required": "computed_optional",
|
||||
"description": "A label for this disk. Assigned by Binary Lane for the primary disk, so must be left null when `primary = true`. Changing this will destroy the disk and create a new one, losing any data on it. Disks that share a description are matched in the order they are listed.",
|
||||
"validators": [
|
||||
{
|
||||
"custom": {
|
||||
"imports": [
|
||||
{
|
||||
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
}
|
||||
],
|
||||
"schema_definition": "stringvalidator.LengthAtLeast(1)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "primary",
|
||||
"bool": {
|
||||
"computed_optional_required": "computed_optional",
|
||||
"description": "If `true` this is the primary disk, which the operating system is installed on. Exactly one disk must be the primary."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size_gigabytes",
|
||||
"int64": {
|
||||
"computed_optional_required": "required",
|
||||
"description": "The size of the disk in GB. The sum across all disks cannot exceed `disk`, and any remainder is left unallocated.",
|
||||
"validators": [
|
||||
{
|
||||
"custom": {
|
||||
"imports": [
|
||||
{
|
||||
"path": "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
||||
}
|
||||
],
|
||||
"schema_definition": "int64validator.AtLeast(1)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"validators": [
|
||||
{
|
||||
"custom": {
|
||||
"schema_definition": "disksValidator{}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"plan_modifiers": [
|
||||
{
|
||||
"custom": {
|
||||
"imports": [
|
||||
{
|
||||
"path": "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
}
|
||||
],
|
||||
"schema_definition": "disksPlanModifier{}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ cat <<<$(jq --tab '. + {"$schema": "https://raw.githubusercontent.com/hashicorp/
|
||||
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
|
||||
|
||||
# Use set instead of list for server_ids in load_balancer resource
|
||||
jq --tab '
|
||||
.resources |= map(
|
||||
|
||||
@@ -37,6 +37,12 @@ cat <<<$(jq 'del(.paths."/images/{image_id}")' $OPENAPI_FILE) >$OPENAPI_FILE
|
||||
|
||||
# Add x-oapi-codegen-extra-tags so structs can be reflected
|
||||
|
||||
## Disk
|
||||
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
|
||||
|
||||
## RouteEntryRequest
|
||||
cat <<<$(jq '.components.schemas.RouteEntryRequest.properties.destination += {"x-oapi-codegen-extra-tags": {"tfsdk": "destination"}}' $OPENAPI_FILE) >$OPENAPI_FILE
|
||||
cat <<<$(jq '.components.schemas.RouteEntryRequest.properties.description += {"x-oapi-codegen-extra-tags": {"tfsdk": "description"}}' $OPENAPI_FILE) >$OPENAPI_FILE
|
||||
|
||||
Reference in New Issue
Block a user