diff --git a/examples/logs/go.mod b/examples/logs/go.mod new file mode 100644 index 000000000..e551bf146 --- /dev/null +++ b/examples/logs/go.mod @@ -0,0 +1,13 @@ +module github.com/stackitcloud/stackit-sdk-go/examples/logs + +go 1.21 + +require ( + github.com/stackitcloud/stackit-sdk-go/core v0.20.1 + github.com/stackitcloud/stackit-sdk-go/services/logs v0.1.0 +) + +require ( + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect +) diff --git a/examples/logs/go.sum b/examples/logs/go.sum new file mode 100644 index 000000000..a89c3d692 --- /dev/null +++ b/examples/logs/go.sum @@ -0,0 +1,10 @@ +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/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.20.1 h1:odiuhhRXmxvEvnVTeZSN9u98edvw2Cd3DcnkepncP3M= +github.com/stackitcloud/stackit-sdk-go/core v0.20.1/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= +github.com/stackitcloud/stackit-sdk-go/services/logs v0.1.0 h1:Fck91pz2Oxk8dUd2lOdnsIMWSCzBzAHJp7ivqAJ59is= +github.com/stackitcloud/stackit-sdk-go/services/logs v0.1.0/go.mod h1:VM+++rhzI2/lvhyGKg0FCiEfnrADWykcdHLbECrl6T0= diff --git a/examples/logs/logs.go b/examples/logs/logs.go new file mode 100644 index 000000000..25cdea8a5 --- /dev/null +++ b/examples/logs/logs.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "log" + + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/logs" +) + +func main() { + ctx := context.Background() + + projectId := "PROJECT_ID" // the uuid of your STACKIT project + regionId := "eu01" + + client, err := logs.NewAPIClient( + config.WithRegion(regionId), + ) + if err != nil { + log.Fatalf("[Logs API] Creating API client: %v\n", err) + } + + // Create a Logs Instance + var createdInstance string + createInstancePayload := logs.CreateLogsInstancePayload{ + DisplayName: utils.Ptr("my-logs-instance"), + RetentionDays: utils.Ptr(int64(1)), + } + createResp, err := client.CreateLogsInstance(ctx, projectId, regionId). + CreateLogsInstancePayload(createInstancePayload). + Execute() + if err != nil { + log.Fatalf("[Logs API] Error when calling `CreateLogsInstance`: %v\n", err) + } + createdInstance = *createResp.Id + log.Printf("[Logs API] Created Logs Instance with ID \"%s\".\n", createdInstance) + + // List Logs Instances + listResp, err := client.ListLogsInstances(ctx, projectId, regionId).Execute() + if err != nil { + log.Fatalf("[Logs API] Error when calling `ListLogsInstances`: %v\n", err) + } + log.Printf("[Logs API] Retrieved %d Logs Instances.\n", len(*listResp.Instances)) + + // Get the created Logs Instance + getResp, err := client.GetLogsInstance(ctx, projectId, regionId, createdInstance).Execute() + if err != nil { + log.Fatalf("[Logs API] Error when calling `GetLogsInstance`: %v\n", err) + } + log.Printf("[Logs API] Retrieved Logs Instance with ID \"%s\" and Display Name \"%s\".\n", *getResp.Id, *getResp.DisplayName) + + // Update the created Logs Instance + updatePayload := logs.UpdateLogsInstancePayload{ + DisplayName: utils.Ptr("my-updated-logs-instance"), + RetentionDays: utils.Ptr(int64(7)), + } + updateResp, err := client.UpdateLogsInstance(ctx, projectId, regionId, createdInstance). + UpdateLogsInstancePayload(updatePayload). + Execute() + if err != nil { + log.Fatalf("[Logs API] Error when calling `UpdateLogsInstance`: %v\n", err) + } + log.Printf("[Logs API] Updated Logs Instance with ID \"%s\" to Display Name \"%s\".\n", *updateResp.Id, *updateResp.DisplayName) + + // Create an Access Token + createTokenPayload := logs.CreateAccessTokenPayload{ + DisplayName: utils.Ptr("my-access-token"), + Permissions: &[]string{"read"}, + } + createTokenResp, err := client.CreateAccessToken(ctx, projectId, regionId, createdInstance). + CreateAccessTokenPayload(createTokenPayload). + Execute() + if err != nil { + log.Fatalf("[Logs API] Error when calling `CreateAccessToken`: %v\n", err) + } + log.Printf("[Logs API] Created Access Token with ID \"%s\".\n", *createTokenResp.Id) + + // Add Access Token to Logs Instance + err = client.UpdateAccessToken(ctx, projectId, regionId, createdInstance, *createTokenResp.Id). + // needs at least an empty payload + UpdateAccessTokenPayload(logs.UpdateAccessTokenPayload{}). + Execute() + if err != nil { + log.Fatalf("[Logs API] Error when calling `UpdateAccessToken`: %v\n", err) + } + + // Delete all Access Tokens from Logs Instance + tokenList, err := client.DeleteAllAccessTokens(ctx, projectId, regionId, createdInstance).Execute() + if err != nil { + log.Fatalf("[Logs API] Error when calling `DeleteAllAccessTokens`: %v\n", err) + } + log.Printf("[Logs API] Deleted %d Access Tokens from Logs Instance with ID \"%s\".\n", len(*tokenList.Tokens), createdInstance) + + // Delete the created Logs Instance + err = client.DeleteLogsInstance(ctx, projectId, regionId, createdInstance).Execute() + if err != nil { + log.Fatalf("[Logs API] Error when calling `DeleteLogsInstance`: %v\n", err) + } + log.Printf("[Logs API] Deleted Logs Instance with ID \"%s\".\n", createdInstance) +} diff --git a/go.work b/go.work index 860a2a575..f661cc459 100644 --- a/go.work +++ b/go.work @@ -14,6 +14,7 @@ use ( ./examples/kms ./examples/loadbalancer ./examples/logme + ./examples/logs ./examples/mariadb ./examples/middleware ./examples/mongodbflex diff --git a/services/logs/go.mod b/services/logs/go.mod index 453bc6e61..a4ff81ccb 100644 --- a/services/logs/go.mod +++ b/services/logs/go.mod @@ -3,6 +3,7 @@ module github.com/stackitcloud/stackit-sdk-go/services/logs 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.20.0 ) diff --git a/services/logs/wait/wait.go b/services/logs/wait/wait.go new file mode 100644 index 000000000..50efa657b --- /dev/null +++ b/services/logs/wait/wait.go @@ -0,0 +1,57 @@ +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/logs" +) + +type APIClientInterface interface { + GetLogsInstanceExecute(ctx context.Context, projectId string, regionId string, instanceId string) (*logs.LogsInstance, error) +} + +func CreateLogsInstanceWaitHandler(ctx context.Context, client APIClientInterface, projectID, region, instanceID string) *wait.AsyncActionHandler[logs.LogsInstance] { + handler := wait.New(func() (waitFinished bool, response *logs.LogsInstance, err error) { + instance, err := client.GetLogsInstanceExecute(ctx, projectID, region, instanceID) + if err != nil { + return false, nil, err + } + if instance.Id == nil || instance.Status == nil { + return false, nil, fmt.Errorf("get instance, project: %q, region: %q, instanceID: %q: missing id or status", projectID, region, instanceID) + } + if *instance.Id == instanceID && *instance.Status == logs.LOGSINSTANCESTATUS_ACTIVE { + return true, instance, nil + } + if *instance.Status == logs.LOGSINSTANCESTATUS_DELETING { + return true, nil, fmt.Errorf("creating log instance failed, instance is being deleted") + } + return false, nil, nil + }) + handler.SetTimeout(10 * time.Minute) + return handler +} + +func DeleteLogsInstanceWaitHandler(ctx context.Context, client APIClientInterface, projectID, region, instanceID string) *wait.AsyncActionHandler[logs.LogsInstance] { + handler := wait.New(func() (waitFinished bool, response *logs.LogsInstance, err error) { + _, err = client.GetLogsInstanceExecute(ctx, projectID, region, 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/logs/wait/wait_test.go b/services/logs/wait/wait_test.go new file mode 100644 index 000000000..0c9c2cb80 --- /dev/null +++ b/services/logs/wait/wait_test.go @@ -0,0 +1,169 @@ +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/logs" +) + +type apiClientMock struct { + getFails bool + returnInstance bool + statusCode int + getLogsResponse *logs.LogsInstance +} + +func (a *apiClientMock) GetLogsInstanceExecute(_ context.Context, _, _, _ string) (*logs.LogsInstance, error) { + if a.getFails { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: a.statusCode, + } + } + if !a.returnInstance { + return nil, nil + } + return a.getLogsResponse, nil +} + +var projectId = uuid.NewString() +var instanceId = uuid.NewString() + +const region = "eu01" + +func TestCreateLogsInstanceWaitHandler(t *testing.T) { + tests := []struct { + description string + getFails bool + wantErr bool + wantResp bool + returnInstance bool + getLogsResponse *logs.LogsInstance + }{ + { + description: "create succeeded", + getFails: false, + wantErr: false, + wantResp: true, + returnInstance: true, + getLogsResponse: &logs.LogsInstance{ + Id: utils.Ptr(instanceId), + Status: utils.Ptr(logs.LOGSINSTANCESTATUS_ACTIVE), + }, + }, + { + description: "create failed with error", + getFails: true, + wantErr: true, + wantResp: false, + returnInstance: true, + getLogsResponse: &logs.LogsInstance{ + Id: utils.Ptr(instanceId), + Status: utils.Ptr(logs.LOGSINSTANCESTATUS_ACTIVE), + }, + }, + { + description: "create without id", + getFails: false, + wantErr: true, + wantResp: false, + returnInstance: true, + getLogsResponse: &logs.LogsInstance{ + Status: utils.Ptr(logs.LOGSINSTANCESTATUS_ACTIVE), + }, + }, + { + description: "create without status", + getFails: false, + wantErr: true, + wantResp: false, + returnInstance: true, + getLogsResponse: &logs.LogsInstance{ + Id: utils.Ptr(instanceId), + }, + }, + { + description: "instance deleting", + getFails: false, + wantErr: true, + wantResp: false, + returnInstance: true, + getLogsResponse: &logs.LogsInstance{ + Id: utils.Ptr(instanceId), + Status: utils.Ptr(logs.LOGSINSTANCESTATUS_DELETING), + }, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &apiClientMock{ + getFails: tt.getFails, + getLogsResponse: tt.getLogsResponse, + returnInstance: tt.returnInstance, + } + var instanceWanted *logs.LogsInstance + if tt.wantResp { + instanceWanted = tt.getLogsResponse + } + + handler := CreateLogsInstanceWaitHandler(context.Background(), client, projectId, region, 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 TestDeleteLogsInstanceWaitHandler(t *testing.T) { + tests := []struct { + description string + getFails bool + wantErr bool + statusCode int + }{ + { + description: "delete succeeded", + getFails: true, + statusCode: http.StatusNotFound, + }, + { + description: "delete failed with error", + getFails: true, + wantErr: true, + statusCode: http.StatusInternalServerError, + }, + { + description: "delete still in progress", + getFails: false, + wantErr: true, + statusCode: http.StatusOK, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &apiClientMock{ + getFails: tt.getFails, + returnInstance: false, + statusCode: tt.statusCode, + getLogsResponse: nil, + } + handler := DeleteLogsInstanceWaitHandler(context.Background(), client, projectId, region, instanceId) + _, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background()) + if (err != nil) != tt.wantErr { + t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}