mirror of
https://github.com/oscarhermoso/terraform-provider-binarylane
synced 2026-10-19 21:49:15 +11:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f73528497f |
@@ -0,0 +1,18 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
- package-ecosystem: 'gomod'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
- package-ecosystem: 'terraform'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
- package-ecosystem: 'docker'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
+13
-18
@@ -12,7 +12,6 @@ Based on [this example from the terraform docs](https://developer.hashicorp.com/
|
||||
|
||||
```hcl
|
||||
provider_installation {
|
||||
|
||||
dev_overrides {
|
||||
# Example GOBIN path, will need to be replaced with your own GOBIN path. Default is $GOPATH/bin
|
||||
"oscarhermoso/binarylane" = "/home/oscarhermoso/Git/terraform-provider-binarylane/bin"
|
||||
@@ -67,20 +66,20 @@ tfplugingen-framework scaffold data-source \
|
||||
Define the interfaces that the resource/data source should implement:
|
||||
|
||||
```diff
|
||||
- var _ resource.Resource = (*replaceMeResource)(nil)
|
||||
- var _ resource.Resource = (*exampleResource)(nil)
|
||||
+ // Ensure the implementation satisfies the expected interfaces.
|
||||
+ var (
|
||||
+ _ resource.Resource = &replaceMeResource{}
|
||||
+ _ resource.ResourceWithConfigure = &replaceMeResource{}
|
||||
+ // _ resource.ResourceWithImportState = &replaceMeResource{}
|
||||
+ _ resource.Resource = &exampleResource{}
|
||||
+ _ resource.ResourceWithConfigure = &exampleResource{}
|
||||
+ // _ resource.ResourceWithImportState = &exampleResource{}
|
||||
+ )
|
||||
```
|
||||
|
||||
Pass the API client to the resource:
|
||||
|
||||
```diff
|
||||
- type replaceMeResource struct{}
|
||||
+ type replaceMeResource struct {
|
||||
- type exampleResource struct{}
|
||||
+ type exampleResource struct {
|
||||
+ bc *BinarylaneClient
|
||||
+ }
|
||||
```
|
||||
@@ -88,7 +87,7 @@ Pass the API client to the resource:
|
||||
Add a `Configure` method to the resource:
|
||||
|
||||
```diff
|
||||
+ func (d *replaceMeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
+ func (d *exampleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
+ if req.ProviderData == nil {
|
||||
+ return
|
||||
+ }
|
||||
@@ -107,18 +106,14 @@ Add a `Configure` method to the resource:
|
||||
+ }
|
||||
```
|
||||
|
||||
Delete the local `*ResourceModel` struct, and import the generated `resources.*Model` instead.
|
||||
Extend the resource model from the generated `resources.*Model`.
|
||||
|
||||
```diff
|
||||
- type replaceMeResourceModel struct {
|
||||
type exampleResourceModel struct {
|
||||
- Id types.String `tfsdk:"id"`
|
||||
- }
|
||||
```
|
||||
|
||||
```diff
|
||||
func (r *replaceMeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
- var data replaceMeResourceModel
|
||||
+ var data resources.ReplaceMeModel
|
||||
+ *resources.ExampleModel
|
||||
+ // Add any additional fields here
|
||||
}
|
||||
```
|
||||
|
||||
Add the new data source/resource to `provider.go`:
|
||||
@@ -126,6 +121,6 @@ Add the new data source/resource to `provider.go`:
|
||||
```diff
|
||||
return []func() resource.Resource{
|
||||
# ...
|
||||
+ NewReplaceMeResource,
|
||||
+ NewExampleResource,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,6 @@ See the examples in the [examples directory](./examples/basic/main.tf).
|
||||
|
||||
```terraform
|
||||
resource "binarylane_server" "example" {
|
||||
name = "example"
|
||||
region = "per"
|
||||
image = "ubuntu-24.04"
|
||||
size = "std-min"
|
||||
|
||||
@@ -28,3 +28,4 @@ Provides a Binary Lane Server resource. This can be used to create and delete se
|
||||
- `name` (String) The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name.
|
||||
- `password` (String, Sensitive) 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 (check server.password_change_supported via the servers endpoint). If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.
|
||||
- `user_data` (String) If provided this will be used to initialise the new server. This must be left null if the Image does not support UserData, see DistributionInfo.Features for more information.
|
||||
- `wait_for_create` (Number) The number of seconds to wait for the server to be created, after which, a timeout error will be reported. If `wait_seconds` is left empty or set to 0, Terraform will succeed without waiting for the server creation to complete.
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
#cloud-config
|
||||
bootcmd:
|
||||
# By default, the provider won't wait for the server installation to complete.
|
||||
#
|
||||
# If you want Terraform to wait until the server has fully provisioned, set
|
||||
# the `wait_for_create` attribute on the `binarylane_server` resource.
|
||||
#
|
||||
# If you have additional installation steps that need to be awaited, define
|
||||
# these in the `bootcmd` section and they will also be awaited.
|
||||
#
|
||||
- [cloud-init-per, once, 'example-installing', sh, -c, 'sleep 60'] # pretend this is a slow installation step
|
||||
- [
|
||||
cloud-init-per,
|
||||
once,
|
||||
'example-done',
|
||||
sh,
|
||||
-c,
|
||||
'echo Finished installing, Terraform will now proceed:`date` >> /var/tmp/user_data.log',
|
||||
]
|
||||
|
||||
runcmd:
|
||||
- echo "Hello World" > /var/tmp/output.txt
|
||||
- [sh, -c, 'sleep 99999'] # The provider won't wait for `runcmd` to complete
|
||||
|
||||
@@ -12,9 +12,10 @@ provider "binarylane" {
|
||||
}
|
||||
|
||||
resource "binarylane_server" "example" {
|
||||
name = "tf-cloud-init-example"
|
||||
region = "per"
|
||||
image = "ubuntu-24.04"
|
||||
size = "std-min"
|
||||
user_data = file("./cloud-config.yml")
|
||||
name = "tf-cloud-init-example"
|
||||
region = "per"
|
||||
image = "ubuntu-24.04"
|
||||
size = "std-min"
|
||||
user_data = file("./cloud-config.yml")
|
||||
wait_for_create = 300 # 5 mins
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
#cloud-config
|
||||
write_files:
|
||||
- path: /etc/nixos/host.nix
|
||||
permissions: "0644"
|
||||
permissions: '0644'
|
||||
content: |
|
||||
{pkgs, ...}:
|
||||
{
|
||||
environment.systemPackages = with pkgs; [ vim ];
|
||||
}
|
||||
runcmd:
|
||||
- curl https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect | NIXOS_IMPORT=./host.nix NIX_CHANNEL=nixos-23.05 bash 2>&1 | tee /var/tmp/infect.log
|
||||
bootcmd:
|
||||
- cloud-init-per once "infect" curl https://raw.githubusercontent.com/elitak/nixos-infect/master/nixos-infect | NIXOS_IMPORT=./host.nix NIX_CHANNEL=nixos-23.05 bash 2>&1 | tee /var/tmp/infect.log
|
||||
|
||||
@@ -12,9 +12,10 @@ provider "binarylane" {
|
||||
}
|
||||
|
||||
resource "binarylane_server" "example" {
|
||||
name = "tf-nix-example"
|
||||
region = "per"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
user_data = file("./cloud-config.yml")
|
||||
name = "tf-nix-example"
|
||||
region = "per"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
user_data = file("./cloud-config.yml")
|
||||
wait_for_create = 300 # 5 mins
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"net/http"
|
||||
"terraform-provider-binarylane/internal/binarylane"
|
||||
"terraform-provider-binarylane/internal/resources"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
@@ -32,6 +34,11 @@ type serverResource struct {
|
||||
bc *BinarylaneClient
|
||||
}
|
||||
|
||||
type serverModel struct {
|
||||
resources.ServerModel
|
||||
WaitForCreateSeconds int32 `tfsdk:"wait_for_create"`
|
||||
}
|
||||
|
||||
func (d *serverResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
@@ -87,10 +94,22 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
}
|
||||
|
||||
// Additional attributes
|
||||
waitDescription := "The number of seconds to wait for the server to be created, after which, a timeout error will " +
|
||||
"be reported. If `wait_seconds` is left empty or set to 0, Terraform will succeed without waiting for the " +
|
||||
"server creation to complete."
|
||||
resp.Schema.Attributes["wait_for_create"] = &schema.Int32Attribute{
|
||||
Description: waitDescription,
|
||||
MarkdownDescription: waitDescription,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: int32default.StaticInt32(0),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var data resources.ServerModel
|
||||
var data serverModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
diags := req.Plan.Get(ctx, &data)
|
||||
@@ -132,24 +151,66 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
|
||||
return
|
||||
}
|
||||
|
||||
assignInt64(serverResp.JSON200.Server.Id, &data.Id)
|
||||
assignStr(serverResp.JSON200.Server.Name, &data.Name)
|
||||
assignStr(serverResp.JSON200.Server.Image.Slug, &data.Image)
|
||||
assignStr(serverResp.JSON200.Server.Region.Slug, &data.Region)
|
||||
assignStr(serverResp.JSON200.Server.Size.Slug, &data.Size)
|
||||
data.Id = types.Int64Value(*serverResp.JSON200.Server.Id)
|
||||
data.Name = types.StringValue(*serverResp.JSON200.Server.Name)
|
||||
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)
|
||||
// assignBool(&serverResp.JSON200.Server.Networks.PortBlocking, &plan.PortBlocking)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Save data into Terraform state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
|
||||
if data.WaitForCreateSeconds <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for server to be ready
|
||||
var createActionId int64
|
||||
for _, action := range *serverResp.JSON200.Links.Actions {
|
||||
if *action.Rel == "create" {
|
||||
createActionId = *action.Id
|
||||
break
|
||||
}
|
||||
}
|
||||
if createActionId == 0 {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unable to wait for server to be created, links.actions with rel=create missing from response",
|
||||
fmt.Sprintf("Received %s creating new server: name=%s. Details: %s", serverResp.Status(), data.Name.ValueString(), serverResp.Body))
|
||||
return
|
||||
}
|
||||
|
||||
timeLimit := time.Now().Add(time.Duration(data.WaitForCreateSeconds) * time.Second)
|
||||
for {
|
||||
tflog.Info(ctx, "Waiting for server to be ready...")
|
||||
|
||||
readyResp, err := r.bc.client.GetServersServerIdActionsActionIdWithResponse(ctx, data.Id.ValueInt64(), createActionId)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error waiting for server to be ready", err.Error())
|
||||
return
|
||||
}
|
||||
if readyResp.StatusCode() == http.StatusOK && readyResp.JSON200.Action.CompletedAt != nil {
|
||||
tflog.Info(ctx, "Server is ready")
|
||||
break
|
||||
}
|
||||
if time.Now().After(timeLimit) {
|
||||
resp.Diagnostics.AddError(
|
||||
"Timed out waiting for server to be ready",
|
||||
fmt.Sprintf(
|
||||
"Timed out waiting for server to be created, as `wait_for_create` was surpassed without "+
|
||||
"recieving a `completed_at` in response: name=%s, status=%s, details: %s",
|
||||
data.Name.ValueString(), readyResp.Status(), readyResp.Body,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
tflog.Debug(ctx, fmt.Sprintf("Waiting for server to be ready: name=%s, status=%s, details: %s", data.Name.ValueString(), readyResp.Status(), readyResp.Body))
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var data resources.ServerModel
|
||||
var data serverModel
|
||||
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
@@ -191,7 +252,7 @@ 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 data resources.ServerModel
|
||||
var data serverModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
@@ -208,7 +269,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
|
||||
}
|
||||
|
||||
func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var data resources.ServerModel
|
||||
var data serverModel
|
||||
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
func TestServerResource(t *testing.T) {
|
||||
// Must assign a password to the server or Binary Lane will send emails
|
||||
pw_bytes := make([]byte, 12)
|
||||
rand.Read(pw_bytes)
|
||||
password := base64.URLEncoding.EncodeToString(pw_bytes)
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
@@ -16,8 +23,9 @@ func TestServerResource(t *testing.T) {
|
||||
resource "binarylane_server" "test" {
|
||||
name = "tf-server-resource-test"
|
||||
region = "per"
|
||||
image = "ubuntu-24.04"
|
||||
image = "debian-12"
|
||||
size = "std-min"
|
||||
password = "` + password + `"
|
||||
user_data = <<EOT
|
||||
#cloud-config
|
||||
echo "Hello World" > /var/tmp/output.txt
|
||||
@@ -32,61 +40,24 @@ data "binarylane_server" "test" {
|
||||
`,
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
// Verify resource values
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "id"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "name", "tf-server-resource-test"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "region", "per"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "image", "ubuntu-24.04"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "image", "debian-12"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "size", "std-min"),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "password", password),
|
||||
resource.TestCheckResourceAttr("binarylane_server.test", "user_data", `#cloud-config
|
||||
echo "Hello World" > /var/tmp/output.txt
|
||||
`),
|
||||
resource.TestCheckResourceAttrSet("binarylane_server.test", "id"),
|
||||
|
||||
// Verify data source values
|
||||
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "id"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "name", "tf-server-resource-test"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "region", "per"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "image", "ubuntu-24.04"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "image", "debian-12"),
|
||||
resource.TestCheckResourceAttr("data.binarylane_server.test", "size", "std-min"),
|
||||
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "id"),
|
||||
// resource.TestCheckResourceAttr("data.binarylane_server.test", "user_data", `
|
||||
// #cloud-config
|
||||
// echo "Hello World" > /var/tmp/output.txt
|
||||
// `),
|
||||
),
|
||||
},
|
||||
// ImportState testing
|
||||
// {
|
||||
// ResourceName: "binarylane_server.test",
|
||||
// ImportState: true,
|
||||
// ImportStateVerify: true,
|
||||
// ImportStateVerifyIgnore: []string{}, // nothing to ignore
|
||||
// },
|
||||
// TODO: Update and Read testing
|
||||
// {
|
||||
// Config: providerConfig + `
|
||||
// resource "binarylane_server" "test" {
|
||||
// items = [
|
||||
// {
|
||||
// coffee = {
|
||||
// id = 2
|
||||
// }
|
||||
// quantity = 2
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
// `,
|
||||
// Check: resource.ComposeAggregateTestCheckFunc(
|
||||
// // Verify first order item updated
|
||||
// resource.TestCheckResourceAttr("binarylane_server.test", "items.0.quantity", "2"),
|
||||
// resource.TestCheckResourceAttr("binarylane_server.test", "items.0.coffee.id", "2"),
|
||||
// // Verify first coffee item has Computed attributes updated.
|
||||
// resource.TestCheckResourceAttr("binarylane_server.test", "items.0.coffee.description", ""),
|
||||
// resource.TestCheckResourceAttr("binarylane_server.test", "items.0.coffee.image", "/packer.png"),
|
||||
// resource.TestCheckResourceAttr("binarylane_server.test", "items.0.coffee.name", "Packer Spiced Latte"),
|
||||
// resource.TestCheckResourceAttr("binarylane_server.test", "items.0.coffee.price", "350"),
|
||||
// resource.TestCheckResourceAttr("binarylane_server.test", "items.0.coffee.teaser", "Packed with goodness to spice up your images"),
|
||||
// ),
|
||||
// },
|
||||
// Delete testing automatically occurs in TestCase
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,40 +6,9 @@ import (
|
||||
d_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
r_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
)
|
||||
|
||||
func assignStr(source *string, target *basetypes.StringValue) {
|
||||
if source != nil {
|
||||
*target = types.StringValue(*source)
|
||||
}
|
||||
}
|
||||
|
||||
// func assignInt(source *int, target *basetypes.Int64Value) {
|
||||
// if source != nil {
|
||||
// *target = types.Int64Value(int64(*source))
|
||||
// }
|
||||
// }
|
||||
|
||||
func assignInt64(source *int64, target *basetypes.Int64Value) {
|
||||
if source != nil {
|
||||
*target = types.Int64Value(int64(*source))
|
||||
}
|
||||
}
|
||||
|
||||
// func assignFloat(source *float32, target *basetypes.Float64Value) {
|
||||
// if source != nil {
|
||||
// *target = types.Float64Value(float64(*source))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func assignBool(source *bool, target *basetypes.BoolValue) {
|
||||
// if source != nil {
|
||||
// *target = types.BoolValue(bool(*source))
|
||||
// }
|
||||
// }
|
||||
|
||||
func convertResourceSchemaToDataSourceSchema(ctx context.Context, rs r_schema.Schema) *d_schema.Schema {
|
||||
ds := d_schema.Schema{
|
||||
Attributes: make(map[string]d_schema.Attribute, len(rs.Attributes)),
|
||||
|
||||
Reference in New Issue
Block a user