diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f4df7e..839cdc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - name: Install Go uses: actions/setup-go@v5.0.1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 945e17e..9a9975a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/latest_backups_test.go b/latest_backups_test.go index 9962491..a19a421 100644 --- a/latest_backups_test.go +++ b/latest_backups_test.go @@ -5,6 +5,9 @@ import ( "net/http/httptest" "testing" + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/latest_backups" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -54,6 +57,29 @@ func TestGetLatestBackup(t *testing.T) { ] }`, ), + getRequest( + t, + "/tasks/50ec6172-8475-4ef6-8b3c-d61e688d8fe5", + `{ + "taskId": "50ec6172-8475-4ef6-8b3c-d61e688d8fe5", + "commandType": "databaseBackupStatusRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-04-15T09:08:07.537915Z", + "response": { + "resourceId": 51051292, + "additionalResourceId": 12, + "resource": {} + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/50ec6172-8475-4ef6-8b3c-d61e688d8fe5", + "type": "GET", + "rel": "self" + } + ] + }`, + ), )) subject, err := clientFromTestServer(server, "key", "secret") @@ -92,14 +118,35 @@ func TestGetFixedLatestBackup(t *testing.T) { `{ "taskId": "ce2cbfea-9b15-4250-a516-f014161a8dd3", "commandType": "databaseBackupStatusRequest", - "status": "processing-error", - "description": "Task request failed during processing. See error information for failure details.", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", "timestamp": "2024-04-15T09:52:26.101936Z", "response": { - "error": { - "type": "DATABASE_BACKUP_DISABLED", - "status": "400 BAD_REQUEST", - "description": "Database backup is disabled" + "resource": { + "status": "success" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/ce2cbfea-9b15-4250-a516-f014161a8dd3", + "type": "GET", + "rel": "self" + } + ] + }`, + ), + getRequest( + t, + "/tasks/ce2cbfea-9b15-4250-a516-f014161a8dd3", + `{ + "taskId": "ce2cbfea-9b15-4250-a516-f014161a8dd3", + "commandType": "databaseBackupStatusRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-04-15T09:52:26.101936Z", + "response": { + "resource": { + "status": "success" } }, "links": [ @@ -116,8 +163,22 @@ func TestGetFixedLatestBackup(t *testing.T) { subject, err := clientFromTestServer(server, "key", "secret") require.NoError(t, err) - _, err = subject.LatestBackup.GetFixed(context.TODO(), 12, 34) + actual, err := subject.LatestBackup.GetFixed(context.TODO(), 12, 34) require.NoError(t, err) + + assert.Equal(t, &latest_backups.LatestBackupStatus{ + CommandType: redis.String("databaseBackupStatusRequest"), + Description: redis.String("Request processing completed successfully and its resources are now being provisioned / de-provisioned."), + Status: redis.String("processing-completed"), + ID: redis.String("ce2cbfea-9b15-4250-a516-f014161a8dd3"), + Response: &latest_backups.Response{ + Resource: &latest_backups.Resource{ + Status: redis.String("success"), + }, + Error: nil, + }, + }, actual) + } func TestGetAALatestBackup(t *testing.T) { @@ -168,6 +229,31 @@ func TestGetAALatestBackup(t *testing.T) { ] }`, ), + getRequest( + t, + "/tasks/ce2cbfea-9b15-4250-a516-f014161a8dd3", + `{ + "taskId": "ce2cbfea-9b15-4250-a516-f014161a8dd3", + "commandType": "databaseBackupStatusRequest", + "status": "processing-error", + "description": "Task request failed during processing. See error information for failure details.", + "timestamp": "2024-04-15T09:52:26.101936Z", + "response": { + "error": { + "type": "DATABASE_BACKUP_DISABLED", + "status": "400 BAD_REQUEST", + "description": "Database backup is disabled" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/ce2cbfea-9b15-4250-a516-f014161a8dd3", + "type": "GET", + "rel": "self" + } + ] + }`, + ), )) subject, err := clientFromTestServer(server, "key", "secret") diff --git a/latest_imports_test.go b/latest_imports_test.go index 52eb111..2c28b15 100644 --- a/latest_imports_test.go +++ b/latest_imports_test.go @@ -4,6 +4,11 @@ import ( "context" "net/http/httptest" "testing" + "time" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/latest_imports" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -56,6 +61,31 @@ func TestGetLatestImportTooEarly(t *testing.T) { ] }`, ), + getRequest( + t, + "/tasks/1dfd6084-21df-40c6-829c-e9b4790e207e", + `{ + "taskId": "1dfd6084-21df-40c6-829c-e9b4790e207e", + "commandType": "databaseImportStatusRequest", + "status": "processing-error", + "description": "Task request failed during processing. See error information for failure details.", + "timestamp": "2024-04-15T10:19:07.331898Z", + "response": { + "error": { + "type": "SUBSCRIPTION_NOT_ACTIVE", + "status": "403 FORBIDDEN", + "description": "Cannot preform any actions for subscription that is not in an active state" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/1dfd6084-21df-40c6-829c-e9b4790e207e", + "type": "GET", + "rel": "self" + } + ] + }`, + ), )) subject, err := clientFromTestServer(server, "key", "secret") @@ -100,7 +130,34 @@ func TestGetFixedLatestImport(t *testing.T) { "response": { "resourceId": 51051302, "additionalResourceId": 110777, - "resource": {} + "resource": { + "status": "importing" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/e9232e43-3781-4263-a38e-f4d150e03475", + "type": "GET", + "rel": "self" + } + ] + }`, + ), + getRequest( + t, + "/tasks/e9232e43-3781-4263-a38e-f4d150e03475", + `{ + "taskId": "e9232e43-3781-4263-a38e-f4d150e03475", + "commandType": "databaseImportStatusRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-04-15T10:44:35.225468Z", + "response": { + "resourceId": 51051302, + "additionalResourceId": 110777, + "resource": { + "status": "importing" + } }, "links": [ { @@ -116,8 +173,22 @@ func TestGetFixedLatestImport(t *testing.T) { subject, err := clientFromTestServer(server, "key", "secret") require.NoError(t, err) - _, err = subject.LatestImport.GetFixed(context.TODO(), 12, 34) + actual, err := subject.LatestImport.GetFixed(context.TODO(), 12, 34) require.NoError(t, err) + + assert.Equal(t, &latest_imports.LatestImportStatus{ + CommandType: redis.String("databaseImportStatusRequest"), + Description: redis.String("Request processing completed successfully and its resources are now being provisioned / de-provisioned."), + Status: redis.String("processing-completed"), + ID: redis.String("e9232e43-3781-4263-a38e-f4d150e03475"), + Response: &latest_imports.Response{ + ID: redis.Int(51051302), + Resource: &latest_imports.Resource{ + Status: redis.String("importing"), + }, + Error: nil, + }, + }, actual) } func TestGetLatestImport(t *testing.T) { @@ -155,7 +226,58 @@ func TestGetLatestImport(t *testing.T) { "response": { "resourceId": 51051302, "additionalResourceId": 110777, - "resource": {} + "resource": { + "failureReason": "file-corrupted", + "failureReasonParams": [ + { + "key": "bytes_configured_bdb_limit", + "value": "1234" + }, + { + "key": "bytes_of_expected_dataset", + "value": "5678" + } + ], + "lastImportTime": "2024-05-21T10:36:26Z", + "status": "failed" + } + }, + "links": [ + { + "href": "https://api-staging.qa.redislabs.com/v1/tasks/e9232e43-3781-4263-a38e-f4d150e03475", + "type": "GET", + "rel": "self" + } + ] + }`, + ), + getRequest( + t, + "/tasks/e9232e43-3781-4263-a38e-f4d150e03475", + `{ + "taskId": "e9232e43-3781-4263-a38e-f4d150e03475", + "commandType": "databaseImportStatusRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-04-15T10:44:35.225468Z", + "response": { + "resourceId": 51051302, + "additionalResourceId": 110777, + "resource": { + "failureReason": "file-corrupted", + "failureReasonParams": [ + { + "key": "bytes_configured_bdb_limit", + "value": "1234" + }, + { + "key": "bytes_of_expected_dataset", + "value": "5678" + } + ], + "lastImportTime": "2024-05-21T10:36:26Z", + "status": "failed" + } }, "links": [ { @@ -171,6 +293,32 @@ func TestGetLatestImport(t *testing.T) { subject, err := clientFromTestServer(server, "key", "secret") require.NoError(t, err) - _, err = subject.LatestImport.Get(context.TODO(), 12, 34) + actual, err := subject.LatestImport.Get(context.TODO(), 12, 34) require.NoError(t, err) + + assert.Equal(t, &latest_imports.LatestImportStatus{ + CommandType: redis.String("databaseImportStatusRequest"), + Description: redis.String("Request processing completed successfully and its resources are now being provisioned / de-provisioned."), + Status: redis.String("processing-completed"), + ID: redis.String("e9232e43-3781-4263-a38e-f4d150e03475"), + Response: &latest_imports.Response{ + ID: redis.Int(51051302), + Resource: &latest_imports.Resource{ + Status: redis.String("failed"), + LastImportTime: redis.Time(time.Date(2024, 5, 21, 10, 36, 26, 0, time.UTC)), + FailureReason: redis.String("file-corrupted"), + FailureReasonParams: []*latest_imports.FailureReasonParam{ + { + Key: redis.String("bytes_configured_bdb_limit"), + Value: redis.String("1234"), + }, + { + Key: redis.String("bytes_of_expected_dataset"), + Value: redis.String("5678"), + }, + }, + }, + Error: nil, + }, + }, actual) } diff --git a/service/latest_backups/model.go b/service/latest_backups/model.go index 7f3fcb7..bfe6d8b 100644 --- a/service/latest_backups/model.go +++ b/service/latest_backups/model.go @@ -1,9 +1,9 @@ package latest_backups import ( - "encoding/json" "fmt" "regexp" + "time" "github.com/RedisLabs/rediscloud-go-api/internal" "github.com/RedisLabs/rediscloud-go-api/redis" @@ -22,23 +22,33 @@ func (o LatestBackupStatus) String() string { } type Response struct { - ID *int `json:"resourceId,omitempty"` - Resource *json.RawMessage `json:"resource,omitempty"` - Error *Error `json:"error,omitempty"` + ID *int `json:"resourceId,omitempty"` + Resource *Resource `json:"resource,omitempty"` + Error *Error `json:"error,omitempty"` } func (o Response) String() string { return internal.ToString(o) } +type Resource struct { + Status *string `json:"status,omitempty"` + LastBackupTime *time.Time `json:"lastBackupTime,omitempty"` + FailureReason *string `json:"failureReason,omitempty"` +} + +func (o Resource) String() string { + return internal.ToString(o) +} + type Error struct { Type *string `json:"type,omitempty"` Description *string `json:"description,omitempty"` Status *string `json:"status,omitempty"` } -func (o Error) String() string { - return internal.ToString(o) +func (e *Error) String() string { + return internal.ToString(e) } func (e *Error) StatusCode() string { @@ -55,36 +65,6 @@ func (e *Error) Error() string { var errorStatusCode = regexp.MustCompile("^(\\d*).*$") -func NewLatestBackupStatus(task *internal.Task) *LatestBackupStatus { - latestBackupStatus := LatestBackupStatus{ - CommandType: task.CommandType, - Description: task.Description, - Status: task.Status, - ID: task.ID, - } - - if task.Response != nil { - r := Response{ - ID: task.Response.ID, - Resource: task.Response.Resource, - } - - if task.Response.Error != nil { - e := Error{ - Type: task.Response.Error.Type, - Description: task.Response.Error.Description, - Status: task.Response.Error.Status, - } - - r.Error = &e - } - - latestBackupStatus.Response = &r - } - - return &latestBackupStatus -} - type NotFound struct { subId int dbId int diff --git a/service/latest_backups/service.go b/service/latest_backups/service.go index 3c07c82..62ff054 100644 --- a/service/latest_backups/service.go +++ b/service/latest_backups/service.go @@ -13,7 +13,7 @@ type HttpClient interface { } type TaskWaiter interface { - WaitForTask(ctx context.Context, id string) (*internal.Task, error) + Wait(ctx context.Context, id string) error } type Log interface { @@ -37,7 +37,7 @@ func (a *API) Get(ctx context.Context, subscription int, database int) (*LatestB if err != nil { return nil, wrap404Error(subscription, database, err) } - return NewLatestBackupStatus(task), nil + return task, nil } func (a *API) GetFixed(ctx context.Context, subscription int, database int) (*LatestBackupStatus, error) { @@ -47,7 +47,7 @@ func (a *API) GetFixed(ctx context.Context, subscription int, database int) (*La if err != nil { return nil, wrap404Error(subscription, database, err) } - return NewLatestBackupStatus(task), nil + return task, nil } func (a *API) GetActiveActive(ctx context.Context, subscription int, database int, region string) (*LatestBackupStatus, error) { @@ -57,19 +57,34 @@ func (a *API) GetActiveActive(ctx context.Context, subscription int, database in if err != nil { return nil, wrap404ErrorActiveActive(subscription, database, region, err) } - return NewLatestBackupStatus(task), nil + return task, nil } -func (a *API) get(ctx context.Context, message string, address string) (*internal.Task, error) { - var taskResponse internal.TaskResponse - err := a.client.Get(ctx, message, address, &taskResponse) +func (a *API) get(ctx context.Context, message string, address string) (*LatestBackupStatus, error) { + var task internal.TaskResponse + err := a.client.Get(ctx, message, address, &task) if err != nil { return nil, err } - a.logger.Printf("Waiting for backup status request %d to complete", taskResponse.ID) + a.logger.Printf("Waiting for backup status request %d to complete", task.ID) - return a.taskWaiter.WaitForTask(ctx, *taskResponse.ID) + err = a.taskWaiter.Wait(ctx, *task.ID) + + a.logger.Printf("Backup status request %d completed, possibly with error", task.ID, err) + + var backupStatusTask *LatestBackupStatus + err = a.client.Get(ctx, + fmt.Sprintf("retrieve completed backup status task %d", task.ID), + "/tasks/"+*task.ID, + &backupStatusTask, + ) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve completed backup status %d: %w", task.ID, err) + } + + return backupStatusTask, nil } func wrap404Error(subId int, dbId int, err error) error { diff --git a/service/latest_imports/model.go b/service/latest_imports/model.go index 7265ab4..8b64d35 100644 --- a/service/latest_imports/model.go +++ b/service/latest_imports/model.go @@ -1,9 +1,9 @@ package latest_imports import ( - "encoding/json" "fmt" "regexp" + "time" "github.com/RedisLabs/rediscloud-go-api/internal" "github.com/RedisLabs/rediscloud-go-api/redis" @@ -22,22 +22,42 @@ func (o LatestImportStatus) String() string { } type Response struct { - ID *int `json:"resourceId,omitempty"` - Resource *json.RawMessage `json:"resource,omitempty"` - Error *Error `json:"error,omitempty"` + ID *int `json:"resourceId,omitempty"` + Resource *Resource `json:"resource,omitempty"` + Error *Error `json:"error,omitempty"` } func (o Response) String() string { return internal.ToString(o) } +type Resource struct { + Status *string `json:"status,omitempty"` + LastImportTime *time.Time `json:"lastImportTime,omitempty"` + FailureReason *string `json:"failureReason,omitempty"` + FailureReasonParams []*FailureReasonParam `json:"failureReasonParams,omitempty"` +} + +func (o Resource) String() string { + return internal.ToString(o) +} + +type FailureReasonParam struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` +} + +func (o FailureReasonParam) String() string { + return internal.ToString(o) +} + type Error struct { Type *string `json:"type,omitempty"` Description *string `json:"description,omitempty"` Status *string `json:"status,omitempty"` } -func (e Error) String() string { +func (e *Error) String() string { return internal.ToString(e) } @@ -55,36 +75,6 @@ func (e *Error) Error() string { var errorStatusCode = regexp.MustCompile("^(\\d*).*$") -func NewLatestImportStatus(task *internal.Task) *LatestImportStatus { - latestImportStatus := LatestImportStatus{ - CommandType: task.CommandType, - Description: task.Description, - Status: task.Status, - ID: task.ID, - } - - if task.Response != nil { - r := Response{ - ID: task.Response.ID, - Resource: task.Response.Resource, - } - - if task.Response.Error != nil { - e := Error{ - Type: task.Response.Error.Type, - Description: task.Response.Error.Description, - Status: task.Response.Error.Status, - } - - r.Error = &e - } - - latestImportStatus.Response = &r - } - - return &latestImportStatus -} - type NotFound struct { subId int dbId int diff --git a/service/latest_imports/service.go b/service/latest_imports/service.go index 9d85e6f..13a934d 100644 --- a/service/latest_imports/service.go +++ b/service/latest_imports/service.go @@ -13,7 +13,7 @@ type HttpClient interface { } type TaskWaiter interface { - WaitForTask(ctx context.Context, id string) (*internal.Task, error) + Wait(ctx context.Context, id string) error } type Log interface { @@ -37,7 +37,7 @@ func (a *API) Get(ctx context.Context, subscription int, database int) (*LatestI if err != nil { return nil, wrap404Error(subscription, database, err) } - return NewLatestImportStatus(task), nil + return task, nil } func (a *API) GetFixed(ctx context.Context, subscription int, database int) (*LatestImportStatus, error) { @@ -47,19 +47,34 @@ func (a *API) GetFixed(ctx context.Context, subscription int, database int) (*La if err != nil { return nil, wrap404Error(subscription, database, err) } - return NewLatestImportStatus(task), nil + return task, nil } -func (a *API) get(ctx context.Context, message string, address string) (*internal.Task, error) { - var taskResponse internal.TaskResponse - err := a.client.Get(ctx, message, address, &taskResponse) +func (a *API) get(ctx context.Context, message string, address string) (*LatestImportStatus, error) { + var task internal.TaskResponse + err := a.client.Get(ctx, message, address, &task) if err != nil { return nil, err } - a.logger.Printf("Waiting for backup status request %d to complete", taskResponse.ID) + a.logger.Printf("Waiting for import status request %d to complete", task.ID) - return a.taskWaiter.WaitForTask(ctx, *taskResponse.ID) + err = a.taskWaiter.Wait(ctx, *task.ID) + + a.logger.Printf("Import status request %d completed, possibly with error", task.ID, err) + + var importStatusTask *LatestImportStatus + err = a.client.Get(ctx, + fmt.Sprintf("retrieve completed import status task %d", task.ID), + "/tasks/"+*task.ID, + &importStatusTask, + ) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve completed import status %d: %w", task.ID, err) + } + + return importStatusTask, nil } func wrap404Error(subId int, dbId int, err error) error {