diff --git a/examples/iaasalpha/virtual_ip/virtual_ip.go b/examples/iaasalpha/virtual_ip/virtual_ip.go new file mode 100644 index 000000000..bda1bcb69 --- /dev/null +++ b/examples/iaasalpha/virtual_ip/virtual_ip.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait" +) + +func main() { + // Specify the project ID and network ID + projectId := "PROJECT_ID" + networkId := "NETWORK_ID" + + // Create a new API client, that uses default authentication and configuration + iaasalphaClient, err := iaasalpha.NewAPIClient( + config.WithRegion("eu01"), + ) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Creating API client: %v\n", err) + os.Exit(1) + } + + virtualIPs, err := iaasalphaClient.ListVirtualIPs(context.Background(), projectId, networkId).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when calling `ListVirtualIPs`: %v\n", err) + } else { + fmt.Printf("[iaasalpha API] Number of virtual IPs: %v\n", len(*virtualIPs.Items)) + } + + // Create a virtual IP + createVirtualIPPayload := iaasalpha.CreateVirtualIPPayload{ + Name: utils.Ptr("example-vip"), + Labels: &map[string]interface{}{ + "key": "value", + }, + } + virtualIP, err := iaasalphaClient.CreateVirtualIP(context.Background(), projectId, networkId).CreateVirtualIPPayload(createVirtualIPPayload).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when calling `CreateVirtualIP`: %v\n", err) + } else { + fmt.Printf("[iaasalpha API] Triggered creation of virtual IP with ID %q.\n", *virtualIP.Id) + } + + // Wait for creation of the virtual IP + virtualIP, err = wait.CreateVirtualIPWaitHandler(context.Background(), iaasalphaClient, projectId, networkId, *virtualIP.Id).WaitWithContext(context.Background()) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when waiting for creation: %v\n", err) + os.Exit(1) + } + + fmt.Printf("[iaasalpha API] Virtual IP %q has been successfully created.\n", *virtualIP.Id) + + // Delete a virtual IP + err = iaasalphaClient.DeleteVirtualIP(context.Background(), projectId, networkId, *virtualIP.Id).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when calling `DeleteVirtualIP`: %v\n", err) + } else { + fmt.Printf("[iaasalpha API] Triggered deletion of virtual IP with ID %q.\n", *virtualIP.Id) + } + + // Wait for deletion of the virtual IP + _, err = wait.DeleteVirtualIPWaitHandler(context.Background(), iaasalphaClient, projectId, networkId, *virtualIP.Id).WaitWithContext(context.Background()) + if err != nil { + fmt.Fprintf(os.Stderr, "[iaasalpha API] Error when waiting for deletion: %v\n", err) + os.Exit(1) + } + + fmt.Printf("[iaasalpha API] Virtual IP %q has been successfully deleted.\n", *virtualIP.Id) +} diff --git a/services/iaasalpha/wait/wait.go b/services/iaasalpha/wait/wait.go index 598c0a80c..437f8b677 100644 --- a/services/iaasalpha/wait/wait.go +++ b/services/iaasalpha/wait/wait.go @@ -12,11 +12,15 @@ import ( ) const ( + DeleteSuccess = "DELETED" + ErrorStatus = "ERROR" + VolumeAvailableStatus = "AVAILABLE" - DeleteSuccess = "DELETED" - ErrorStatus = "ERROR" - ServerActiveStatus = "ACTIVE" - ServerResizingStatus = "RESIZING" + + ServerActiveStatus = "ACTIVE" + ServerResizingStatus = "RESIZING" + + VirtualIpCreatedStatus = "CREATED" RequestCreateAction = "CREATE" RequestUpdateAction = "UPDATE" @@ -35,6 +39,7 @@ type APIClientInterface interface { GetServerExecute(ctx context.Context, projectId string, serverId string) (*iaasalpha.Server, error) GetProjectRequestExecute(ctx context.Context, projectId string, requestId string) (*iaasalpha.Request, error) GetAttachedVolumeExecute(ctx context.Context, projectId string, serverId string, volumeId string) (*iaasalpha.VolumeAttachment, error) + GetVirtualIPExecute(ctx context.Context, projectId string, networkId string, virtualIpId string) (*iaasalpha.VirtualIp, error) } // CreateVolumeWaitHandler will wait for volume creation @@ -291,3 +296,53 @@ func RemoveVolumeFromServerWaitHandler(ctx context.Context, a APIClientInterface handler.SetTimeout(10 * time.Minute) return handler } + +// CreateVirtualIPWaitHandler will wait for server creation +func CreateVirtualIPWaitHandler(ctx context.Context, a APIClientInterface, projectId, networkId, virtualIpId string) *wait.AsyncActionHandler[iaasalpha.VirtualIp] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.VirtualIp, err error) { + virtualIp, err := a.GetVirtualIPExecute(ctx, projectId, networkId, virtualIpId) + if err != nil { + return false, virtualIp, err + } + if virtualIp.Id == nil || virtualIp.Status == nil { + return false, virtualIp, fmt.Errorf("create failed for virtual ip with id %s, the response is not valid: the id or the status are missing", networkId) + } + if *virtualIp.Id == virtualIpId && *virtualIp.Status == VirtualIpCreatedStatus { + return true, virtualIp, nil + } + if *virtualIp.Id == virtualIpId && *virtualIp.Status == ErrorStatus { + return true, virtualIp, fmt.Errorf("create failed for virtual ip with id %s", networkId) + } + return false, virtualIp, nil + }) + handler.SetTimeout(15 * time.Minute) + return handler +} + +// DeleteVirtualIPWaitHandler will wait for volume deletion +func DeleteVirtualIPWaitHandler(ctx context.Context, a APIClientInterface, projectId, networkId, virtualIpId string) *wait.AsyncActionHandler[iaasalpha.VirtualIp] { + handler := wait.New(func() (waitFinished bool, response *iaasalpha.VirtualIp, err error) { + virtualIp, err := a.GetVirtualIPExecute(ctx, projectId, networkId, virtualIpId) + if err == nil { + if virtualIp != nil { + if virtualIp.Id == nil || virtualIp.Status == nil { + return false, virtualIp, fmt.Errorf("delete failed for virtual ip with id %s, the response is not valid: the id or the status are missing", virtualIpId) + } + if *virtualIp.Id == virtualIpId && *virtualIp.Status == DeleteSuccess { + return true, virtualIp, 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, virtualIp, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError: %w", err) + } + if oapiErr.StatusCode != http.StatusNotFound { + return false, virtualIp, err + } + return true, nil, nil + }) + handler.SetTimeout(15 * time.Minute) + return handler +} diff --git a/services/iaasalpha/wait/wait_test.go b/services/iaasalpha/wait/wait_test.go index 77b26e912..6a222e511 100644 --- a/services/iaasalpha/wait/wait_test.go +++ b/services/iaasalpha/wait/wait_test.go @@ -16,6 +16,7 @@ type apiClientMocked struct { getServerFails bool getProjectRequestFails bool getAttachedVolumeFails bool + getVirtualIPFails bool isDeleted bool isAttached bool resourceState string @@ -102,6 +103,25 @@ func (a *apiClientMocked) GetAttachedVolumeExecute(_ context.Context, _, _, _ st }, nil } +func (a *apiClientMocked) GetVirtualIPExecute(_ context.Context, _, _, _ string) (*iaasalpha.VirtualIp, error) { + if a.getVirtualIPFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 500, + } + } + + if a.isDeleted { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: 404, + } + } + + return &iaasalpha.VirtualIp{ + Id: utils.Ptr("vipid"), + Status: &a.resourceState, + }, nil +} + func TestCreateVolumeWaitHandler(t *testing.T) { tests := []struct { desc string @@ -644,3 +664,130 @@ func TestRemoveVolumeFromServerWaitHandler(t *testing.T) { }) } } + +func TestCreateVirtualIPWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + resourceState string + wantErr bool + wantResp bool + }{ + { + desc: "create_succeeded", + getFails: false, + resourceState: VirtualIpCreatedStatus, + wantErr: false, + wantResp: true, + }, + { + desc: "error_status", + getFails: false, + resourceState: ErrorStatus, + wantErr: true, + wantResp: true, + }, + { + desc: "get_fails", + getFails: true, + resourceState: "", + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + resourceState: "ANOTHER Status", + wantErr: true, + wantResp: true, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getVirtualIPFails: tt.getFails, + resourceState: tt.resourceState, + } + + var wantRes *iaasalpha.VirtualIp + if tt.wantResp { + wantRes = &iaasalpha.VirtualIp{ + Id: utils.Ptr("vipid"), + Status: utils.Ptr(tt.resourceState), + } + } + + handler := CreateVirtualIPWaitHandler(context.Background(), apiClient, "pid", "nid", "vipid") + + 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) + } + }) + } +} + +func TestDeleteVirtualIPWaitHandler(t *testing.T) { + tests := []struct { + desc string + getFails bool + isDeleted bool + resourceState string + wantErr bool + wantResp bool + }{ + { + desc: "delete_succeeded", + getFails: false, + isDeleted: true, + wantErr: false, + wantResp: false, + }, + { + desc: "get_fails", + getFails: true, + resourceState: "", + wantErr: true, + wantResp: false, + }, + { + desc: "timeout", + getFails: false, + resourceState: "ANOTHER Status", + wantErr: true, + wantResp: false, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + apiClient := &apiClientMocked{ + getVolumeFails: tt.getFails, + isDeleted: tt.isDeleted, + resourceState: tt.resourceState, + } + + var wantRes *iaasalpha.VirtualIp + if tt.wantResp { + wantRes = &iaasalpha.VirtualIp{ + Id: utils.Ptr("vipid"), + Status: utils.Ptr(tt.resourceState), + } + } + + handler := DeleteVirtualIPWaitHandler(context.Background(), apiClient, "pid", "nid", "vipid") + + 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) + } + }) + } +}