diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bb3aaa8 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +TF_ACC=1 +BINARYLANE_API_TOKEN= diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..780ee5e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "go.testEnvFile": "${workspaceFolder}/.env", +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 101ac20..5828523 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index 7a4af82..dbe596a 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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 ``` @@ -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 ``` diff --git a/docs/data-sources/ssh_key.md b/docs/data-sources/ssh_key.md index b76ed43..a87eb2b 100644 --- a/docs/data-sources/ssh_key.md +++ b/docs/data-sources/ssh_key.md @@ -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. diff --git a/docs/resources/ssh_key.md b/docs/resources/ssh_key.md index 7db34a3..245419e 100644 --- a/docs/resources/ssh_key.md +++ b/docs/resources/ssh_key.md @@ -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. diff --git a/internal/provider/load_balancer_resource.go b/internal/provider/load_balancer_resource.go index 6b11c2d..61056bd 100644 --- a/internal/provider/load_balancer_resource.go +++ b/internal/provider/load_balancer_resource.go @@ -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" @@ -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) { diff --git a/internal/provider/load_balancer_resource_test.go b/internal/provider/load_balancer_resource_test.go index c797666..dfb3810 100644 --- a/internal/provider/load_balancer_resource_test.go +++ b/internal/provider/load_balancer_resource_test.go @@ -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 diff --git a/internal/provider/server_firewall_rules_resource_test.go b/internal/provider/server_firewall_rules_resource_test.go index 9eb3ade..ace2aa6 100644 --- a/internal/provider/server_firewall_rules_resource_test.go +++ b/internal/provider/server_firewall_rules_resource_test.go @@ -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 diff --git a/internal/provider/server_resource_test.go b/internal/provider/server_resource_test.go index 3db0a6c..7a7cee2 100644 --- a/internal/provider/server_resource_test.go +++ b/internal/provider/server_resource_test.go @@ -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 diff --git a/internal/provider/ssh_key_data_source.go b/internal/provider/ssh_key_data_source.go index 0db9203..059fce1 100644 --- a/internal/provider/ssh_key_data_source.go +++ b/internal/provider/ssh_key_data_source.go @@ -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" ) @@ -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)...) @@ -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)...) diff --git a/internal/provider/ssh_key_resource.go b/internal/provider/ssh_key_resource.go index fcabe65..a3a28ea 100644 --- a/internal/provider/ssh_key_resource.go +++ b/internal/provider/ssh_key_resource.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strconv" "terraform-provider-binarylane/internal/binarylane" "terraform-provider-binarylane/internal/resources" @@ -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 @@ -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)...) @@ -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 @@ -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)...) @@ -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)...) @@ -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)...) @@ -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, ¶ms) + 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...) } diff --git a/internal/provider/ssh_key_resource_test.go b/internal/provider/ssh_key_resource_test.go index 2963bdb..0e04f50 100644 --- a/internal/provider/ssh_key_resource_test.go +++ b/internal/provider/ssh_key_resource_test.go @@ -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 @@ -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 + ` @@ -73,7 +81,7 @@ 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) } @@ -81,3 +89,20 @@ func GeneratePublicKey(t *testing.T) string { encoded := base64.StdEncoding.EncodeToString(pub) return fmt.Sprintf("ssh-ed25519 %s test@company.internal", 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 +} diff --git a/internal/provider/vpc_resource_test.go b/internal/provider/vpc_resource_test.go index ce79b92..da42b6a 100644 --- a/internal/provider/vpc_resource_test.go +++ b/internal/provider/vpc_resource_test.go @@ -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