Skip to content

Commit

Permalink
🥢 import ssh key by fingerprint, make tests faster (#26)
Browse files Browse the repository at this point in the history
* 🥢 import ssh key by fingerprint, make tests faster

* ⌚ Wait for load balancer creation
  • Loading branch information
oscarhermoso authored Oct 4, 2024
1 parent ffcadb7 commit 1084c27
Show file tree
Hide file tree
Showing 14 changed files with 198 additions and 27 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TF_ACC=1
BINARYLANE_API_TOKEN=
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"go.testEnvFile": "${workspaceFolder}/.env",
}
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ terraform apply


```sh
TF_DEBUG=1 BINARYLANE_API_TOKEN=********* TF_ACC=1 go test -v ./...
cp .env.example .env
# Add your API token to .env
eval export $(cat .env)
go test -v ./internal/provider/...
```

### Update modules
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Images:

```sh
curl -X GET "https://api.binarylane.com.au/v2/images?type=distribution&&page=1&per_page=200" \
-H "Authorization: Bearer **********" > tmp/images.json
-H "Authorization: Bearer $BINARYLANE_API_TOKEN" > tmp/images.json

jq '[ .images[] | .slug ] | sort' tmp/images.json
```
Expand Down Expand Up @@ -81,7 +81,7 @@ Regions:

```sh
curl -X GET "https://api.binarylane.com.au/v2/regions" \
-H "Authorization: Bearer **********"" > tmp/regions.json
-H "Authorization: Bearer $BINARYLANE_API_TOKEN" > tmp/regions.json

jq '[ .regions[] | .slug ] | sort' tmp/regions.json
```
Expand All @@ -103,7 +103,7 @@ jq '[ .regions[] | .slug ] | sort' tmp/regions.json

```sh
curl -X GET "https://api.binarylane.com.au/v2/sizes" \
-H "Authorization: Bearer **********"" > tmp/sizes.json
-H "Authorization: Bearer $BINARYLANE_API_TOKEN" > tmp/sizes.json

jq '[ .sizes[] | .slug ] | sort' tmp/sizes.json
```
Expand Down
1 change: 1 addition & 0 deletions docs/data-sources/ssh_key.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ description: |-
### Read-Only

- `default` (Boolean) Optional: If true this will be added to all new server installations (if we support SSH Key injection for the server's operating system).
- `fingerprint` (String) The fingerprint of the SSH key.
- `name` (String) A name to help you identify the key.
- `public_key` (String) The public key in OpenSSH "authorized_keys" format.
4 changes: 4 additions & 0 deletions docs/resources/ssh_key.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ description: |-

- `default` (Boolean) Optional: If true this will be added to all new server installations (if we support SSH Key injection for the server's operating system).
- `id` (Number) The ID or fingerprint of the SSH Key to fetch.

### Read-Only

- `fingerprint` (String) The fingerprint of the SSH key.
44 changes: 44 additions & 0 deletions internal/provider/load_balancer_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"
"time"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
Expand Down Expand Up @@ -153,6 +154,49 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)

// Wait for server to be ready
var createActionId int64
for _, action := range *lbResp.JSON200.Links.Actions {
if *action.Rel == "create_load_balancer" {
createActionId = *action.Id
break
}
}
if createActionId == 0 {
resp.Diagnostics.AddError(
"Unable to wait for load balancer to be created, links.actions with rel=create_load_balancer missing from response",
fmt.Sprintf("Received %s creating new load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body))
return
}

timeLimit := time.Now().Add(time.Duration(60 * time.Second))
for {
tflog.Info(ctx, "Waiting for load balancer to be ready...")

readyResp, err := r.bc.client.GetActionsActionIdWithResponse(ctx, createActionId)
if err != nil {
resp.Diagnostics.AddError("Error waiting for load balancer to be ready", err.Error())
return
}
if readyResp.StatusCode() == http.StatusOK && readyResp.JSON200.Action.CompletedAt != nil {
tflog.Info(ctx, "Load balancer is ready")
break
}
if time.Now().After(timeLimit) {
resp.Diagnostics.AddError(
"Timed out waiting for load balancer to be ready",
fmt.Sprintf(
"Timed out waiting for load balancer 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 load balancer to be ready: name=%s, status=%s, details: %s", data.Name.ValueString(), readyResp.Status(), readyResp.Body))
time.Sleep(time.Second * 5)
}
}

func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/load_balancer_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestLoadBalancerResource(t *testing.T) {
}
password := base64.URLEncoding.EncodeToString(pw_bytes)

resource.Test(t, resource.TestCase{
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/server_firewall_rules_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestServerFirewallRulesResource(t *testing.T) {
rand.Read(pw_bytes)
password := base64.URLEncoding.EncodeToString(pw_bytes)

resource.Test(t, resource.TestCase{
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/server_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestServerResource(t *testing.T) {
}
password := base64.URLEncoding.EncodeToString(pw_bytes)

resource.Test(t, resource.TestCase{
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
Expand Down
15 changes: 13 additions & 2 deletions internal/provider/ssh_key_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"terraform-provider-binarylane/internal/resources"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

Expand Down Expand Up @@ -59,10 +60,20 @@ func (d *sshKeyDataSource) Schema(ctx context.Context, req datasource.SchemaRequ
}
resp.Schema = *ds
// resp.Schema.Description = "TODO"

// Additional attributes
fingerprintDescription := "The fingerprint of the SSH key."
resp.Schema.Attributes["fingerprint"] = &schema.StringAttribute{
Description: fingerprintDescription,
MarkdownDescription: fingerprintDescription,
Optional: false,
Required: false,
Computed: true,
}
}

func (d *sshKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data resources.SshKeyModel
var data sshKeyModel

// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
Expand All @@ -88,11 +99,11 @@ func (d *sshKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest,
return
}

// Example data value setting
data.Id = types.Int64Value(*sshResp.JSON200.SshKey.Id)
data.Default = types.BoolValue(*sshResp.JSON200.SshKey.Default)
data.Name = types.StringValue(*sshResp.JSON200.SshKey.Name)
data.PublicKey = types.StringValue(*sshResp.JSON200.SshKey.PublicKey)
data.Fingerprint = types.StringValue(*sshResp.JSON200.SshKey.Fingerprint)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
Expand Down
94 changes: 86 additions & 8 deletions internal/provider/ssh_key_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"

Expand Down Expand Up @@ -31,6 +32,11 @@ type sshKeyResource struct {
bc *BinarylaneClient
}

type sshKeyModel struct {
resources.SshKeyModel
Fingerprint types.String `tfsdk:"fingerprint"`
}

func (d *sshKeyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
Expand Down Expand Up @@ -74,10 +80,20 @@ func (r *sshKeyResource) Schema(ctx context.Context, req resource.SchemaRequest,
MarkdownDescription: public_key.GetMarkdownDescription(),
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
}

// Additional attributes
fingerprintDescription := "The fingerprint of the SSH key."
resp.Schema.Attributes["fingerprint"] = &schema.StringAttribute{
Description: fingerprintDescription,
MarkdownDescription: fingerprintDescription,
Optional: false,
Required: false,
Computed: true,
}
}

func (r *sshKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data resources.SshKeyModel
var data sshKeyModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
Expand Down Expand Up @@ -109,6 +125,7 @@ func (r *sshKeyResource) Create(ctx context.Context, req resource.CreateRequest,

// Set data values
data.Id = types.Int64Value(*sshResp.JSON200.SshKey.Id)
data.Fingerprint = types.StringValue(*sshResp.JSON200.SshKey.Fingerprint)

if resp.Diagnostics.HasError() {
return
Expand All @@ -119,7 +136,7 @@ func (r *sshKeyResource) Create(ctx context.Context, req resource.CreateRequest,
}

func (r *sshKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data resources.SshKeyModel
var data sshKeyModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
Expand Down Expand Up @@ -151,14 +168,15 @@ func (r *sshKeyResource) Read(ctx context.Context, req resource.ReadRequest, res
data.Id = types.Int64Value(*sshResp.JSON200.SshKey.Id)
data.Default = types.BoolValue(*sshResp.JSON200.SshKey.Default)
data.Name = types.StringValue(*sshResp.JSON200.SshKey.Name)
// data.PublicKey = types.StringValue(*sshResp.JSON200.SshKey.PublicKey) // don't set or it will force replacement every time
data.PublicKey = types.StringValue(*sshResp.JSON200.SshKey.PublicKey)
data.Fingerprint = types.StringValue(*sshResp.JSON200.SshKey.Fingerprint)

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *sshKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data resources.SshKeyModel
var data sshKeyModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
Expand Down Expand Up @@ -191,7 +209,7 @@ func (r *sshKeyResource) Update(ctx context.Context, req resource.UpdateRequest,
}

func (r *sshKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data resources.SshKeyModel
var data sshKeyModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
Expand All @@ -218,7 +236,67 @@ func (r *sshKeyResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
}

func (r *sshKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Retrieve import ID and save to id attribute
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
func (r *sshKeyResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
// Import by ID
id, err := strconv.ParseInt(req.ID, 10, 32)
if err == nil {
diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(id))
resp.Diagnostics.Append(diags...)
return
}

// Import by name
var page int32 = 1
perPage := int32(200)
var sshKey binarylane.SshKey
var nextPage bool = true

for nextPage { // Need to paginate because the API does not support filtering by fingerprint
params := binarylane.GetAccountKeysParams{
Page: &page,
PerPage: &perPage,
}

sshResp, err := r.bc.client.GetAccountKeysWithResponse(ctx, &params)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Error getting SSH key for import: fingerprint=%s", req.ID), err.Error())
return
}

if sshResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code getting SSH key for import",
fmt.Sprintf("Received %s reading SSH key: fingerprint=%s. Details: %s", sshResp.Status(), req.ID,
sshResp.Body))
return
}

sshKeys := sshResp.JSON200.SshKeys
for _, key := range sshKeys {
if *key.Fingerprint == req.ID {
sshKey = key
nextPage = false
break
}
}
if sshResp.JSON200.Links == nil || sshResp.JSON200.Links.Pages == nil || sshResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}

page++
}

if sshKey.Id == nil {
resp.Diagnostics.AddError(
"Could not find SSH key by fingerprint",
fmt.Sprintf("Error finding SSH key: fingerprint=%s", req.ID),
)
return
}

diags := resp.State.SetAttribute(ctx, path.Root("id"), *sshKey.Id)
resp.Diagnostics.Append(diags...)
}
Loading

0 comments on commit 1084c27

Please sign in to comment.