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 = extendedTimeout + 1 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 failed 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) 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) 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: 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 }