298 lines
9.4 KiB
Go
298 lines
9.4 KiB
Go
package postgresflexalpha
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
|
|
postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha"
|
|
|
|
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
|
"github.com/stackitcloud/stackit-sdk-go/core/wait"
|
|
)
|
|
|
|
// "READY" "PENDING" "PROGRESSING" "FAILURE" "UNKNOWN" "TERMINATING"
|
|
const (
|
|
InstanceStateEmpty = ""
|
|
InstanceStateProgressing = "PROGRESSING"
|
|
InstanceStateSuccess = "READY"
|
|
InstanceStateFailed = "FAILURE"
|
|
InstanceStateTerminating = "TERMINATING"
|
|
InstanceStateUnknown = "UNKNOWN"
|
|
InstanceStatePending = "PENDING"
|
|
)
|
|
|
|
// APIClientInstanceInterface Interface needed for tests
|
|
type APIClientInstanceInterface interface {
|
|
GetInstanceRequestExecute(ctx context.Context, projectId, region, instanceId string) (
|
|
*postgresflex.GetInstanceResponse,
|
|
error,
|
|
)
|
|
|
|
ListUsersRequestExecute(
|
|
ctx context.Context,
|
|
projectId string,
|
|
region string,
|
|
instanceId string,
|
|
) (*postgresflex.ListUserResponse, error)
|
|
}
|
|
|
|
// APIClientUserInterface Interface needed for tests
|
|
type APIClientUserInterface interface {
|
|
GetUserRequestExecute(ctx context.Context, projectId, region, instanceId string, userId int32) (
|
|
*postgresflex.GetUserResponse,
|
|
error,
|
|
)
|
|
|
|
GetDatabaseRequestExecute(
|
|
ctx context.Context,
|
|
projectId string,
|
|
region string,
|
|
instanceId string,
|
|
databaseId int32,
|
|
) (*postgresflex.GetDatabaseResponse, error)
|
|
}
|
|
|
|
// CreateInstanceWaitHandler will wait for instance creation
|
|
func CreateInstanceWaitHandler(
|
|
ctx context.Context, a APIClientInstanceInterface, projectId, region,
|
|
instanceId string,
|
|
) *wait.AsyncActionHandler[postgresflex.GetInstanceResponse] {
|
|
instanceCreated := false
|
|
var instanceGetResponse *postgresflex.GetInstanceResponse
|
|
maxWait := time.Minute * 45
|
|
startTime := time.Now()
|
|
extendedTimeout := 0
|
|
|
|
handler := wait.New(
|
|
func() (waitFinished bool, response *postgresflex.GetInstanceResponse, err error) {
|
|
if !instanceCreated {
|
|
s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
if s == nil || s.Id == nil || *s.Id != instanceId || s.Status == nil {
|
|
return false, nil, nil
|
|
}
|
|
tflog.Debug(
|
|
ctx, "waiting for instance ready", map[string]interface{}{
|
|
"status": *s.Status,
|
|
},
|
|
)
|
|
switch *s.Status {
|
|
default:
|
|
return true, s, fmt.Errorf("instance with id %s has unexpected status %s", instanceId, *s.Status)
|
|
case InstanceStateEmpty:
|
|
return false, nil, nil
|
|
case InstanceStatePending:
|
|
return false, nil, nil
|
|
case InstanceStateUnknown:
|
|
return false, nil, nil
|
|
case InstanceStateProgressing:
|
|
if time.Since(startTime) < maxWait {
|
|
return false, nil, nil
|
|
}
|
|
tflog.Warn(
|
|
ctx,
|
|
fmt.Sprintf(
|
|
"Wait handler still got status %s after %v for instance: %s",
|
|
InstanceStateProgressing,
|
|
maxWait,
|
|
instanceId,
|
|
),
|
|
)
|
|
if extendedTimeout < 3 {
|
|
maxWait += time.Minute * 5
|
|
extendedTimeout++
|
|
if *s.Network.AccessScope == "SNA" {
|
|
ready := true
|
|
if s.Network == nil || s.Network.InstanceAddress == nil {
|
|
tflog.Warn(ctx, "Waiting for instance_address")
|
|
ready = false
|
|
}
|
|
if s.Network.RouterAddress == nil {
|
|
tflog.Warn(ctx, "Waiting for router_address")
|
|
ready = false
|
|
}
|
|
if !ready {
|
|
return false, nil, nil
|
|
}
|
|
}
|
|
if s.IsDeletable == nil {
|
|
tflog.Warn(ctx, "Waiting for is_deletable")
|
|
return false, nil, nil
|
|
}
|
|
}
|
|
|
|
instanceCreated = true
|
|
instanceGetResponse = s
|
|
case InstanceStateSuccess:
|
|
if s.Network != nil && s.Network.AccessScope != nil && *s.Network.AccessScope == "SNA" {
|
|
if s.Network.InstanceAddress == nil {
|
|
tflog.Warn(ctx, "Waiting for instance_address")
|
|
return false, nil, nil
|
|
}
|
|
if s.Network.RouterAddress == nil {
|
|
tflog.Warn(ctx, "Waiting for router_address")
|
|
return false, nil, nil
|
|
}
|
|
}
|
|
instanceCreated = true
|
|
instanceGetResponse = s
|
|
case InstanceStateFailed:
|
|
tflog.Warn(ctx, fmt.Sprintf("Wait handler got status FAILURE for instance: %s", instanceId))
|
|
return false, nil, nil
|
|
// API responds with FAILURE for some seconds and then the instance goes to READY
|
|
// return true, s, fmt.Errorf("create failed for instance with id %s", instanceId)
|
|
}
|
|
}
|
|
|
|
tflog.Info(ctx, "Waiting for instance (calling list users")
|
|
// // User operations aren't available right after an instance is deemed successful
|
|
// // To check if they are, perform a users request
|
|
_, err = a.ListUsersRequestExecute(ctx, projectId, region, instanceId)
|
|
if err == nil {
|
|
return true, instanceGetResponse, nil
|
|
}
|
|
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) // nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
|
if !ok {
|
|
return false, nil, err
|
|
}
|
|
// TODO: refactor and cooperate with api guys to mitigate
|
|
if oapiErr.StatusCode < 500 {
|
|
return true, instanceGetResponse, fmt.Errorf(
|
|
"users request after instance creation returned %d status code",
|
|
oapiErr.StatusCode,
|
|
)
|
|
}
|
|
return false, nil, nil
|
|
},
|
|
)
|
|
// Sleep before wait is set because sometimes API returns 404 right after creation request
|
|
handler.SetTimeout(90 * time.Minute).SetSleepBeforeWait(30 * time.Second)
|
|
return handler
|
|
}
|
|
|
|
// PartialUpdateInstanceWaitHandler will wait for instance update
|
|
func PartialUpdateInstanceWaitHandler(
|
|
ctx context.Context, a APIClientInstanceInterface, projectId, region,
|
|
instanceId string,
|
|
) *wait.AsyncActionHandler[postgresflex.GetInstanceResponse] {
|
|
handler := wait.New(
|
|
func() (waitFinished bool, response *postgresflex.GetInstanceResponse, err error) {
|
|
s, err := a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
if s == nil || s.Id == nil || *s.Id != instanceId || s.Status == nil {
|
|
return false, nil, nil
|
|
}
|
|
switch *s.Status {
|
|
default:
|
|
return true, s, fmt.Errorf("instance with id %s has unexpected status %s", instanceId, *s.Status)
|
|
case InstanceStateEmpty:
|
|
return false, nil, nil
|
|
case InstanceStatePending:
|
|
return false, nil, nil
|
|
case InstanceStateProgressing:
|
|
return false, nil, nil
|
|
case InstanceStateSuccess:
|
|
return true, s, nil
|
|
case InstanceStateTerminating:
|
|
return false, nil, nil
|
|
case InstanceStateUnknown:
|
|
return false, nil, nil
|
|
case InstanceStateFailed:
|
|
return true, s, fmt.Errorf("update got status FAILURE for instance with id %s", instanceId)
|
|
}
|
|
},
|
|
)
|
|
handler.SetTimeout(45 * time.Minute).SetSleepBeforeWait(30 * time.Second)
|
|
return handler
|
|
}
|
|
|
|
// GetUserByIdWaitHandler will wait for instance creation
|
|
func GetUserByIdWaitHandler(
|
|
ctx context.Context,
|
|
a APIClientUserInterface,
|
|
projectId, instanceId, region string,
|
|
userId int64,
|
|
) *wait.AsyncActionHandler[postgresflex.GetUserResponse] {
|
|
handler := wait.New(
|
|
func() (waitFinished bool, response *postgresflex.GetUserResponse, err error) {
|
|
if userId > math.MaxInt32 {
|
|
return false, nil, fmt.Errorf("userId value is too big for int32")
|
|
}
|
|
userId32 := int32(userId) //nolint:gosec // we need to convert databaseId to int32 because API expects int32
|
|
s, err := a.GetUserRequestExecute(ctx, projectId, region, instanceId, userId32)
|
|
if err != nil {
|
|
var oapiErr *oapierror.GenericOpenAPIError
|
|
ok := errors.As(err, &oapiErr)
|
|
if !ok {
|
|
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
|
|
}
|
|
switch oapiErr.StatusCode {
|
|
case http.StatusBadGateway, http.StatusGatewayTimeout, http.StatusServiceUnavailable:
|
|
case http.StatusNotFound:
|
|
tflog.Warn(
|
|
ctx, "api responded with status", map[string]interface{}{
|
|
"status": oapiErr.StatusCode,
|
|
},
|
|
)
|
|
return false, nil, nil
|
|
default:
|
|
return false, nil, err
|
|
}
|
|
}
|
|
return true, s, nil
|
|
},
|
|
)
|
|
return handler
|
|
}
|
|
|
|
// GetDatabaseByIdWaitHandler will wait for instance creation
|
|
func GetDatabaseByIdWaitHandler(
|
|
ctx context.Context,
|
|
a APIClientUserInterface,
|
|
projectId, instanceId, region string,
|
|
databaseId int64,
|
|
) *wait.AsyncActionHandler[postgresflex.GetDatabaseResponse] {
|
|
handler := wait.New(
|
|
func() (waitFinished bool, response *postgresflex.GetDatabaseResponse, err error) {
|
|
dbId32 := int32(databaseId) //nolint:gosec // we need to convert databaseId to int32 because API expects int32
|
|
s, err := a.GetDatabaseRequestExecute(ctx, projectId, region, instanceId, dbId32)
|
|
if err != nil {
|
|
var oapiErr *oapierror.GenericOpenAPIError
|
|
ok := errors.As(err, &oapiErr)
|
|
if !ok {
|
|
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
|
|
}
|
|
switch oapiErr.StatusCode {
|
|
case http.StatusBadGateway, http.StatusGatewayTimeout, http.StatusServiceUnavailable:
|
|
tflog.Warn(
|
|
ctx, "api responded with 50[2,3,4] status", map[string]interface{}{
|
|
"status": oapiErr.StatusCode,
|
|
},
|
|
)
|
|
return false, nil, nil
|
|
case http.StatusNotFound:
|
|
tflog.Warn(
|
|
ctx, "api responded with 404 status", map[string]interface{}{
|
|
"status": oapiErr.StatusCode,
|
|
},
|
|
)
|
|
return false, nil, nil
|
|
default:
|
|
return false, nil, err
|
|
}
|
|
}
|
|
return true, s, nil
|
|
},
|
|
)
|
|
return handler
|
|
}
|