diff --git a/CHANGELOG.md b/CHANGELOG.md index e379bdaf4..5558bbc75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -467,6 +467,9 @@ - `v1api`: New package which can be used for communication with the STACKIT vpn v1 API - `v1beta1api`: Align package to latest API specification - `v1alpha1api`: Align package to latest API specification + - [v0.8.0](services/vpn/CHANGELOG.md#v080) + - `v1api`: + - **Feature:** Add new wait handlers for gateway creation (`CreateGatewayWaitHandler`), update (`UpdateGatewayWaitHandler`), and deletion (`DeleteGatewayWaitHandler`) ## Release (2026-04-07) - `alb`: [v0.13.1](services/alb/CHANGELOG.md#v0131) diff --git a/examples/vpn/go.mod b/examples/vpn/go.mod new file mode 100644 index 000000000..4ca5551ad --- /dev/null +++ b/examples/vpn/go.mod @@ -0,0 +1,16 @@ +module github.com/stackitcloud/stackit-sdk-go/examples/vpn + +go 1.25 + +// This is not needed in production. This is only here to point the golangci linter to the local version instead of the last release on GitHub. +replace github.com/stackitcloud/stackit-sdk-go/services/vpn => ../../services/vpn + +require ( + github.com/stackitcloud/stackit-sdk-go/core v0.26.0 + github.com/stackitcloud/stackit-sdk-go/services/vpn v0.8.0 +) + +require ( + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/uuid v1.6.0 // indirect +) diff --git a/examples/vpn/go.sum b/examples/vpn/go.sum new file mode 100644 index 000000000..3712a0c87 --- /dev/null +++ b/examples/vpn/go.sum @@ -0,0 +1,8 @@ +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/stackitcloud/stackit-sdk-go/core v0.26.0 h1:jQEb9gkehfp6VCP6TcYk7BI10cz4l0KM2L6hqYBH2QA= +github.com/stackitcloud/stackit-sdk-go/core v0.26.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= diff --git a/examples/vpn/vpn.go b/examples/vpn/vpn.go new file mode 100644 index 000000000..14c295baf --- /dev/null +++ b/examples/vpn/vpn.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/stackitcloud/stackit-sdk-go/core/config" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" +) + +func main() { + region := "eu01" // Region where the resources will be created + projectId := "PROJECT_ID" // the uuid of your STACKIT project + planId := "PLAN_ID" // the id of the plan you want to use for the VPN Gateway. You can get the available plans with `ListPlans`. + + // STACKIT VPN enforces the following requirements for a secure PSK: + // - must be at least 20 characters long + // - must be at least 16 different characters + // - must have at least one upper case letter + // - must have at least one lower case letter + // - must have at least one number + psk := "Super.$ecret_Shared3Key12345" + + // Create a new API client, that uses default authentication and configuration + vpnClient, err := vpn.NewAPIClient( + config.WithRegion(region), + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Creating API client: %v\n", err) + os.Exit(1) + } + + // Create a VPN Gateway + createVpnGatewayPayload := vpn.CreateGatewayPayload{ + DisplayName: "exampleVpnGateway", + PlanId: planId, + RoutingType: vpn.ROUTINGTYPE_ROUTE_BASED, + AvailabilityZones: vpn.CreateGatewayPayloadAvailabilityZones{ + Tunnel1: "eu01-1", + Tunnel2: "eu01-2", + }, + } + + gatewayResp, err := vpnClient.DefaultAPI.CreateGateway(context.Background(), projectId, vpn.Region(region)).CreateGatewayPayload(createVpnGatewayPayload).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `CreateVpnGateway`: %v\n", err) + os.Exit(1) + } + fmt.Printf("Created VPN Gateway with id \"%s\".\n", *gatewayResp.Id) + + // Create a VPN Connection + phase1 := vpn.TunnelConfigurationPhase1{ + DhGroups: []string{"ecp384"}, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + } + phase2 := vpn.TunnelConfigurationPhase2{ + DhGroups: []string{"ecp384"}, + EncryptionAlgorithms: []string{"aes256"}, + IntegrityAlgorithms: []string{"sha2_384"}, + } + tunnel := vpn.TunnelConfiguration{ + Phase1: phase1, + Phase2: phase2, + PreSharedKey: &psk, + RemoteAddress: "0.0.0.0", + } + + createGatewayConnectionPayload := vpn.CreateGatewayConnectionPayload{ + DisplayName: "exampleVpnConnection", + Tunnel1: tunnel, + Tunnel2: tunnel, + } + connectionResp, err := vpnClient.DefaultAPI.CreateGatewayConnection(context.Background(), projectId, vpn.Region(region), *gatewayResp.Id).CreateGatewayConnectionPayload(createGatewayConnectionPayload).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `CreateVpnConnection`: %v\n", err) + os.Exit(1) + } + fmt.Printf("Created VPN Connection with id \"%s\".\n", *connectionResp.Id) + + // Delete the VPN Connection + err = vpnClient.DefaultAPI.DeleteGatewayConnection(context.Background(), projectId, vpn.Region(region), *gatewayResp.Id, *connectionResp.Id).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DeleteVpnConnection`: %v\n", err) + os.Exit(1) + } + fmt.Printf("Deleted VPN Connection with id \"%s\".\n", *connectionResp.Id) + + // Delete the VPN Gateway + err = vpnClient.DefaultAPI.DeleteGateway(context.Background(), projectId, vpn.Region(region), *gatewayResp.Id).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DeleteVpnGateway`: %v\n", err) + os.Exit(1) + } + fmt.Printf("Deleted VPN Gateway with id \"%s\".\n", *gatewayResp.Id) +} diff --git a/go.work b/go.work index a6c578ab7..976faa29d 100644 --- a/go.work +++ b/go.work @@ -37,6 +37,7 @@ use ( ./examples/sqlserverflex ./examples/telemetrylink ./examples/telemetryrouter + ./examples/vpn ./examples/waiter ./services/alb ./services/albwaf diff --git a/services/vpn/CHANGELOG.md b/services/vpn/CHANGELOG.md index 3227044a6..e35c178a7 100644 --- a/services/vpn/CHANGELOG.md +++ b/services/vpn/CHANGELOG.md @@ -1,3 +1,7 @@ +## v0.8.0 +- `v1api`: + - **Feature:** Add new wait handlers for gateway creation (`CreateGatewayWaitHandler`), update (`UpdateGatewayWaitHandler`), and deletion (`DeleteGatewayWaitHandler`) + ## v0.7.0 - `v1api`: New package which can be used for communication with the STACKIT vpn v1 API - `v1beta1api`: Align package to latest API specification diff --git a/services/vpn/VERSION b/services/vpn/VERSION index e7f5d1aa6..4ea5cafac 100644 --- a/services/vpn/VERSION +++ b/services/vpn/VERSION @@ -1 +1 @@ -v0.7.0 \ No newline at end of file +v0.8.0 \ No newline at end of file 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 diff --git a/services/vpn/v1api/wait/wait.go b/services/vpn/v1api/wait/wait.go new file mode 100644 index 000000000..1217454b1 --- /dev/null +++ b/services/vpn/v1api/wait/wait.go @@ -0,0 +1,59 @@ +package wait + +import ( + "context" + "errors" + "time" + + "github.com/stackitcloud/stackit-sdk-go/core/wait" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" +) + +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.GetGateway(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 CreateGatewayWaitHandler(ctx context.Context, a vpn.DefaultAPI, projectId string, region vpn.Region, gatewayId string) *wait.AsyncActionHandler[vpn.GatewayResponse] { + return createOrUpdateGatewayWaitHandler(ctx, a, projectId, region, gatewayId) +} + +func UpdateGatewayWaitHandler(ctx context.Context, a vpn.DefaultAPI, projectId string, region vpn.Region, gatewayId string) *wait.AsyncActionHandler[vpn.GatewayResponse] { + return createOrUpdateGatewayWaitHandler(ctx, a, projectId, region, gatewayId) +} + +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.GetGateway(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}, + } + + handler := wait.New(waitConfig.Wait()) + handler.SetTimeout(20 * time.Minute) + return handler +} diff --git a/services/vpn/v1api/wait/wait_test.go b/services/vpn/v1api/wait/wait_test.go new file mode 100644 index 000000000..ae8228e99 --- /dev/null +++ b/services/vpn/v1api/wait/wait_test.go @@ -0,0 +1,415 @@ +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/v1api" +) + +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{ + GetGatewayExecuteMock: utils.Ptr(func(_ vpn.ApiGetGatewayRequest) (*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 TestCreateGatewayWaitHandler(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 := CreateGatewayWaitHandler(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 TestUpdateGatewayWaitHandler(t *testing.T) { + tests := []struct { + desc string + mockSettings []mockSettings + wantGatewayState vpn.GatewayStatus + wantErr bool + wantResp bool + }{ + { + desc: "update_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 := UpdateGatewayWaitHandler(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) + } + }) + }) + } +}