diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ff75217..1baec815f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - **New**: STACKIT Git module can be used to manage STACKIT Git - [v0.2.0](services/git/CHANGELOG.md#v020-2025-04-16) - **Features**: Add new methods to manage the STACKIT Git: `CreateInstance`, `DeleteInstance`, `GetInstance` + - [v0.3.0](services/git/CHANGELOG.md#v030-2025-04-22) + - **Features**: Add waiters to manage the STACKIT Git - `observability`: [v0.5.0](services/observability/CHANGELOG.md#v050-2025-04-16) - **Feature:** Add new methods `ListLogsAlertgroups`, `CreateLogsAlertgroups`, `GetLogsAlertgroup`, `UpdateLogsAlertgroup`, `DeleteLogsAlertgroup` diff --git a/services/git/CHANGELOG.md b/services/git/CHANGELOG.md index c63b5fb69..c1277c33a 100644 --- a/services/git/CHANGELOG.md +++ b/services/git/CHANGELOG.md @@ -1,3 +1,6 @@ +## v0.3.0 (2025-04-22) +- **Features**: Add waiters to manage the STACKIT Git + ## v0.2.0 (2025-04-16) - **Features**: Add new methods to manage the STACKIT Git: `CreateInstance`, `DeleteInstance`, `GetInstance` diff --git a/services/git/go.mod b/services/git/go.mod index 2b391788a..53d00a814 100644 --- a/services/git/go.mod +++ b/services/git/go.mod @@ -3,6 +3,7 @@ module github.com/stackitcloud/stackit-sdk-go/services/git go 1.21 require ( + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/stackitcloud/stackit-sdk-go/core v0.17.1 ) diff --git a/services/git/wait/wait.go b/services/git/wait/wait.go new file mode 100644 index 000000000..bef8246ed --- /dev/null +++ b/services/git/wait/wait.go @@ -0,0 +1,64 @@ +package wait + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/wait" + "github.com/stackitcloud/stackit-sdk-go/services/git" +) + +const ( + InstanceStateReady = "Ready" + InstanceStateCreating = "Creating" + InstanceStateError = "Error" +) + +// APIClientInterface Interfaces needed for tests +type APIClientInterface interface { + GetInstanceExecute(ctx context.Context, projectId string, instanceId string) (*git.Instance, error) +} + +func CreateGitInstanceWaitHandler(ctx context.Context, a APIClientInterface, projectId, instanceId string) *wait.AsyncActionHandler[git.Instance] { + handler := wait.New(func() (waitFinished bool, response *git.Instance, err error) { + instance, err := a.GetInstanceExecute(ctx, projectId, instanceId) + if err != nil { + return false, nil, err + } + if instance.Id == nil || instance.State == nil { + return false, nil, fmt.Errorf("could not get Instance id or State from response for project %s and instanceId %s", projectId, instanceId) + } + if *instance.Id == instanceId && *instance.State == InstanceStateReady { + return true, instance, nil + } + if *instance.Id == instanceId && *instance.State == InstanceStateError { + return true, instance, fmt.Errorf("create failed for Instance with id %s", instanceId) + } + return false, nil, nil + }) + handler.SetTimeout(10 * time.Minute) + return handler +} + +func DeleteGitInstanceWaitHandler(ctx context.Context, a APIClientInterface, projectId, instanceId string) *wait.AsyncActionHandler[git.Instance] { + handler := wait.New(func() (waitFinished bool, response *git.Instance, err error) { + _, err = a.GetInstanceExecute(ctx, projectId, instanceId) + // the instances is still gettable, e.g. not deleted, when the errors is null + if err == nil { + return false, nil, nil + } + var oapiError *oapierror.GenericOpenAPIError + if errors.As(err, &oapiError) { + if statusCode := oapiError.StatusCode; statusCode == http.StatusNotFound { + return true, nil, nil + } + } + return false, nil, err + }) + handler.SetTimeout(10 * time.Minute) + return handler +} diff --git a/services/git/wait/wait_test.go b/services/git/wait/wait_test.go new file mode 100644 index 000000000..151798e5c --- /dev/null +++ b/services/git/wait/wait_test.go @@ -0,0 +1,226 @@ +package wait + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/git" +) + +type apiClientMocked struct { + getFails bool + errorCode int + returnInstance bool + projectId string + instanceId string + getGitResponse *git.Instance +} + +func (a *apiClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*git.Instance, error) { + if a.getFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: a.errorCode, + } + } + if !a.returnInstance { + return nil, nil + } + return a.getGitResponse, nil +} + +var PROJECT_ID = uuid.New().String() +var INSTANCE_ID = uuid.New().String() + +func TestCreateGitInstanceWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + wantErr bool + wantResp bool + projectId string + instanceId string + returnInstance bool + getGitResponse *git.Instance + }{ + { + desc: "Creation of an instance succeeded", + getFails: false, + wantErr: false, + wantResp: true, + projectId: uuid.New().String(), + instanceId: INSTANCE_ID, + returnInstance: true, + getGitResponse: &git.Instance{ + Created: utils.Ptr(time.Now()), + Id: utils.Ptr(INSTANCE_ID), + Name: utils.Ptr("instance-test"), + State: utils.Ptr(InstanceStateReady), + Url: utils.Ptr("https://testing.git.onstackit.cloud"), + Version: utils.Ptr("v1.6.0"), + }, + }, + { + desc: "Creation of an instance failed with error", + getFails: true, + wantErr: true, + wantResp: false, + projectId: uuid.New().String(), + instanceId: INSTANCE_ID, + returnInstance: true, + getGitResponse: &git.Instance{ + Created: utils.Ptr(time.Now()), + Id: utils.Ptr(INSTANCE_ID), + Name: utils.Ptr("instance-test"), + State: utils.Ptr(InstanceStateReady), + Url: utils.Ptr("https://testing.git.onstackit.cloud"), + Version: utils.Ptr("v1.6.0"), + }, + }, + { + desc: "Creation of an instance with response failed and without error", + getFails: false, + wantErr: true, + wantResp: true, + projectId: uuid.New().String(), + instanceId: INSTANCE_ID, + returnInstance: true, + getGitResponse: &git.Instance{ + Created: utils.Ptr(time.Now()), + Id: utils.Ptr(INSTANCE_ID), + Name: utils.Ptr("instance-test"), + State: utils.Ptr(InstanceStateError), + Url: utils.Ptr("https://testing.git.onstackit.cloud"), + Version: utils.Ptr("v1.6.0"), + }, + }, + { + desc: "Creation of an instance failed without id on the response", + getFails: false, + wantErr: true, + wantResp: false, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + returnInstance: true, + getGitResponse: &git.Instance{ + Created: utils.Ptr(time.Now()), + Name: utils.Ptr("instance-test"), + State: utils.Ptr(InstanceStateError), + Url: utils.Ptr("https://testing.git.onstackit.cloud"), + Version: utils.Ptr("v1.6.0"), + }, + }, + + { + desc: "Creation of an instance without state on the response", + getFails: false, + wantErr: true, + wantResp: false, + projectId: PROJECT_ID, + instanceId: INSTANCE_ID, + returnInstance: true, + getGitResponse: &git.Instance{ + Created: utils.Ptr(time.Now()), + Id: utils.Ptr(INSTANCE_ID), + Name: utils.Ptr("instance-test"), + Url: utils.Ptr("https://testing.git.onstackit.cloud"), + Version: utils.Ptr("v1.6.0"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getFails: tt.getFails, + projectId: tt.projectId, + instanceId: tt.instanceId, + getGitResponse: tt.getGitResponse, + returnInstance: tt.returnInstance, + } + var instanceWanted *git.Instance + if tt.wantResp { + instanceWanted = tt.getGitResponse + } + + handler := CreateGitInstanceWaitHandler(context.Background(), apiClient, apiClient.projectId, apiClient.instanceId) + + response, 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(response, instanceWanted) { + t.Fatalf("handler gotRes = %v, want %v", response, instanceWanted) + } + }) + } +} + +func TestDeleteGitInstanceWaitHandler(t *testing.T) { + tests := []struct { + desc string + wantErr bool + wantReturnedInstance bool + getFails bool + errorCode int + returnInstance bool + getGitResponse *git.Instance + }{ + { + desc: "Instance deletion failed with error", + wantErr: true, + getFails: true, + }, + { + desc: "Instance deletion failed returning existing instance", + wantErr: true, + getFails: false, + + wantReturnedInstance: false, + returnInstance: true, + getGitResponse: &git.Instance{ + Created: utils.Ptr(time.Now()), + Id: utils.Ptr(INSTANCE_ID), + Name: utils.Ptr("instance-test"), + State: utils.Ptr(InstanceStateReady), + Url: utils.Ptr("https://testing.git.onstackit.cloud"), + Version: utils.Ptr("v1.6.0"), + }, + }, + { + desc: "Instance deletion successful", + wantErr: false, + getFails: true, + errorCode: http.StatusNotFound, + wantReturnedInstance: false, + returnInstance: false, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + + projectId: uuid.New().String(), + getFails: tt.getFails, + errorCode: tt.errorCode, + returnInstance: tt.returnInstance, + getGitResponse: tt.getGitResponse, + } + + handler := DeleteGitInstanceWaitHandler(context.Background(), apiClient, apiClient.projectId, apiClient.instanceId) + response, 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 (response != nil) != tt.wantReturnedInstance { + t.Fatalf("handler gotRes = %v, want nil", response) + } + }) + } +}