From a05a8f6810dca5dacb6c1f0799b91113b5d4adf8 Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Mon, 4 May 2026 10:20:47 +0200 Subject: [PATCH 1/3] feat: Add wait handlers for VPN gateway creation, update and deletion --- services/vpn/v1beta1api/wait/wait.go | 54 ++++ services/vpn/v1beta1api/wait/wait_test.go | 290 ++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 services/vpn/v1beta1api/wait/wait.go create mode 100644 services/vpn/v1beta1api/wait/wait_test.go diff --git a/services/vpn/v1beta1api/wait/wait.go b/services/vpn/v1beta1api/wait/wait.go new file mode 100644 index 000000000..81b1cf36d --- /dev/null +++ b/services/vpn/v1beta1api/wait/wait.go @@ -0,0 +1,54 @@ +package wait + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/stackitcloud/stackit-sdk-go/core/wait" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" +) + +func CreateOrUpdateGatewayWaitHandler(ctx context.Context, a vpn.DefaultAPI, projectId string, region vpn.Region, gatewayId string) *wait.AsyncActionHandler[vpn.GatewayResponse] { + waitConfig := wait.WaiterHelper[vpn.GatewayResponse, vpn.GatewayStatus]{ + FetchInstance: a.GetVPNGateway(ctx, projectId, region, gatewayId).Execute, + GetState: func(resp *vpn.GatewayResponse) (vpn.GatewayStatus, error) { + if resp == nil { + return "", errors.New("could not get gateway status: response is nil") + } + if resp.State == nil { + return "", errors.New("could not get gateway status: state is nil") + } + return *resp.State, nil + }, + ActiveState: []vpn.GatewayStatus{vpn.GATEWAYSTATUS_READY}, + ErrorState: []vpn.GatewayStatus{vpn.GATEWAYSTATUS_ERROR, vpn.GATEWAYSTATUS_DELETING}, + } + + handler := wait.New(waitConfig.Wait()) + handler.SetTimeout(45 * time.Minute) + return handler +} + +func DeleteGatewayWaitHandler(ctx context.Context, a vpn.DefaultAPI, projectId string, region vpn.Region, gatewayId string) *wait.AsyncActionHandler[vpn.GatewayResponse] { + waitConfig := wait.WaiterHelper[vpn.GatewayResponse, vpn.GatewayStatus]{ + FetchInstance: a.GetVPNGateway(ctx, projectId, region, gatewayId).Execute, + GetState: func(resp *vpn.GatewayResponse) (vpn.GatewayStatus, error) { + if resp == nil { + return "", errors.New("could not get gateway status: response is nil") + } + if resp.State == nil { + return "", errors.New("could not get gateway status: state is nil") + } + return *resp.State, nil + }, + ErrorState: []vpn.GatewayStatus{vpn.GATEWAYSTATUS_ERROR}, + // used default so technically not needed to be set: + DeleteHttpErrorStatusCodes: []int{http.StatusForbidden, http.StatusNotFound, http.StatusGone}, + } + + handler := wait.New(waitConfig.Wait()) + handler.SetTimeout(20 * time.Minute) + return handler +} diff --git a/services/vpn/v1beta1api/wait/wait_test.go b/services/vpn/v1beta1api/wait/wait_test.go new file mode 100644 index 000000000..0c1d8835c --- /dev/null +++ b/services/vpn/v1beta1api/wait/wait_test.go @@ -0,0 +1,290 @@ +package wait + +import ( + "context" + "net/http" + "testing" + "testing/synctest" + + "github.com/google/go-cmp/cmp" + + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1beta1api" +) + +type mockSettings struct { + getFails bool + getNotFound bool + getForbidden bool + getGone bool + gatewayState vpn.GatewayStatus + gatewayId string +} + +func newAPIMock(settings []mockSettings) vpn.DefaultAPI { + count := 0 + return &vpn.DefaultAPIServiceMock{ + GetVPNGatewayExecuteMock: utils.Ptr(func(_ vpn.ApiGetVPNGatewayRequest) (*vpn.GatewayResponse, error) { + setting := settings[count%len(settings)] + count++ + + if setting.getFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusInternalServerError, + } + } + + if setting.getNotFound { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusNotFound, + } + } + + if setting.getForbidden { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusForbidden, + } + } + + if setting.getGone { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusGone, + } + } + + return &vpn.GatewayResponse{ + Id: &setting.gatewayId, + State: &setting.gatewayState, + }, nil + }), + } +} + +func TestCreateOrUpdateGatewayWaitHandler(t *testing.T) { + tests := []struct { + desc string + mockSettings []mockSettings + wantGatewayState vpn.GatewayStatus + wantErr bool + wantResp bool + }{ + { + desc: "create_succeeded", + mockSettings: []mockSettings{ + {gatewayState: vpn.GATEWAYSTATUS_READY, gatewayId: "gw-1"}, + }, + wantGatewayState: vpn.GATEWAYSTATUS_READY, + wantErr: false, + wantResp: true, + }, + { + desc: "pending_multiple_times", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GATEWAYSTATUS_PENDING, + gatewayId: "gw-1", + }, + { + gatewayState: vpn.GATEWAYSTATUS_PENDING, + gatewayId: "gw-1", + }, + { + gatewayState: vpn.GATEWAYSTATUS_READY, + gatewayId: "gw-1", + }, + }, + wantGatewayState: vpn.GATEWAYSTATUS_READY, + wantErr: false, + wantResp: true, + }, + { + desc: "error_state", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GATEWAYSTATUS_PENDING, + gatewayId: "gw-1", + }, + { + gatewayState: vpn.GATEWAYSTATUS_ERROR, + gatewayId: "gw-1", + }, + }, + wantErr: true, + wantResp: false, + }, + { + desc: "deleting_state", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GATEWAYSTATUS_PENDING, + gatewayId: "gw-1", + }, + { + gatewayState: vpn.GATEWAYSTATUS_DELETING, + gatewayId: "gw-1", + }, + }, + wantErr: true, + wantResp: false, + }, + { + desc: "get_fails", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GATEWAYSTATUS_PENDING, + gatewayId: "gw-1", + }, + { + gatewayState: vpn.GATEWAYSTATUS_PENDING, + gatewayId: "gw-1", + }, + { + getFails: true, + }, + }, + wantErr: true, + wantResp: false, + }, + { + desc: "unknown_state", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GatewayStatus("UNKNOWN_STATE"), + gatewayId: "gw-1", + }, + }, + wantErr: true, + wantResp: false, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + apiClient := newAPIMock(tt.mockSettings) + + var wantRes *vpn.GatewayResponse + if tt.wantResp { + wantRes = &vpn.GatewayResponse{ + Id: utils.Ptr("gw-1"), + State: utils.Ptr(tt.wantGatewayState), + } + } + + handler := CreateOrUpdateGatewayWaitHandler(context.Background(), apiClient, "pid", vpn.REGION_EU01, "gw-1") + + gotRes, err := handler.WaitWithContext(context.Background()) + + if (err != nil) != tt.wantErr { + t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !cmp.Equal(gotRes, wantRes) { + t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes) + } + }) + }) + } +} + +func TestDeleteGatewayWaitHandler(t *testing.T) { + tests := []struct { + desc string + mockSettings []mockSettings + wantErr bool + }{ + { + desc: "delete_succeeded", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GatewayStatus(""), + getFails: false, + getNotFound: true, + }, + }, + wantErr: false, + }, + { + desc: "delete_succeeded_forbidden", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GatewayStatus(""), + getForbidden: true, + }, + }, + wantErr: false, + }, + { + desc: "delete_succeeded_gone", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GatewayStatus(""), + getGone: true, + }, + }, + wantErr: false, + }, + { + desc: "delete_pending", + mockSettings: []mockSettings{ + { + getFails: false, + getNotFound: false, + gatewayState: vpn.GATEWAYSTATUS_DELETING, + gatewayId: "gw-1", + }, + { + getFails: false, + getNotFound: false, + gatewayState: vpn.GATEWAYSTATUS_DELETING, + gatewayId: "gw-1", + }, + { + getFails: false, + getNotFound: true, + gatewayState: vpn.GatewayStatus(""), + }, + }, + wantErr: false, + }, + { + desc: "error_state", + mockSettings: []mockSettings{ + { + gatewayState: vpn.GATEWAYSTATUS_DELETING, + gatewayId: "gw-1", + }, + { + gatewayState: vpn.GATEWAYSTATUS_ERROR, + gatewayId: "gw-1", + }, + }, + wantErr: true, + }, + { + desc: "timeout", + mockSettings: []mockSettings{ + { + getFails: false, + gatewayState: vpn.GatewayStatus("UNKNOWN_STATE"), + gatewayId: "gw-1", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + apiClient := newAPIMock(tt.mockSettings) + + handler := DeleteGatewayWaitHandler(context.Background(), apiClient, "pid", vpn.REGION_EU01, "gw-1") + + _, err := handler.WaitWithContext(context.Background()) + + if (err != nil) != tt.wantErr { + t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) + } + }) + }) + } +} From 806af61963c38241764a3dd59b067022326241a6 Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Mon, 4 May 2026 14:29:03 +0200 Subject: [PATCH 2/3] chore: ran sync-tidy --- services/vpn/go.mod | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/vpn/go.mod b/services/vpn/go.mod index 6ad1d8d46..6fdb7111a 100644 --- a/services/vpn/go.mod +++ b/services/vpn/go.mod @@ -2,7 +2,10 @@ module github.com/stackitcloud/stackit-sdk-go/services/vpn go 1.25 -require github.com/stackitcloud/stackit-sdk-go/core v0.26.0 +require ( + github.com/google/go-cmp v0.7.0 + github.com/stackitcloud/stackit-sdk-go/core v0.26.0 +) require ( github.com/golang-jwt/jwt/v5 v5.3.1 // indirect From a67637a14189879c717a5182bcb4015f42eaa974 Mon Sep 17 00:00:00 2001 From: "Inter, Sven" Date: Mon, 4 May 2026 14:52:54 +0200 Subject: [PATCH 3/3] chore: Update VPN service to version 0.7.0 --- .gitignore | 3 +++ CHANGELOG.md | 2 ++ services/vpn/CHANGELOG.md | 3 +++ services/vpn/VERSION | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cd0231d2e..e2c35c139 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ bin/ *.vscode/ go.work.sum + +# OS generated files +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b78064d7..9ffc9f38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -435,6 +435,8 @@ - **Dependencies:** Bump STACKIT SDK core module to `v0.26.0` - [v0.6.0](services/vpn/CHANGELOG.md#v060) - **Feature:** Added `_UNKNOWN_DEFAULT_OPEN_API` fallback value to all enums to handle unknown API values gracefully. + - [v0.7.0](services/vpn/CHANGELOG.md#v070) + - **Feature:** Add new wait handlers for gateway creation, update (`CreateOrUpdateGatewayWaitHandler`), and gateway deletion (`DeleteGatewayWaitHandler`) ## Release (2026-04-07) diff --git a/services/vpn/CHANGELOG.md b/services/vpn/CHANGELOG.md index 8b4a721cc..f12c6aafc 100644 --- a/services/vpn/CHANGELOG.md +++ b/services/vpn/CHANGELOG.md @@ -1,3 +1,6 @@ +## v0.7.0 +- **Feature:** Add new wait handlers for gateway creation, update (`CreateOrUpdateGatewayWaitHandler`), and gateway deletion (`DeleteGatewayWaitHandler`) + ## v0.6.0 - **Feature:** Added `_UNKNOWN_DEFAULT_OPEN_API` fallback value to all enums to handle unknown API values gracefully. diff --git a/services/vpn/VERSION b/services/vpn/VERSION index e07d136c7..e7f5d1aa6 100644 --- a/services/vpn/VERSION +++ b/services/vpn/VERSION @@ -1 +1 @@ -v0.6.0 \ No newline at end of file +v0.7.0 \ No newline at end of file