Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⭕ Retry if Servers, SSH Keys, or Load Balancer creation returns 500 response #29

Merged
merged 3 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
TF_ACC=1
BINARYLANE_API_TOKEN=

# Uncomment to enable debug logging (will dump request bodies)
# TF_LOG=DEBUG

# Uncomment to disable the Go cache when testing, useful for flaky tests
# GOCACHE=off
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ jobs:
env:
TF_ACC: '1'
BINARYLANE_API_TOKEN: ${{ secrets.BINARYLANE_API_TOKEN }}
- name: Run sweepers
if: failure()
run: go test -v ./internal/provider/... -sweep=all
env:
TF_ACC: '1'
BINARYLANE_API_TOKEN: ${{ secrets.BINARYLANE_API_TOKEN }}
# unit:
# name: Unit Tests
# runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ eval export $(cat .env)
go test -v ./internal/provider/...
```

To run Sweepers:

```sh
go test -v ./internal/provider/... -sweep=all
```

### Update modules

```sh
Expand Down
49 changes: 49 additions & 0 deletions internal/binarylane/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package binarylane

import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httputil"

"github.com/deepmap/oapi-codegen/pkg/securityprovider"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

func NewClientWithAuth(endpoint string, token string) (*ClientWithResponses, error) {
if token == "" {
return nil, errors.New("missing or empty value for the Binary Lane API " +
"token. Set the `api_token` value in the configuration or use the " +
"BINARYLANE_API_TOKEN environment variable. If either is already set, " +
"ensure the value is not empty")
}

auth, err := securityprovider.NewSecurityProviderBearerToken(token)
if err != nil {
return nil, fmt.Errorf("failed to create API client with supplied API token: %w", err)
}

if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}

client, err := NewClientWithResponses(
endpoint,
WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
dump, err := httputil.DumpRequestOut(req, true)
if err != nil {
return err
}
tflog.Debug(ctx, fmt.Sprintf("%q\n", dump))
return nil
}),
WithRequestEditorFn(auth.Intercept), // include auth AFTER the request logger
)

if err != nil {
return nil, fmt.Errorf("failed to create Binary Lane API client: %w", err)
}

return client, nil
}
57 changes: 41 additions & 16 deletions internal/provider/load_balancer_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,29 +128,54 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe

tflog.Info(ctx, fmt.Sprintf("Creating Load Balancer: name=%s", data.Name.ValueString()))

lbResp, err := r.bc.client.PostLoadBalancersWithResponse(ctx, body)
if err != nil {
tflog.Info(ctx, fmt.Sprintf("Attempted to create new load balancer: request=%+v", body))
resp.Diagnostics.AddError(
fmt.Sprintf("Error sending request to create load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
const maxRetries = 3
var lbResp *binarylane.PostLoadBalancersResponse

retryLoop:
for i := 0; i < maxRetries; i++ {
var err error
lbResp, err = r.bc.client.PostLoadBalancersWithResponse(ctx, body)
if err != nil {
tflog.Info(ctx, fmt.Sprintf("Attempted to create new load balancer: request=%+v", body))
resp.Diagnostics.AddError(
fmt.Sprintf("Error sending request to create load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}

switch lbResp.StatusCode() {

case http.StatusOK:
break retryLoop

case http.StatusInternalServerError:
if i < maxRetries-1 {
tflog.Warn(ctx, "Received 500 creating load balancer, retrying...")
time.Sleep(time.Second * 5)
continue
}

default:
tflog.Info(ctx, fmt.Sprintf("Attempted to create new load balancer: request=%+v", body))
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating load balancer",
fmt.Sprintf("Received %s creating new load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body),
)
return
}
}

// Check if retries exceeded
if lbResp.StatusCode() != http.StatusOK {
tflog.Info(ctx, fmt.Sprintf("Attempted to create new load balancer: request=%+v", body))
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating load balancer",
fmt.Sprintf("Received %s creating new load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body))
"Failed to create load balancer after retries",
fmt.Sprintf("Final status code: %d", lbResp.StatusCode()),
)
return
}

diags = SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
data.Id = types.Int64Value(*lbResp.JSON200.LoadBalancer.Id)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
Expand Down
73 changes: 73 additions & 0 deletions internal/provider/load_balancer_resource_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package provider

import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"net/http"
"os"
"strings"
"terraform-provider-binarylane/internal/binarylane"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
Expand Down Expand Up @@ -84,3 +91,69 @@ data "binarylane_load_balancer" "test" {
},
})
}

func init() {
resource.AddTestSweepers("load_balancer", &resource.Sweeper{
Name: "load_balancer",
Dependencies: []string{"server"},
F: func(_ string) error {
endpoint := os.Getenv("BINARYLANE_API_ENDPOINT")
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}
token := os.Getenv("BINARYLANE_API_TOKEN")

client, err := binarylane.NewClientWithAuth(
endpoint,
token,
)

if err != nil {
return fmt.Errorf("Error creating Binary Lane API client: %w", err)
}

ctx := context.Background()

var page int32 = 1
perPage := int32(200)
var nextPage bool = true

for nextPage {
params := binarylane.GetLoadBalancersParams{
Page: &page,
PerPage: &perPage,
}

lbResp, err := client.GetLoadBalancersWithResponse(ctx, &params)
if err != nil {
return fmt.Errorf("Error getting load balancers for test sweep: %w", err)
}

if lbResp.StatusCode() != http.StatusOK {
return fmt.Errorf("Unexpected status code getting load balancers for test sweep: %s", lbResp.Body)
}

loadBalancers := *lbResp.JSON200.LoadBalancers
for _, lb := range loadBalancers {
if strings.HasPrefix(*lb.Name, "tf-test-") {
lbResp, err := client.DeleteLoadBalancersLoadBalancerIdWithResponse(ctx, *lb.Id)
if err != nil {
return fmt.Errorf("Error deleting load balancer %d for test sweep: %w", *lb.Id, err)
}
if lbResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting load balancer %d in test sweep: %s", lbResp.StatusCode(), *lb.Id, lbResp.Body)
}
log.Println("Deleted load balancer during test sweep:", *lb.Id)
}
}
if lbResp.JSON200.Links == nil || lbResp.JSON200.Links.Pages == nil || lbResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}

page++
}
return nil
},
})
}
64 changes: 13 additions & 51 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import (
"os"
"terraform-provider-binarylane/internal/binarylane"

"github.com/deepmap/oapi-codegen/pkg/securityprovider"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
Expand Down Expand Up @@ -67,60 +65,24 @@ func (p *binarylaneProvider) Configure(ctx context.Context, req provider.Configu
return
}

if config.Endpoint.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("api_endpoint"),
"Unknown Binary Lane API endpoint",
"The provider cannot create the Binary Lane API client as there is an unknown configuration value for the "+
"Binary Lane API endpoint. Either target apply the source of the value first, set the value statically in "+
"the configuration, or use the BINARYLANE_API_ENDPOINT environment variable.",
)
}
if config.Token.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("api_token"),
"Unknown Binary Lane token",
"The provider cannot create the Binary Lane API client as there is an unknown configuration value for the "+
"Binary Lane API token. Either target apply the source of the value first, set the value statically in the "+
"configuration, or use the BINARYLANE_API_TOKEN environment variable.",
)
}
if resp.Diagnostics.HasError() {
return
}

// Default values to environment variables, but override
// with Terraform configuration value if set.
endpoint := os.Getenv("BINARYLANE_API_ENDPOINT")
token := os.Getenv("BINARYLANE_API_TOKEN")
if !config.Endpoint.IsNull() {
endpoint = config.Endpoint.ValueString()
}
if !config.Token.IsNull() {
token = config.Token.ValueString()
}
endpoint := config.Endpoint.ValueString()
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
endpoint = os.Getenv("BINARYLANE_API_ENDPOINT")
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}
}

token := config.Token.ValueString()
if token == "" {
resp.Diagnostics.AddAttributeError(
path.Root("api_token"),
"Missing Binary Lane API token",
"The provider cannot create the Binary Lane API client as there is a missing or empty value for the Binary "+
"Lane API token. Set the token value in the configuration or use the BINARYLANE_API_TOKEN environment "+
"variable. If either is already set, ensure the value is not empty.",
)
}
if resp.Diagnostics.HasError() {
return
}
auth, err := securityprovider.NewSecurityProviderBearerToken(token)
if err != nil {
resp.Diagnostics.AddError("Failed to create security provider with supplied token", err.Error())
return
token = os.Getenv("BINARYLANE_API_TOKEN")
}

client, err := binarylane.NewClientWithResponses(endpoint, binarylane.WithRequestEditorFn(auth.Intercept))
client, err := binarylane.NewClientWithAuth(
endpoint,
token,
)

if err != nil {
resp.Diagnostics.AddError("Failed to create Binary Lane API client", err.Error())
return
Expand Down
48 changes: 39 additions & 9 deletions internal/provider/server_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,21 +255,51 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
data.Password = types.StringNull()
} else {
body.Password = data.Password.ValueStringPointer()
ctx = tflog.MaskMessageStrings(ctx, data.Password.String())
}

serverResp, err := r.bc.client.PostServersWithResponse(ctx, body)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating server: name=%s", data.Name.ValueString()),
err.Error(),
)
return
const maxRetries = 3
var serverResp *binarylane.PostServersResponse

retryLoop:
for i := 0; i < maxRetries; i++ {
var err error
serverResp, err = r.bc.client.PostServersWithResponse(ctx, body)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating server: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}

switch serverResp.StatusCode() {

case http.StatusOK:
break retryLoop

case http.StatusInternalServerError:
if i < maxRetries-1 {
tflog.Warn(ctx, "Received 500 creating server, retrying...")
time.Sleep(time.Second * 5)
continue
}

default:
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating server",
fmt.Sprintf("Received %s creating new server: name=%s. Details: %s", serverResp.Status(), data.Name.ValueString(), serverResp.Body),
)
return
}
}

// Check if retries exceeded
if serverResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating server",
fmt.Sprintf("Received %s creating new server: name=%s. Details: %s", serverResp.Status(), data.Name.ValueString(), serverResp.Body))
"Failed to create server after retries",
fmt.Sprintf("Final status code: %d", serverResp.StatusCode()),
)
return
}

Expand Down
Loading
Loading