Skip to content

Commit

Permalink
🥢 import ssh key by fingerprint, make tests faster
Browse files Browse the repository at this point in the history
  • Loading branch information
oscarhermoso committed Sep 27, 2024
1 parent d1adea2 commit b3e05bf
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 24 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
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.
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...)
}
43 changes: 34 additions & 9 deletions internal/provider/ssh_key_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)

func TestSshKeyResource(t *testing.T) {
publicKey := GeneratePublicKey(t)
resource.Test(t, resource.TestCase{

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
Expand Down Expand Up @@ -43,13 +45,19 @@ data "binarylane_ssh_key" "test" {
),
},
// ImportState testing
// TODO
// {
// ResourceName: "binarylane_ssh_key.test",
// ImportState: true,
// ImportStateVerify: true,
// ImportStateVerifyIgnore: []string{}, // nothing to ignore
// },
{
ResourceName: "binarylane_ssh_key.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{}, // nothing to ignore
},
{
ResourceName: "binarylane_ssh_key.test",
ImportStateIdFunc: ImportByFingerprint,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{}, // nothing to ignore
},
// TODO: Update and Read testing
// {
// Config: providerConfig + `
Expand All @@ -73,11 +81,28 @@ data "binarylane_ssh_key" "test" {
func GeneratePublicKey(t *testing.T) string {
t.Helper()

_, pub, err := ed25519.GenerateKey(nil)
pub, _, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}

encoded := base64.StdEncoding.EncodeToString(pub)
return fmt.Sprintf("ssh-ed25519 %s [email protected]", encoded)
}

func ImportByFingerprint(state *terraform.State) (fingerprint string, err error) {
resourceName := "binarylane_ssh_key.test"
var rawState map[string]string
for _, m := range state.Modules {
if len(m.Resources) > 0 {
if v, ok := m.Resources[resourceName]; ok {
rawState = v.Primary.Attributes
}
}
}
if rawState == nil {
return "", fmt.Errorf("resource not found: %s", resourceName)
}

return rawState["fingerprint"], nil
}
2 changes: 1 addition & 1 deletion internal/provider/vpc_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func TestVpcResource(t *testing.T) {
resource.Test(t, resource.TestCase{
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
Expand Down

0 comments on commit b3e05bf

Please sign in to comment.