diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1770719..01b3b6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: uses: actions/checkout@v4.1.7 - name: Install Go - uses: actions/setup-go@v5.0.1 + uses: actions/setup-go@v5.0.2 with: go-version-file: go.mod diff --git a/client.go b/client.go index dc4aeb0..90576a5 100644 --- a/client.go +++ b/client.go @@ -22,9 +22,9 @@ import ( "github.com/RedisLabs/rediscloud-go-api/service/fixed/plans" "github.com/RedisLabs/rediscloud-go-api/service/fixed/plans/plan_subscriptions" fixedSubscriptions "github.com/RedisLabs/rediscloud-go-api/service/fixed/subscriptions" - "github.com/RedisLabs/rediscloud-go-api/service/latest_backups" "github.com/RedisLabs/rediscloud-go-api/service/latest_imports" + "github.com/RedisLabs/rediscloud-go-api/service/maintenance" "github.com/RedisLabs/rediscloud-go-api/service/pricing" "github.com/RedisLabs/rediscloud-go-api/service/regions" "github.com/RedisLabs/rediscloud-go-api/service/subscriptions" @@ -39,6 +39,7 @@ type Client struct { LatestBackup *latest_backups.API LatestImport *latest_imports.API Pricing *pricing.API + Maintenance *maintenance.API // fixed FixedPlans *plans.API FixedSubscriptions *fixedSubscriptions.API @@ -84,6 +85,7 @@ func NewClient(configs ...Option) (*Client, error) { LatestBackup: latest_backups.NewAPI(client, t, config.logger), LatestImport: latest_imports.NewAPI(client, t, config.logger), Pricing: pricing.NewAPI(client), + Maintenance: maintenance.NewAPI(client, t, config.logger), // fixed FixedPlans: plans.NewAPI(client, config.logger), FixedPlanSubscriptions: plan_subscriptions.NewAPI(client, config.logger), diff --git a/go.mod b/go.mod index 1a6e582..a6e1e0b 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,14 @@ go 1.22.4 require ( github.com/avast/retry-go/v4 v4.6.0 github.com/stretchr/testify v1.9.0 - golang.org/x/tools v0.22.0 + golang.org/x/tools v0.23.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - golang.org/x/mod v0.18.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 758db13..a38c232 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,12 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/maintenance_test.go b/maintenance_test.go new file mode 100644 index 0000000..56e2cd9 --- /dev/null +++ b/maintenance_test.go @@ -0,0 +1,231 @@ +package rediscloud_api + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/RedisLabs/rediscloud-go-api/redis" + "github.com/RedisLabs/rediscloud-go-api/service/maintenance" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetMaintenanceAutomatic(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + getRequest( + t, + "/subscriptions/113569/maintenance-windows", + `{ + "mode": "automatic" + }`, + ), + ), + ) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + actual, err := subject.Maintenance.Get(context.TODO(), 113569) + require.NoError(t, err) + + assert.Equal(t, &maintenance.Maintenance{ + Mode: redis.String("automatic"), + }, actual) +} + +func TestGetMaintenanceManual(t *testing.T) { + server := httptest.NewServer( + testServer( + "key", + "secret", + getRequest( + t, + "/subscriptions/113569/maintenance-windows", + `{ + "mode": "manual", + "timeZone": "string", + "windows": [ + { + "days": [ + "Monday" + ], + "startHour": 3, + "durationInHours": 8 + } + ], + "skipStatus": { + "remainingSkips": 0, + "currentSkipEnd": "string" + } + }`, + ), + ), + ) + + subject, err := clientFromTestServer(server, "key", "secret") + require.NoError(t, err) + + actual, err := subject.Maintenance.Get(context.TODO(), 113569) + require.NoError(t, err) + + assert.Equal(t, &maintenance.Maintenance{ + Mode: redis.String("manual"), + Windows: []*maintenance.Window{ + { + StartHour: redis.Int(3), + DurationInHours: redis.Int(8), + Days: []*string{redis.String("Monday")}, + }, + }, + }, actual) +} + +func TestUpdateMaintenanceAutomatic(t *testing.T) { + server := httptest.NewServer( + testServer( + "apiKey", "secret", + putRequest( + t, + "/subscriptions/113569/maintenance-windows", + `{ + "mode": "automatic" + }`, + `{ + "taskId" : "7e7b57f4-70f3-47f3-b5a4-0b3c270a9117", + "commandType" : "subscriptionMaintenanceWindowsUpdateRequest", + "status" : "received", + "description" : "Task request received and is being queued for processing.", + "timestamp" : "2024-07-15T13:25:39.304430279Z", + "links" : [ { + "href" : "https://api-staging.qa.redislabs.com/v1/tasks/7e7b57f4-70f3-47f3-b5a4-0b3c270a9117", + "rel" : "task", + "type" : "GET" + } ] + }`, + ), + getRequest( + t, + "/tasks/7e7b57f4-70f3-47f3-b5a4-0b3c270a9117", + `{ + "taskId" : "7e7b57f4-70f3-47f3-b5a4-0b3c270a9117", + "commandType" : "subscriptionMaintenanceWindowsUpdateRequest", + "status" : "processing-completed", + "description" : "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp" : "2024-07-15T13:25:41.067889681Z", + "response" : { + "resourceId" : 113972 + }, + "links" : [ { + "href" : "https://api-staging.qa.redislabs.com/v1/subscriptions/113972", + "rel" : "resource", + "type" : "GET" + }, { + "href" : "https://api-staging.qa.redislabs.com/v1/tasks/7e7b57f4-70f3-47f3-b5a4-0b3c270a9117", + "rel" : "self", + "type" : "GET" + } ] + }`, + ), + ), + ) + + subject, err := clientFromTestServer(server, "apiKey", "secret") + require.NoError(t, err) + + err = subject.Maintenance.Update( + context.TODO(), + 113569, + maintenance.Maintenance{ + Mode: redis.String("automatic"), + }, + ) + + require.NoError(t, err) +} + +func TestUpdateMaintenanceManual(t *testing.T) { + server := httptest.NewServer( + testServer( + "apiKey", "secret", + putRequest( + t, + "/subscriptions/113569/maintenance-windows", + `{ + "mode": "manual", + "windows": [ + { + "startHour": 12, + "durationInHours": 8, + "days": [ + "Monday", + "Wednesday" + ] + } + ] + }`, + `{ + "taskId" : "b6e0b40f-be10-4dce-8481-f4c4812855bc", + "commandType" : "subscriptionMaintenanceWindowsUpdateRequest", + "status" : "received", + "description" : "Task request received and is being queued for processing.", + "timestamp" : "2024-07-15T13:22:29.483556324Z", + "links" : [ { + "href" : "https://api-staging.qa.redislabs.com/v1/tasks/b6e0b40f-be10-4dce-8481-f4c4812855bc", + "rel" : "task", + "type" : "GET" + } ] + }`, + ), + getRequest( + t, + "/tasks/b6e0b40f-be10-4dce-8481-f4c4812855bc", + `{ + "taskId" : "b6e0b40f-be10-4dce-8481-f4c4812855bc", + "commandType" : "subscriptionMaintenanceWindowsUpdateRequest", + "status" : "processing-completed", + "description" : "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp" : "2024-07-15T13:22:32.934703954Z", + "response" : { + "resourceId" : 113972 + }, + "links" : [ { + "href" : "https://api-staging.qa.redislabs.com/v1/subscriptions/113972", + "rel" : "resource", + "type" : "GET" + }, { + "href" : "https://api-staging.qa.redislabs.com/v1/tasks/b6e0b40f-be10-4dce-8481-f4c4812855bc", + "rel" : "self", + "type" : "GET" + } ] + }`, + ), + ), + ) + + subject, err := clientFromTestServer(server, "apiKey", "secret") + require.NoError(t, err) + + err = subject.Maintenance.Update( + context.TODO(), + 113569, + maintenance.Maintenance{ + Mode: redis.String("manual"), + Windows: []*maintenance.Window{ + { + StartHour: redis.Int(12), + DurationInHours: redis.Int(8), + Days: []*string{ + redis.String("Monday"), + redis.String("Wednesday"), + }, + }, + }, + }, + ) + + require.NoError(t, err) +} diff --git a/service/maintenance/model.go b/service/maintenance/model.go new file mode 100644 index 0000000..4cfdd8e --- /dev/null +++ b/service/maintenance/model.go @@ -0,0 +1,34 @@ +package maintenance + +import ( + "fmt" + + "github.com/RedisLabs/rediscloud-go-api/internal" +) + +type Maintenance struct { + Mode *string `json:"mode,omitempty"` + Windows []*Window `json:"windows,omitempty"` +} + +func (o Maintenance) String() string { + return internal.ToString(o) +} + +type Window struct { + StartHour *int `json:"startHour,omitempty"` + DurationInHours *int `json:"durationInHours,omitempty"` + Days []*string `json:"days,omitempty"` +} + +func (o Window) String() string { + return internal.ToString(o) +} + +type NotFound struct { + subId int +} + +func (f *NotFound) Error() string { + return fmt.Sprintf("maintenance in subscription %d not found", f.subId) +} diff --git a/service/maintenance/service.go b/service/maintenance/service.go new file mode 100644 index 0000000..e65318c --- /dev/null +++ b/service/maintenance/service.go @@ -0,0 +1,63 @@ +package maintenance + +import ( + "context" + "fmt" + "net/http" + + "github.com/RedisLabs/rediscloud-go-api/internal" +) + +type Log interface { + Printf(format string, args ...interface{}) +} + +type HttpClient interface { + Get(ctx context.Context, name, path string, responseBody interface{}) error + Put(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error +} + +type TaskWaiter interface { + Wait(ctx context.Context, id string) error +} + +type API struct { + client HttpClient + taskWaiter TaskWaiter + logger Log +} + +func NewAPI(client HttpClient, taskWaiter TaskWaiter, logger Log) *API { + return &API{client: client, taskWaiter: taskWaiter, logger: logger} +} + +// Get will retrieve a subscription's maintenance detail +func (a *API) Get(ctx context.Context, subscription int) (*Maintenance, error) { + var m Maintenance + err := a.client.Get(ctx, fmt.Sprintf("get maintenance for subscription %d", subscription), fmt.Sprintf("/subscriptions/%d/maintenance-windows", subscription), &m) + if err != nil { + return nil, wrap404Error(subscription, err) + } + + return &m, nil +} + +// Update will update a subscription's maintenance detail +func (a *API) Update(ctx context.Context, subscription int, m Maintenance) error { + var task internal.TaskResponse + err := a.client.Put(ctx, fmt.Sprintf("update maintenance for subscription %d", subscription), fmt.Sprintf("/subscriptions/%d/maintenance-windows", subscription), m, &task) + if err != nil { + return err + } + + a.logger.Printf("Waiting for fixed database %d for subscription %d to finish being updated", subscription) + + return a.taskWaiter.Wait(ctx, *task.ID) +} + +func wrap404Error(subId int, err error) error { + if v, ok := err.(*internal.HTTPError); ok && v.StatusCode == http.StatusNotFound { + return &NotFound{subId: subId} + } + return err +}