Compare commits

..

1 Commits

Author SHA1 Message Date
Oscar Hermoso 9a58a8112c 👍 Support vpc_ipv4_address, bump BinaryLane OpenAPI schema to v0.34.0 (#114)
* 👍 Bump BinaryLane OpenAPI schema to v0.34.0

* 💄 Support change of IPv4 addresses
2026-02-08 16:58:30 +08:00
17 changed files with 383 additions and 139 deletions
+1
View File
@@ -53,6 +53,7 @@ data "binarylane_server" "example" {
- `ssh_keys` (List of Number) This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys.
- `user_data` (String) A script or cloud-config YAML file to configure the server. Can only be specified if the OS image supports UserData (i.e. not Windows). See more: https://cloudinit.readthedocs.io/en/latest/explanation/format.html#user-data-script
- `vpc_id` (Number) Leave null to use default (public) network for the selected region.
- `vpc_ipv4_address` (String) If provided this will be the IPv4 address for the server's private VPC network adapter. If this is unspecified, then an unused IPv4 address will be assigned. This field is only valid when `vpc_id` is provided.
<a id="nestedatt--advanced_features"></a>
### Nested Schema for `advanced_features`
+1
View File
@@ -58,6 +58,7 @@ resource "binarylane_server" "example" {
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))
- `user_data` (String) A script or cloud-config YAML file to configure the server. Can only be specified if the OS image supports UserData (i.e. not Windows). See more: https://cloudinit.readthedocs.io/en/latest/explanation/format.html#user-data-script
- `vpc_id` (Number) Leave null to use default (public) network for the selected region.
- `vpc_ipv4_address` (String) If provided this will be the IPv4 address for the server's private VPC network adapter. If this is unspecified, then an unused IPv4 address will be assigned. This field is only valid when `vpc_id` is provided.
### Read-Only
+41
View File
@@ -0,0 +1,41 @@
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
*.tfvars.json
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Ignore transient lock info files created by terraform apply
.terraform.tfstate.lock.info
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Ignore SSH keys
.id_ed25519*
.kube
+4 -1
View File
@@ -438,7 +438,7 @@ type Account struct {
// TaxCode The tax code that currently applies to transactions for this account.
TaxCode TaxCode `json:"tax_code"`
// TwoFactorAuthenticationEnabled Whether this account has enabled two factor authentication.
// TwoFactorAuthenticationEnabled Whether this account has enabled app-based two factor authentication.
TwoFactorAuthenticationEnabled bool `json:"two_factor_authentication_enabled"`
}
@@ -1250,6 +1250,9 @@ type CreateServerRequest struct {
// VpcId Leave null to use default (public) network for the selected region.
VpcId *int64 `json:"vpc_id"`
// VpcIpv4Address If provided this will be the Ipv4 address for the server's private VPC network adapter. If this is null an unused Ipv4 address will be assigned. This field is only valid when VpcId is provided.
VpcIpv4Address *string `json:"vpc_ipv4_address"`
}
// CreateServerResponse defines model for CreateServerResponse.
@@ -182,7 +182,7 @@ func init() {
return fmt.Errorf("Unexpected status code getting load balancers for test sweep: %s", listResp.Body)
}
loadBalancers := listResp.JSON200.LoadBalancers
loadBalancers := listResp.JSON200.LoadBalancers
for _, lb := range loadBalancers {
if strings.HasPrefix(lb.Name, "tf-test-") {
deleteResp, err := client.DeleteLoadBalancersLoadBalancerIdWithResponse(ctx, lb.Id)
+15 -2
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"slices"
"strings"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework/attr"
@@ -107,18 +108,30 @@ func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
data.Id = types.Int64Value(serverResp.JSON200.Server.Id)
data.Name = types.StringValue(serverResp.JSON200.Server.Name)
data.Image = types.StringPointerValue(serverResp.JSON200.Server.Image.Slug)
data.Image = types.StringValue(*serverResp.JSON200.Server.Image.Slug)
data.Region = types.StringValue(serverResp.JSON200.Server.Region.Slug)
data.Size = types.StringValue(serverResp.JSON200.Server.Size.Slug)
data.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
data.Ipv6 = types.BoolValue(len(serverResp.JSON200.Server.Networks.V6) > 0)
data.PortBlocking = types.BoolValue(serverResp.JSON200.Server.Networks.PortBlocking)
data.VpcId = types.Int64PointerValue(serverResp.JSON200.Server.VpcId)
data.Permalink = types.StringPointerValue(serverResp.JSON200.Server.Permalink)
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.SourceAndDestinationCheck = types.BoolPointerValue(serverResp.JSON200.Server.Networks.SourceAndDestinationCheck)
if serverResp.JSON200.Server.VpcId == nil {
data.VpcIpv4Address = types.StringNull()
} else if len(serverResp.JSON200.Server.Networks.V4) > 0 {
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
// Skip addresses in 172.21.0.0/16, these are BL internal addresses that are not part of the user's VPC
if v4address.Type == "private" && !strings.HasPrefix(v4address.IpAddress, "172.21.") {
data.VpcIpv4Address = types.StringValue(v4address.IpAddress)
break
}
}
}
advFeat := serverResp.JSON200.Server.AdvancedFeatures.EnabledAdvancedFeatures
data.AdvancedFeatures, diags = resources.NewAdvancedFeaturesValue(
resources.AdvancedFeaturesValue{}.AttributeTypes(ctx),
@@ -121,29 +121,6 @@ func (r *serverFirewallRulesResource) Create(ctx context.Context, req resource.C
return
}
// Read back the firewall rules to get the actual state from the API
readResp, err := r.bc.client.GetServersServerIdAdvancedFirewallRulesWithResponse(ctx, data.ServerId.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading server firewall rules after create: server_id=%d", data.ServerId.ValueInt64()),
err.Error(),
)
return
}
if readResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading server firewall rules after create",
fmt.Sprintf("Received %s reading server firewall rules: server_id=%d. Details: %s", readResp.Status(), data.ServerId.ValueInt64(), readResp.Body))
return
}
firewallRulesValue, diags := GetFirewallRulesState(ctx, &readResp.JSON200.FirewallRules)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.FirewallRules = firewallRulesValue
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -230,29 +207,6 @@ func (r *serverFirewallRulesResource) Update(ctx context.Context, req resource.U
return
}
// Read back the firewall rules to get the actual state from the API
readResp, err := r.bc.client.GetServersServerIdAdvancedFirewallRulesWithResponse(ctx, data.ServerId.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading server firewall rules after update: server_id=%d", data.ServerId.ValueInt64()),
err.Error(),
)
return
}
if readResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading server firewall rules after update",
fmt.Sprintf("Received %s reading server firewall rules: server_id=%d. Details: %s", readResp.Status(), data.ServerId.ValueInt64(), readResp.Body))
return
}
firewallRulesValue, diags := GetFirewallRulesState(ctx, &readResp.JSON200.FirewallRules)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.FirewallRules = firewallRulesValue
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
@@ -308,8 +262,6 @@ func GetFirewallRulesState(ctx context.Context, firewallRules *[]binarylane.Adva
if rule.DestinationPorts != nil {
destinationPorts, diag = types.ListValueFrom(ctx, types.StringType, rule.DestinationPorts)
diags.Append(diag...)
} else {
destinationPorts = types.ListNull(types.StringType)
}
sourceAddresses, diag := types.ListValueFrom(ctx, types.StringType, rule.SourceAddresses)
@@ -126,47 +126,6 @@ resource "binarylane_server_firewall_rules" "test" {
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.action", "accept"),
),
},
// Test ICMP rule without destination_ports
{
Config: providerConfig + `
resource "binarylane_server" "test" {
name = "tf-test-server-fw-rules"
region = "per"
image = "debian-12"
size = "std-min"
password = "` + password + `"
public_ipv4_count = 0
}
resource "binarylane_server_firewall_rules" "test" {
server_id = binarylane_server.test.id
firewall_rules = [
{
description = "Allow ICMP"
protocol = "icmp"
source_addresses = ["0.0.0.0/0"]
destination_addresses = [binarylane_server.test.private_ipv4_addresses.0]
action = "accept"
# destination_ports intentionally not specified for ICMP
},
]
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify resource values with ICMP and no destination_ports
resource.TestCheckResourceAttrSet("binarylane_server_firewall_rules.test", "server_id"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.#", "1"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.description", "Allow ICMP"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.protocol", "icmp"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.source_addresses.#", "1"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.source_addresses.0", "0.0.0.0/0"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_addresses.#", "1"),
resource.TestCheckResourceAttrSet("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_addresses.0"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_ports.#", "0"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.action", "accept"),
),
},
// Delete testing automatically occurs in TestCase
},
})
+126 -20
View File
@@ -186,10 +186,9 @@ func serverSchema(ctx context.Context) schema.Schema {
}
// Additional attributes
pwDescription :=
"If this is provided the specified or default remote user's account password will be set to this value. " +
"Only valid if the server supports password change actions. If omitted and the server supports password " +
"change actions a random password will be generated and emailed to the account email address."
pwDescription := "If this is provided the specified or default remote user's account password will be set to this value. " +
"Only valid if the server supports password change actions. If omitted and the server supports password " +
"change actions a random password will be generated and emailed to the account email address."
s.Attributes["password"] = schema.StringAttribute{
Description: pwDescription,
MarkdownDescription: pwDescription,
@@ -364,11 +363,29 @@ func serverSchema(ctx context.Context) schema.Schema {
},
}
s.Attributes["timeouts"] =
timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
Update: true,
})
s.Attributes["timeouts"] = timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
Update: true,
})
vpcIpv4Addr := s.Attributes["vpc_ipv4_address"]
vpcIpv4AddrDescription := "If provided this will be the IPv4 address for the server's private VPC network adapter." +
" If this is unspecified, then an unused IPv4 address will be assigned. This field is only valid when `vpc_id`" +
" is provided."
s.Attributes["vpc_ipv4_address"] = schema.StringAttribute{
Description: vpcIpv4AddrDescription,
MarkdownDescription: vpcIpv4AddrDescription,
Optional: vpcIpv4Addr.IsOptional(),
Computed: vpcIpv4Addr.IsComputed(),
Validators: []validator.String{
stringvalidator.AlsoRequires(path.Expressions{
path.MatchRoot("vpc_id"),
}...),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
}
return s
}
@@ -401,6 +418,15 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
resp.Diagnostics.Append(resp.Plan.Set(ctx, &plan)...)
}
if plan.VpcIpv4Address.IsUnknown() {
if plan.VpcId.IsNull() {
plan.VpcIpv4Address = types.StringNull()
} else {
plan.VpcIpv4Address = types.StringUnknown()
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, &plan)...)
}
if req.State.Raw.IsNull() {
// Creation plan, no further modification needed
return
@@ -412,7 +438,10 @@ func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan
return
}
if !plan.VpcId.Equal(state.VpcId) {
if !plan.VpcId.Equal(state.VpcId) || !plan.VpcIpv4Address.Equal(state.VpcIpv4Address) {
if config.VpcIpv4Address.IsNull() {
plan.VpcIpv4Address = types.StringUnknown()
}
plan.PrivateIPv4Addresses = types.ListUnknown(plan.PrivateIPv4Addresses.ElementType(ctx))
}
@@ -580,7 +609,9 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
if !data.Disk.IsNull() && !data.Disk.IsUnknown() {
body.Options.Disk = data.Disk.ValueInt32Pointer()
}
if !data.VpcIpv4Address.IsNull() && !data.VpcIpv4Address.IsUnknown() {
body.VpcIpv4Address = data.VpcIpv4Address.ValueStringPointer()
}
if data.Password.IsNull() {
data.Password = types.StringNull()
} else {
@@ -625,14 +656,14 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
data.Id = types.Int64Value(serverResp.JSON200.Server.Id)
data.Name = types.StringValue(serverResp.JSON200.Server.Name)
data.Image = types.StringPointerValue(serverResp.JSON200.Server.Image.Slug)
data.Image = types.StringValue(*serverResp.JSON200.Server.Image.Slug)
data.Region = types.StringValue(serverResp.JSON200.Server.Region.Slug)
data.Size = types.StringValue(serverResp.JSON200.Server.Size.Slug)
data.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
data.Ipv6 = types.BoolValue(len(serverResp.JSON200.Server.Networks.V6) > 0)
data.PortBlocking = types.BoolValue(serverResp.JSON200.Server.Networks.PortBlocking)
data.VpcId = types.Int64PointerValue(serverResp.JSON200.Server.VpcId)
data.Permalink = types.StringPointerValue(serverResp.JSON200.Server.Permalink)
data.Permalink = types.StringValue(*serverResp.JSON200.Server.Permalink)
data.PasswordChangeSupported = types.BoolValue(serverResp.JSON200.Server.PasswordChangeSupported)
data.Memory = types.Int32Value(serverResp.JSON200.Server.Memory)
data.Disk = types.Int32Value(serverResp.JSON200.Server.Disk)
@@ -640,6 +671,18 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
serverRespSourceDestCheck := types.BoolPointerValue(serverResp.JSON200.Server.Networks.SourceAndDestinationCheck)
data.SourceAndDestinationCheck = serverRespSourceDestCheck
if serverResp.JSON200.Server.VpcId == nil {
data.VpcIpv4Address = types.StringNull()
} else if len(serverResp.JSON200.Server.Networks.V4) > 0 {
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
// Skip addresses in 172.21.0.0/16, these are BL internal addresses that are not part of the user's VPC
if v4address.Type == "private" && !strings.HasPrefix(v4address.IpAddress, "172.21.") {
data.VpcIpv4Address = types.StringValue(v4address.IpAddress)
break
}
}
}
advFeat := serverResp.JSON200.Server.AdvancedFeatures.EnabledAdvancedFeatures
data.AdvancedFeatures, diags = resources.NewAdvancedFeaturesValue(
resources.AdvancedFeaturesValue{}.AttributeTypes(ctx),
@@ -843,6 +886,55 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
}
}
// Change VPC IPv4 address
if !plan.VpcIpv4Address.Equal(state.VpcIpv4Address) && !plan.VpcIpv4Address.IsNull() && !plan.VpcIpv4Address.IsUnknown() {
// If VPC was just changed, IP address needs to be fetched so it can be sent in the request
if refreshNeeded {
diag := r.fetchServerResourceState(ctx, &state)
resp.Diagnostics.Append(diag...)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
}
// We might have got lucky - the new randomly assigned IP address might be the one we want, so check before making API call
if state.VpcIpv4Address.ValueString() != plan.VpcIpv4Address.ValueString() {
vpcIpv4Resp, err := r.bc.client.PostServersServerIdActionsChangeVpcIpv4WithResponse(
ctx,
state.Id.ValueInt64(),
binarylane.PostServersServerIdActionsChangeVpcIpv4JSONRequestBody{
Type: "change_vpc_ipv4",
CurrentIpv4Address: state.VpcIpv4Address.ValueString(),
NewIpv4Address: plan.VpcIpv4Address.ValueString(),
},
)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error changing VPC IPv4 address for server: server_id=%s", state.Id.String()),
err.Error())
return
}
if vpcIpv4Resp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code changing VPC IPv4 address for server",
fmt.Sprintf("Received %s changing VPC IPv4 address for server: server_id=%s. Details: %s", vpcIpv4Resp.Status(), state.Id.String(), vpcIpv4Resp.Body))
return
}
err = r.waitForServerAction(ctx, state.Id.ValueInt64(), vpcIpv4Resp.JSON200.Action.Id)
if err != nil {
resp.Diagnostics.AddError("Error waiting for VPC IPv4 address to change", err.Error())
return
}
state.VpcIpv4Address = plan.VpcIpv4Address
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
refreshNeeded = true // Refresh to populate private_ipv4_addresses
if resp.Diagnostics.HasError() {
return
}
}
}
// Resize operation
if !plan.Size.Equal(state.Size) ||
!plan.Memory.IsNull() && !plan.Memory.IsUnknown() && !plan.Memory.Equal(state.Memory) ||
@@ -936,7 +1028,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
}
}
// Change ipv6
// Change IPv6
if !plan.Ipv6.Equal(state.Ipv6) {
ipv6Resp, err := r.bc.client.PostServersServerIdActionsChangeIpv6WithResponse(
ctx,
@@ -1329,13 +1421,13 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
state.Id = types.Int64Value(serverResp.JSON200.Server.Id)
state.Name = types.StringValue(serverResp.JSON200.Server.Name)
state.Image = types.StringPointerValue(serverResp.JSON200.Server.Image.Slug)
state.Image = types.StringValue(*serverResp.JSON200.Server.Image.Slug)
state.Region = types.StringValue(serverResp.JSON200.Server.Region.Slug)
state.Size = types.StringValue(serverResp.JSON200.Server.Size.Slug)
state.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
state.PortBlocking = types.BoolValue(serverResp.JSON200.Server.Networks.PortBlocking)
state.VpcId = types.Int64PointerValue(serverResp.JSON200.Server.VpcId)
state.Permalink = types.StringPointerValue(serverResp.JSON200.Server.Permalink)
state.Permalink = types.StringValue(*serverResp.JSON200.Server.Permalink)
state.PasswordChangeSupported = types.BoolValue(serverResp.JSON200.Server.PasswordChangeSupported)
state.SourceAndDestinationCheck = types.BoolPointerValue(serverResp.JSON200.Server.Networks.SourceAndDestinationCheck)
state.Memory = types.Int32Value(serverResp.JSON200.Server.Memory)
@@ -1343,6 +1435,18 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
state.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
state.Ipv6 = types.BoolValue(len(serverResp.JSON200.Server.Networks.V6) > 0)
if serverResp.JSON200.Server.VpcId == nil {
state.VpcIpv4Address = types.StringNull()
} else if len(serverResp.JSON200.Server.Networks.V4) > 0 {
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
// Skip addresses in 172.21.0.0/16, these are BL internal addresses that are not part of the user's VPC
if v4address.Type == "private" && !strings.HasPrefix(v4address.IpAddress, "172.21.") {
state.VpcIpv4Address = types.StringValue(v4address.IpAddress)
break
}
}
}
advFeat := serverResp.JSON200.Server.AdvancedFeatures.EnabledAdvancedFeatures
state.AdvancedFeatures, diags = resources.NewAdvancedFeaturesValue(
resources.AdvancedFeaturesValue{}.AttributeTypes(ctx),
@@ -1366,9 +1470,10 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
privateIpv4Addresses := []string{}
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
if v4address.Type == "public" {
switch v4address.Type {
case "public":
publicIpv4Addresses = append(publicIpv4Addresses, v4address.IpAddress)
} else if v4address.Type == "private" {
case "private":
privateIpv4Addresses = append(privateIpv4Addresses, v4address.IpAddress)
}
}
@@ -1395,9 +1500,10 @@ func (r *serverResource) fetchServerResourceState(ctx context.Context, state *se
privateIpv6Addresses := []string{}
for _, v6address := range serverResp.JSON200.Server.Networks.V6 {
if v6address.Type == "public" {
switch v6address.Type {
case "public":
publicIpv6Addresses = append(publicIpv6Addresses, v6address.IpAddress)
} else {
case "private":
privateIpv6Addresses = append(privateIpv6Addresses, v6address.IpAddress)
}
}
+150 -1
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"log"
"net/http"
"regexp"
"strings"
"terraform-provider-binarylane/internal/binarylane"
"testing"
@@ -81,6 +82,7 @@ data "binarylane_server" "test" {
resource.TestCheckResourceAttr("binarylane_server.test", "memory", "1152"),
resource.TestCheckResourceAttr("binarylane_server.test", "disk", "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"),
resource.TestCheckResourceAttr("binarylane_server.test", "password", password1),
resource.TestCheckResourceAttr("binarylane_server.test", "user_data", `#cloud-config
@@ -114,6 +116,7 @@ echo "Hello World" > /var/tmp/output.txt
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"),
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "vpc_ipv4_address"),
resource.TestCheckResourceAttrPair("data.binarylane_server.test", "permalink", "binarylane_server.test", "permalink"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "user_data", `#cloud-config
echo "Hello World" > /var/tmp/output.txt
@@ -321,6 +324,153 @@ resource "binarylane_server" "test" {
})
}
func TestServerVpcIpv4Change(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{
// Plan should fail if vpc_ipv4_address is set without vpc_id
{
Config: providerConfig + `
resource "binarylane_server" "test" {
name = "tf-test-server-vpcipv4-1"
region = "per"
image = "debian-11"
size = "std-min"
public_ipv4_count = 0
password = "` + password + `"
vpc_ipv4_address = "10.241.69.69"
}
`,
PlanOnly: true,
ExpectError: regexp.MustCompile("Attribute \"vpc_id\" must be specified when \"vpc_ipv4_address\" is specified"),
},
// Setup
{
Config: providerConfig + `
resource "binarylane_vpc" "test1" {
name = "tf-test-server-vpcipv4-1"
ip_range = "10.240.0.0/16"
}
resource "binarylane_vpc" "test2" {
name = "tf-test-server-vpcipv4-2"
ip_range = "10.241.0.0/16"
}
resource "binarylane_server" "test" {
name = "tf-test-server-vpcipv4-1"
region = "per"
image = "debian-11"
size = "std-min"
public_ipv4_count = 0
password = "` + password + `"
vpc_id = binarylane_vpc.test1.id
vpc_ipv4_address = "10.240.0.69"
}
data "binarylane_server" "test" {
depends_on = [binarylane_server.test]
id = binarylane_server.test.id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair("binarylane_server.test", "vpc_id", "binarylane_vpc.test1", "id"),
resource.TestCheckResourceAttr("binarylane_server.test", "vpc_ipv4_address", "10.240.0.69"),
resource.TestCheckResourceAttr("binarylane_server.test", "private_ipv4_addresses.#", "1"),
resource.TestCheckResourceAttr("binarylane_server.test", "private_ipv4_addresses.0", "10.240.0.69"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "vpc_ipv4_address", "10.240.0.69"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "private_ipv4_addresses.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "private_ipv4_addresses.0", "10.240.0.69"),
),
},
// Change IP without changing VPC
{
Config: providerConfig + `
resource "binarylane_vpc" "test1" {
name = "tf-test-server-vpcipv4-1"
ip_range = "10.240.0.0/16"
}
resource "binarylane_vpc" "test2" {
name = "tf-test-server-vpcipv4-2"
ip_range = "10.241.0.0/16"
}
resource "binarylane_server" "test" {
name = "tf-test-server-vpcipv4-1"
region = "per"
image = "debian-11"
size = "std-min"
public_ipv4_count = 0
password = "` + password + `"
vpc_id = binarylane_vpc.test1.id
vpc_ipv4_address = "10.240.4.20"
}
data "binarylane_server" "test" {
depends_on = [binarylane_server.test]
id = binarylane_server.test.id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair("binarylane_server.test", "vpc_id", "binarylane_vpc.test1", "id"),
resource.TestCheckResourceAttr("binarylane_server.test", "vpc_ipv4_address", "10.240.4.20"),
resource.TestCheckResourceAttr("binarylane_server.test", "private_ipv4_addresses.#", "1"),
resource.TestCheckResourceAttr("binarylane_server.test", "private_ipv4_addresses.0", "10.240.4.20"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "vpc_ipv4_address", "10.240.4.20"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "private_ipv4_addresses.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "private_ipv4_addresses.0", "10.240.4.20"),
),
},
// Change VPC (should also change IP)
{
Config: providerConfig + `
resource "binarylane_vpc" "test1" {
name = "tf-test-server-vpcipv4-1"
ip_range = "10.240.0.0/16"
}
resource "binarylane_vpc" "test2" {
name = "tf-test-server-vpcipv4-2"
ip_range = "10.241.0.0/16"
}
resource "binarylane_server" "test" {
name = "tf-test-server-vpcipv4-1"
region = "per"
image = "debian-11"
size = "std-min"
public_ipv4_count = 0
password = "` + password + `"
vpc_id = binarylane_vpc.test2.id
vpc_ipv4_address = "10.241.69.69"
}
data "binarylane_server" "test" {
depends_on = [binarylane_server.test]
id = binarylane_server.test.id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair("binarylane_server.test", "vpc_id", "binarylane_vpc.test2", "id"),
resource.TestCheckResourceAttr("binarylane_server.test", "vpc_ipv4_address", "10.241.69.69"),
resource.TestCheckResourceAttr("binarylane_server.test", "private_ipv4_addresses.#", "1"),
resource.TestCheckResourceAttr("binarylane_server.test", "private_ipv4_addresses.0", "10.241.69.69"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "vpc_ipv4_address", "10.241.69.69"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "private_ipv4_addresses.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "private_ipv4_addresses.0", "10.241.69.69"),
),
},
},
})
}
func GenerateTestPassword(t *testing.T) string {
t.Helper()
pwBytes := make([]byte, 12)
@@ -336,7 +486,6 @@ func init() {
Name: "server",
F: func(_ string) error {
client, err := binarylane.NewClientWithDefaultConfig()
if err != nil {
return fmt.Errorf("Error creating Binary Lane API client: %w", err)
}
+1 -1
View File
@@ -83,7 +83,7 @@ func (d *sizesDataSource) Read(ctx context.Context, req datasource.ReadRequest,
}
regResults = append(regResults, listResp.JSON200.Sizes...)
if listResp.JSON200.Links == nil || listResp.JSON200.Links.Pages.Next == nil {
if listResp.JSON200.Links == nil || listResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
+2 -2
View File
@@ -318,7 +318,7 @@ func (r *sshKeyResource) ImportState(
break
}
}
if sshResp.JSON200.Links == nil || sshResp.JSON200.Links.Pages.Next == nil {
if sshResp.JSON200.Links == nil || sshResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
@@ -326,7 +326,7 @@ func (r *sshKeyResource) ImportState(
page++
}
if false {
if sshKey.Id == 0 {
resp.Diagnostics.AddError(
"Could not find SSH key by fingerprint",
fmt.Sprintf("Error finding SSH key: fingerprint=%s", req.ID),
+8 -8
View File
@@ -151,14 +151,14 @@ func init() {
for _, k := range keys {
if strings.HasPrefix(*k.Name, "tf-test-") {
deleteResp, err := client.DeleteAccountKeysKeyIdWithResponse(ctx, int(k.Id))
if err != nil {
return fmt.Errorf("Error deleting SSH key %d for test sweep: %w", k.Id, err)
}
if deleteResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting SSH key %d for test sweep: %s", deleteResp.StatusCode(), k.Id, deleteResp.Body)
}
log.Println("Deleted SSH key for test sweep:", k.Id)
deleteResp, err := client.DeleteAccountKeysKeyIdWithResponse(ctx, int(k.Id))
if err != nil {
return fmt.Errorf("Error deleting SSH key %d for test sweep: %w", k.Id, err)
}
if deleteResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting SSH key %d for test sweep: %s", deleteResp.StatusCode(), k.Id, deleteResp.Body)
}
log.Println("Deleted SSH key for test sweep:", k.Id)
}
}
if listResp.JSON200.Links == nil || listResp.JSON200.Links.Pages.Next == nil {
+10 -10
View File
@@ -156,17 +156,17 @@ func init() {
}
vpcs := listResp.JSON200.Vpcs
for _, vpc := range vpcs {
if strings.HasPrefix(vpc.Name, "tf-test-") {
for _, vpc := range vpcs {
if strings.HasPrefix(vpc.Name, "tf-test-") {
deleteResp, err := client.DeleteVpcsVpcIdWithResponse(ctx, vpc.Id)
if err != nil {
return fmt.Errorf("Error deleting VPC %d in test sweep: %w", vpc.Id, err)
}
if deleteResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting VPC %d for test sweep: %s", deleteResp.StatusCode(), vpc.Id, deleteResp.Body)
}
log.Println("Deleted VPC for test sweep:", vpc.Id)
deleteResp, err := client.DeleteVpcsVpcIdWithResponse(ctx, vpc.Id)
if err != nil {
return fmt.Errorf("Error deleting VPC %d in test sweep: %w", vpc.Id, err)
}
if deleteResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting VPC %d for test sweep: %s", deleteResp.StatusCode(), vpc.Id, deleteResp.Body)
}
log.Println("Deleted VPC for test sweep:", vpc.Id)
}
}
if listResp.JSON200.Links == nil || listResp.JSON200.Links.Pages.Next == nil {
@@ -161,6 +161,12 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
Description: "Leave null to use default (public) network for the selected region.",
MarkdownDescription: "Leave null to use default (public) network for the selected region.",
},
"vpc_ipv4_address": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "If provided this will be the Ipv4 address for the server's private VPC network adapter. If this is null an unused Ipv4 address will be assigned. This field is only valid when VpcId is provided.",
MarkdownDescription: "If provided this will be the Ipv4 address for the server's private VPC network adapter. If this is null an unused Ipv4 address will be assigned. This field is only valid when VpcId is provided.",
},
},
}
}
@@ -178,6 +184,7 @@ type ServerModel struct {
SshKeys types.List `tfsdk:"ssh_keys"`
UserData types.String `tfsdk:"user_data"`
VpcId types.Int64 `tfsdk:"vpc_id"`
VpcIpv4Address types.String `tfsdk:"vpc_ipv4_address"`
}
var _ basetypes.ObjectTypable = AdvancedFeaturesType{}
+9 -4
View File
@@ -2,13 +2,13 @@
"openapi": "3.0.4",
"info": {
"title": "BinaryLane API",
"description": "\n\n# Introduction\n\nThis API is used by BinaryLane's 'mPanel' website, so you'll find everything you need here to automate your use of BinaryLane or develop tools for others. You could even build your own version of 'mPanel' if you wanted!\n\n\n## Getting Started Guide\n\nWe are working on a complete 'Getting Started' guide as well as this introductory section.\n\n\n# Developer Preview Notes\n\n**Warning: this specification is currently in active development.** Until this warning is removed breaking changes are possible without the version changing.\nHowever, this API is relatively mature and we are actively developing a new website and other tools against this API so breaking changes should be rare at this point.\n\nWe currently have a CLI (command line interface) tool in development and are in the early stage of planning other tools.\n\nWe welcome all feedback from API users at the support email address above.\n\n\n## Active Development Status\n\nThis is the list of BinaryLane features that are not yet available through the API but are currently under active development.\nIf you need an ETA on when these will be available please contact support.\n\n- Domains\n\n\n## Future Development Status\n\nThis is the list of BinaryLane features that are not yet available through the API and are not currently in active development. This list is in a rough order of priority.\nIf you need an ETA or want to ask for increased priority on any of these please contact support, we are happy to hear any feedback on this.\n\n- Account Updates\n- Payments\n\n\n\n# API Tools\n\nThis section will include links to tools to assist with consuming this API. We have a CLI tool and a GO library in active development, and are planning libraries for other languages.\n\n\n# Version Information\n\nThis section will contain a link to our changelog once the developer preview period is complete.\n\n",
"description": "# Introduction\n\nThis API is used by BinaryLane's 'mPanel' website, so you'll find everything you need here to automate your use of BinaryLane or develop tools for others. You could even build your own version of 'mPanel' if you wanted!\n\n\n## Getting Started Guide\n\nYou can find a detailed guide to using the API to manage your servers <a href='https://support.binarylane.com.au/support/solutions/articles/11000125203-getting-started-with-binarylane-api' target='_blank' rel='noopener noreferrer'>here</a>.\n\n\n# API Tools\n\nWe have created a CLI tool to interact with the API. You can find the repository <a href='https://github.com/binarylane/binarylane-cli' target='_blank' rel='noopener noreferrer'>here</a>.\n\n\n# Developer Preview Notes\n\n**Warning: this specification is currently in active development.** Until this warning is removed breaking changes are possible without the version changing.\nHowever, this API is mature and we have a fully functioning new version of mPanel that uses it, so breaking changes will be very limited.\n\nWe welcome all feedback from API users at the support email address above.\n\n\n# Version Information\n\nThis section will contain a link to our changelog once the developer preview period is complete.",
"contact": {
"name": "BinaryLane Support",
"url": "https://support.binarylane.com.au",
"url": "https://support.binarylane.com.au/support/home",
"email": "support@binarylane.com.au"
},
"version": "0.33.1",
"version": "0.34.0",
"x-logo": {
"url": "https://www.binarylane.com.au/res/images/binarylane/logo.png",
"backgroundColor": "#000000",
@@ -5929,7 +5929,7 @@
},
"two_factor_authentication_enabled": {
"type": "boolean",
"description": "Whether this account has enabled two factor authentication."
"description": "Whether this account has enabled app-based two factor authentication."
},
"status": {
"allOf": [
@@ -7610,6 +7610,11 @@
"format": "int64",
"nullable": true
},
"vpc_ipv4_address": {
"type": "string",
"description": "If provided this will be the Ipv4 address for the server's private VPC network adapter. If this is null an unused Ipv4 address will be assigned. This field is only valid when VpcId is provided.",
"nullable": true
},
"ssh_keys": {
"type": "array",
"items": {
+7
View File
@@ -929,6 +929,13 @@
"description": "Leave null to use default (public) network for the selected region."
}
},
{
"name": "vpc_ipv4_address",
"string": {
"computed_optional_required": "computed_optional",
"description": "If provided this will be the Ipv4 address for the server's private VPC network adapter. If this is null an unused Ipv4 address will be assigned. This field is only valid when VpcId is provided."
}
},
{
"name": "id",
"int64": {