Feat/alpa postgres user (#5)

* chore: add stackit_postgresflexalpha_user resource

* chore: refactor postgresflex user resource to postgresflexalpha

* chore: refactor wait handlers and update API client interfaces for postgresflexalpha

* chore: add stackit_postgresflexalpha_user data source example

---------

Co-authored-by: Andre Harms <andre.harms@stackit.cloud>
This commit is contained in:
Marcel S. Henselin 2025-12-19 11:48:21 +01:00 committed by GitHub
parent 75e003ae9a
commit ce2f3fca00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 765 additions and 581 deletions

View file

@ -21,22 +21,89 @@ const (
// Interface needed for tests
type APIClientInstanceInterface interface {
GetInstanceRequestExecute(ctx context.Context, projectId, region, instanceId string) (*postgresflex.GetInstanceResponse, error)
ListUsersRequestExecute(ctx context.Context, projectId, region, instanceId string) (*postgresflex.ListUserResponse, error)
GetInstanceRequestExecute(ctx context.Context, projectId, region, instanceId string) (
*postgresflex.GetInstanceResponse,
error,
)
ListUsersRequestExecute(
ctx context.Context, projectId string, region string,
instanceId string,
) (*postgresflex.ListUserResponse, error)
}
// Interface needed for tests
type APIClientUserInterface interface {
GetUserRequestExecute(ctx context.Context, projectId, region, instanceId string, userId int64) (*postgresflex.GetUserResponse, error)
GetUserRequestExecute(ctx context.Context, projectId, region, instanceId string, userId int64) (
*postgresflex.GetUserResponse,
error,
)
}
// CreateInstanceWaitHandler will wait for instance creation
func CreateInstanceWaitHandler(ctx context.Context, a APIClientInstanceInterface, projectId, region, instanceId string) *wait.AsyncActionHandler[postgresflex.GetInstanceResponse] {
func CreateInstanceWaitHandler(
ctx context.Context, a APIClientInstanceInterface, projectId, region,
instanceId string,
) *wait.AsyncActionHandler[postgresflex.GetInstanceResponse] {
instanceCreated := false
var instanceGetResponse *postgresflex.GetInstanceResponse
handler := wait.New(func() (waitFinished bool, response *postgresflex.GetInstanceResponse, err error) {
if !instanceCreated {
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
}
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 InstanceStateProgressing:
return false, nil, nil
case InstanceStateSuccess:
instanceCreated = true
instanceGetResponse = s
case InstanceStateFailed:
return true, s, fmt.Errorf("create failed for instance with id %s", instanceId)
}
}
// // 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
}
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(45 * time.Minute).SetSleepBeforeWait(15 * 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
@ -52,119 +119,93 @@ func CreateInstanceWaitHandler(ctx context.Context, a APIClientInstanceInterface
case InstanceStateProgressing:
return false, nil, nil
case InstanceStateSuccess:
instanceCreated = true
instanceGetResponse = s
return true, s, nil
case InstanceStateFailed:
return true, s, fmt.Errorf("create failed for instance with id %s", instanceId)
return true, s, fmt.Errorf("update failed for instance with id %s", instanceId)
}
}
// 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
}
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(45 * time.Minute).SetSleepBeforeWait(15 * 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 InstanceStateProgressing:
return false, nil, nil
case InstanceStateSuccess:
return true, s, nil
case InstanceStateFailed:
return true, s, fmt.Errorf("update failed for instance with id %s", instanceId)
}
})
},
)
handler.SetTimeout(45 * time.Minute)
return handler
}
// DeleteInstanceWaitHandler will wait for instance deletion
func DeleteInstanceWaitHandler(ctx context.Context, a APIClientInstanceInterface, projectId, region, instanceId string) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(func() (waitFinished bool, response *struct{}, 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, nil, fmt.Errorf("instance with id %s has unexpected status %s", instanceId, *s.Status)
case InstanceStateSuccess:
return false, nil, nil
case InstanceStateDeleted:
return true, nil, nil
}
})
func DeleteInstanceWaitHandler(
ctx context.Context,
a APIClientInstanceInterface,
projectId, region, instanceId string,
) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(
func() (waitFinished bool, response *struct{}, 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, nil, fmt.Errorf("instance with id %s has unexpected status %s", instanceId, *s.Status)
case InstanceStateSuccess:
return false, nil, nil
case InstanceStateDeleted:
return true, nil, nil
}
},
)
handler.SetTimeout(5 * time.Minute)
return handler
}
// ForceDeleteInstanceWaitHandler will wait for instance deletion
func ForceDeleteInstanceWaitHandler(ctx context.Context, a APIClientInstanceInterface, projectId, region, instanceId string) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(func() (waitFinished bool, response *struct{}, err error) {
_, err = a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
if err == nil {
return false, nil, 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
}
if oapiErr.StatusCode != 404 {
return false, nil, err
}
return true, nil, nil
})
func ForceDeleteInstanceWaitHandler(
ctx context.Context,
a APIClientInstanceInterface,
projectId, region, instanceId string,
) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(
func() (waitFinished bool, response *struct{}, err error) {
_, err = a.GetInstanceRequestExecute(ctx, projectId, region, instanceId)
if err == nil {
return false, nil, 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
}
if oapiErr.StatusCode != 404 {
return false, nil, err
}
return true, nil, nil
},
)
handler.SetTimeout(15 * time.Minute)
return handler
}
// DeleteUserWaitHandler will wait for delete
func DeleteUserWaitHandler(ctx context.Context, a APIClientUserInterface, projectId, region, instanceId string, userId int64) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(func() (waitFinished bool, response *struct{}, err error) {
_, err = a.GetUserRequestExecute(ctx, projectId, region, instanceId, userId)
if err == nil {
return false, nil, 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
}
if oapiErr.StatusCode != 404 {
return false, nil, err
}
return true, nil, nil
})
func DeleteUserWaitHandler(
ctx context.Context,
a APIClientUserInterface,
projectId, region, instanceId string, userId int64,
) *wait.AsyncActionHandler[struct{}] {
handler := wait.New(
func() (waitFinished bool, response *struct{}, err error) {
_, err = a.GetUserRequestExecute(ctx, projectId, region, instanceId, userId)
if err == nil {
return false, nil, 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
}
if oapiErr.StatusCode != 404 {
return false, nil, err
}
return true, nil, nil
},
)
handler.SetTimeout(1 * time.Minute)
return handler
}

View file

@ -2,6 +2,7 @@ package wait
import (
"context"
"math"
"testing"
"time"
@ -157,19 +158,19 @@ func TestCreateInstanceWaitHandler(t *testing.T) {
Id: &instanceId,
Status: postgresflex.GetInstanceResponseGetStatusAttributeType(utils.Ptr(tt.instanceState)),
}
}
handler := CreateInstanceWaitHandler(context.Background(), apiClient, "", "", instanceId)
handler := CreateInstanceWaitHandler(context.Background(), apiClient, "", "", instanceId)
gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * time.Millisecond).WaitWithContext(context.Background())
gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * time.Millisecond).WaitWithContext(context.Background())
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
if !cmp.Equal(gotRes, wantRes) {
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
}
})
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
if !cmp.Equal(gotRes, wantRes) {
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
}
},
)
}
}
@ -232,19 +233,19 @@ func TestUpdateInstanceWaitHandler(t *testing.T) {
Id: &instanceId,
Status: postgresflex.GetInstanceResponseGetStatusAttributeType(utils.Ptr(tt.instanceState)),
}
}
handler := PartialUpdateInstanceWaitHandler(context.Background(), apiClient, "", "", instanceId)
handler := PartialUpdateInstanceWaitHandler(context.Background(), apiClient, "", "", instanceId)
gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
gotRes, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
if !cmp.Equal(gotRes, wantRes) {
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
}
})
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
if !cmp.Equal(gotRes, wantRes) {
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
}
},
)
}
}
@ -274,23 +275,25 @@ func TestDeleteInstanceWaitHandler(t *testing.T) {
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
instanceId := "foo-bar"
t.Run(
tt.desc, func(t *testing.T) {
instanceId := "foo-bar"
apiClient := &apiClientInstanceMocked{
instanceGetFails: tt.instanceGetFails,
instanceId: instanceId,
instanceState: tt.instanceState,
}
apiClient := &apiClientInstanceMocked{
instanceGetFails: tt.instanceGetFails,
instanceId: instanceId,
instanceState: tt.instanceState,
}
handler := DeleteInstanceWaitHandler(context.Background(), apiClient, "", "", instanceId)
handler := DeleteInstanceWaitHandler(context.Background(), apiClient, "", "", instanceId)
_, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
_, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
})
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
},
)
}
}
@ -320,24 +323,26 @@ func TestForceDeleteInstanceWaitHandler(t *testing.T) {
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
instanceId := "foo-bar"
t.Run(
tt.desc, func(t *testing.T) {
instanceId := "foo-bar"
apiClient := &apiClientInstanceMocked{
instanceGetFails: tt.instanceGetFails,
instanceIsForceDeleted: tt.instanceState == InstanceStateDeleted,
instanceId: instanceId,
instanceState: tt.instanceState,
}
apiClient := &apiClientInstanceMocked{
instanceGetFails: tt.instanceGetFails,
instanceIsForceDeleted: tt.instanceState == InstanceStateDeleted,
instanceId: instanceId,
instanceState: tt.instanceState,
}
handler := ForceDeleteInstanceWaitHandler(context.Background(), apiClient, "", "", instanceId)
handler := ForceDeleteInstanceWaitHandler(context.Background(), apiClient, "", "", instanceId)
_, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
_, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
})
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
},
)
}
}
@ -377,13 +382,14 @@ func TestDeleteUserWaitHandler(t *testing.T) {
isUserDeleted: !tt.deleteFails,
}
handler := DeleteUserWaitHandler(context.Background(), apiClient, "", "", "", userId)
handler := DeleteUserWaitHandler(context.Background(), apiClient, "", "", "", userId)
_, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
_, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background())
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
})
if (err != nil) != tt.wantErr {
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
}
},
)
}
}