diff --git a/mongodbatlas/checkpoints.go b/mongodbatlas/checkpoints.go new file mode 100644 index 000000000..71e310941 --- /dev/null +++ b/mongodbatlas/checkpoints.go @@ -0,0 +1,103 @@ +package mongodbatlas + +import ( + "context" + "fmt" + "net/http" +) + +const ( + backupCheckpoints = "groups/%s/clusters/%s/backupCheckpoints" +) + +// CheckpointsService is an interface for interfacing with the Checkpoint +// endpoints of the MongoDB Atlas API. +type CheckpointsService interface { + List(context.Context, string, string, *ListOptions) (*Checkpoints, *Response, error) + Get(context.Context, string, string, string) (*Checkpoint, *Response, error) +} + +// CheckpointsServiceOp handles communication with the checkpoint related methods of the +// MongoDB Atlas API +type CheckpointsServiceOp struct { + Client RequestDoer +} + +var _ CheckpointsService = &CheckpointsServiceOp{} + +// Checkpoint represents MongoDB Checkpoint +type Checkpoint struct { + ClusterID string `json:"clusterId"` + Completed string `json:"completed,omitempty"` + GroupID string `json:"groupId"` + ID string `json:"id,omitempty"` // Unique identifier of the checkpoint. + Links []*Link `json:"links,omitempty"` // One or more links to sub-resources and/or related resources. + Parts []*Part `json:"parts,omitempty"` + Restorable bool `json:"restorable"` + Started string `json:"started"` + Timestamp string `json:"timestamp"` +} + +// CheckpointPart represents the individual parts that comprise the complete checkpoint. +type CheckpointPart struct { + ShardName string `json:"shardName"` + TokenDiscovered bool `json:"tokenDiscovered"` + TokenTimestamp SnapshotTimestamp `json:"tokenTimestamp"` +} + +// Checkpoints represents all the backup checkpoints related to a cluster. +type Checkpoints struct { + Results []*Checkpoint `json:"results,omitempty"` // Includes one Checkpoint object for each item detailed in the results array section. + Links []*Link `json:"links,omitempty"` // One or more links to sub-resources and/or related resources. + TotalCount int `json:"totalCount,omitempty"` // Count of the total number of items in the result set. It may be greater than the number of objects in the results array if the entire result set is paginated. +} + +// List all checkpoints for the specified sharded cluster. +// See more: https://docs.atlas.mongodb.com/reference/api/checkpoints-get-all/ +func (s CheckpointsServiceOp) List(ctx context.Context, groupID, clusterName string, listOptions *ListOptions) (*Checkpoints, *Response, error) { + if groupID == "" { + return nil, nil, NewArgError("groupId", "must be set") + } + if clusterName == "" { + return nil, nil, NewArgError("clusterName", "must be set") + } + + path := fmt.Sprintf(backupCheckpoints, groupID, clusterName) + + req, err := s.Client.NewRequest(ctx, http.MethodGet, path, listOptions) + if err != nil { + return nil, nil, err + } + + root := new(Checkpoints) + resp, err := s.Client.Do(ctx, req, root) + + return root, resp, err +} + +// Get one checkpoint for the specified sharded cluster. +// See more: https://docs.atlas.mongodb.com/reference/api/checkpoints-get-one/ +func (s CheckpointsServiceOp) Get(ctx context.Context, groupID, clusterName, checkpointID string) (*Checkpoint, *Response, error) { + if groupID == "" { + return nil, nil, NewArgError("groupId", "must be set") + } + if clusterName == "" { + return nil, nil, NewArgError("clusterName", "must be set") + } + if checkpointID == "" { + return nil, nil, NewArgError("checkpointID", "must be set") + } + + basePath := fmt.Sprintf(backupCheckpoints, groupID, clusterName) + path := fmt.Sprintf("%s/%s", basePath, checkpointID) + req, err := s.Client.NewRequest(ctx, http.MethodGet, path, nil) + + if err != nil { + return nil, nil, err + } + + root := new(Checkpoint) + resp, err := s.Client.Do(ctx, req, root) + + return root, resp, err +} diff --git a/mongodbatlas/checkpoints_test.go b/mongodbatlas/checkpoints_test.go new file mode 100644 index 000000000..34efca50f --- /dev/null +++ b/mongodbatlas/checkpoints_test.go @@ -0,0 +1,367 @@ +package mongodbatlas + +import ( + "fmt" + "net/http" + "testing" + + "github.com/go-test/deep" +) + +func TestCheckpoints_List(t *testing.T) { + setup() + defer teardown() + + groupID := "6b8cd3c380eef5349ef77gf7" + clusterName := "Cluster0" + + path := fmt.Sprintf("/groups/%s/clusters/%s/backupCheckpoints", groupID, clusterName) + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{ + "links":[ + { + "href":"https://cloud.mongodb.com/api/atlas/v1.0/groups/6b8cd3c380eef5349ef77gf7/clusters/Cluster0/backupCheckpoints?pageNum=1&itemsPerPage=100", + "rel":"self" + } + ], + "results":[ + { + "clusterId":"6b8cd61180eef547110159d9", + "completed":"2018-02-08T23:20:25Z", + "groupId":"6b8cd3c380eef5349ef77gf7", + "id":"5a7cdb3980eef53de5bffdcf", + "links":[ + { + "href":"https://cloud.mongodb.com/api/atlas/v1.0/groups/6b8cd3c380eef5349ef77gf7/clusters/Cluster0/backupCheckpoints", + "rel":"self" + } + ], + "parts":[ + { + "replicaSetName":"Cluster0-shard-1", + "shardName":"Cluster0-shard-1", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-08T23:20:25Z", + "increment":1 + }, + "typeName":"REPLICA_SET" + }, + { + "replicaSetName":"Cluster0-shard-0", + "shardName":"Cluster0-shard-0", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-08T23:20:25Z", + "increment":1 + }, + "typeName":"REPLICA_SET" + }, + { + "replicaSetName":"Cluster0-config-0", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-08T23:20:25Z", + "increment":2 + }, + "typeName":"CONFIG_SERVER_REPLICA_SET" + } + ], + "restorable":true, + "started":"2018-02-08T23:20:25Z", + "timestamp":"2018-02-08T23:19:37Z" + }, + { + "clusterId":"6b8cd61180eef547110159d9", + "completed":"2018-02-09T14:50:33Z", + "groupId":"6b8cd3c380eef5349ef77gf7", + "id":"5a7db53987d9d64fe298ff46", + "links":[ + { + "href":"https://cloud.mongodb.com/api/atlas/v1.0/groups/6b8cd3c380eef5349ef77gf7/clusters/Cluster0/backupCheckpoints?pretty=true", + "rel":"self" + } + ], + "parts":[ + { + "replicaSetName":"Cluster0-shard-1", + "shardName":"Cluster0-shard-1", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-09T14:50:33Z", + "increment":1 + }, + "typeName":"REPLICA_SET" + }, + { + "replicaSetName":"Cluster0-shard-0", + "shardName":"Cluster0-shard-0", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-09T14:50:33Z", + "increment":2 + }, + "typeName":"REPLICA_SET" + }, + { + "replicaSetName":"Cluster0-config-0", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-09T14:50:33Z", + "increment":4 + }, + "typeName":"CONFIG_SERVER_REPLICA_SET" + } + ], + "restorable":true, + "started":"2018-02-09T14:50:33Z", + "timestamp":"2018-02-09T14:50:18Z" + } + ], + "totalCount":2 + }`, + ) + }) + + snapshots, _, err := client.Checkpoints.List(ctx, groupID, clusterName, nil) + if err != nil { + t.Fatalf("Checkpoints.List returned error: %v", err) + } + + expected := &Checkpoints{ + Results: []*Checkpoint{ + { + ClusterID: "6b8cd61180eef547110159d9", + Completed: "2018-02-08T23:20:25Z", + GroupID: "6b8cd3c380eef5349ef77gf7", + ID: "5a7cdb3980eef53de5bffdcf", + Links: []*Link{ + { + Rel: "self", + Href: "https://cloud.mongodb.com/api/atlas/v1.0/groups/6b8cd3c380eef5349ef77gf7/clusters/Cluster0/backupCheckpoints", + }, + }, + Parts: []*Part{ + { + ReplicaSetName: "Cluster0-shard-1", + TypeName: "REPLICA_SET", + CheckpointPart: CheckpointPart{ + ShardName: "Cluster0-shard-1", + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-08T23:20:25Z", + Increment: 1, + }}, + }, + { + ReplicaSetName: "Cluster0-shard-0", + TypeName: "REPLICA_SET", + CheckpointPart: CheckpointPart{ + ShardName: "Cluster0-shard-0", + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-08T23:20:25Z", + Increment: 1, + }}, + }, + { + ReplicaSetName: "Cluster0-config-0", + TypeName: "CONFIG_SERVER_REPLICA_SET", + CheckpointPart: CheckpointPart{ + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-08T23:20:25Z", + Increment: 2, + }}, + }, + }, + Restorable: true, + Started: "2018-02-08T23:20:25Z", + Timestamp: "2018-02-08T23:19:37Z", + }, + { + ClusterID: "6b8cd61180eef547110159d9", + Completed: "2018-02-09T14:50:33Z", + GroupID: "6b8cd3c380eef5349ef77gf7", + ID: "5a7db53987d9d64fe298ff46", + Links: []*Link{ + { + Rel: "self", + Href: "https://cloud.mongodb.com/api/atlas/v1.0/groups/6b8cd3c380eef5349ef77gf7/clusters/Cluster0/backupCheckpoints?pretty=true", + }, + }, + Parts: []*Part{ + { + ReplicaSetName: "Cluster0-shard-1", + TypeName: "REPLICA_SET", + CheckpointPart: CheckpointPart{ + ShardName: "Cluster0-shard-1", + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-09T14:50:33Z", + Increment: 1, + }}, + }, + { + ReplicaSetName: "Cluster0-shard-0", + TypeName: "REPLICA_SET", + CheckpointPart: CheckpointPart{ + ShardName: "Cluster0-shard-0", + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-09T14:50:33Z", + Increment: 2, + }}, + }, + { + ReplicaSetName: "Cluster0-config-0", + TypeName: "CONFIG_SERVER_REPLICA_SET", + CheckpointPart: CheckpointPart{ + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-09T14:50:33Z", + Increment: 4, + }}, + }, + }, + Restorable: true, + Started: "2018-02-09T14:50:33Z", + Timestamp: "2018-02-09T14:50:18Z", + }, + }, + Links: []*Link{ + { + Href: "https://cloud.mongodb.com/api/atlas/v1.0/groups/6b8cd3c380eef5349ef77gf7/clusters/Cluster0/backupCheckpoints?pageNum=1&itemsPerPage=100", + Rel: "self", + }, + }, + TotalCount: 2, + } + + if diff := deep.Equal(snapshots, expected); diff != nil { + t.Error(diff) + } +} + +func TestCheckpoints_Get(t *testing.T) { + setup() + defer teardown() + + groupID := "6b8cd3c380eef5349ef77gf7" + clusterName := "Cluster0" + checkpointID := "6b8cd61180eef547110159d9" + path := fmt.Sprintf("/groups/%s/clusters/%s/backupCheckpoints/%s", groupID, clusterName, checkpointID) + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{ + "clusterId":"6b8cd61180eef547110159d9", + "completed":"2018-02-08T23:20:25Z", + "groupId":"6b8cd3c380eef5349ef77gf7", + "id":"5a7cdb3980eef53de5bffdcf", + "links":[ + { + "href":"https://cloud.mongodb.com/api/atlas/v1.0/groups/6b8cd3c380eef5349ef77gf7/clusters/Cluster0/backupCheckpoints", + "rel":"self" + } + ], + "parts":[ + { + "replicaSetName":"Cluster0-shard-1", + "shardName":"Cluster0-shard-1", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-08T23:20:25Z", + "increment":1 + }, + "typeName":"REPLICA_SET" + }, + { + "replicaSetName":"Cluster0-shard-0", + "shardName":"Cluster0-shard-0", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-08T23:20:25Z", + "increment":1 + }, + "typeName":"REPLICA_SET" + }, + { + "replicaSetName":"Cluster0-config-0", + "tokenDiscovered":true, + "tokenTimestamp":{ + "date":"2018-02-08T23:20:25Z", + "increment":2 + }, + "typeName":"CONFIG_SERVER_REPLICA_SET" + } + ], + "restorable":true, + "started":"2018-02-08T23:20:25Z", + "timestamp":"2018-02-08T23:19:37Z" + }`) + }) + + cloudProviderSnapshot, _, err := client.Checkpoints.Get(ctx, groupID, clusterName, checkpointID) + if err != nil { + t.Fatalf("ContinuousSnapshots.Get returned error: %v", err) + } + + expected := &Checkpoint{ + ClusterID: "6b8cd61180eef547110159d9", + Completed: "2018-02-08T23:20:25Z", + GroupID: "6b8cd3c380eef5349ef77gf7", + ID: "5a7cdb3980eef53de5bffdcf", + Links: []*Link{ + { + Rel: "self", + Href: "https://cloud.mongodb.com/api/atlas/v1.0/groups/6b8cd3c380eef5349ef77gf7/clusters/Cluster0/backupCheckpoints", + }, + }, + Parts: []*Part{ + + { + ReplicaSetName: "Cluster0-shard-1", + TypeName: "REPLICA_SET", + CheckpointPart: CheckpointPart{ + ShardName: "Cluster0-shard-1", + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-08T23:20:25Z", + Increment: 1, + }, + }, + }, + { + ReplicaSetName: "Cluster0-shard-0", + TypeName: "REPLICA_SET", + CheckpointPart: CheckpointPart{ + ShardName: "Cluster0-shard-0", + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-08T23:20:25Z", + Increment: 1, + }}, + }, + { + ReplicaSetName: "Cluster0-config-0", + TypeName: "CONFIG_SERVER_REPLICA_SET", + CheckpointPart: CheckpointPart{ + TokenDiscovered: true, + TokenTimestamp: SnapshotTimestamp{ + Date: "2018-02-08T23:20:25Z", + Increment: 2, + }}, + }, + }, + Restorable: true, + Started: "2018-02-08T23:20:25Z", + Timestamp: "2018-02-08T23:19:37Z", + } + + if diff := deep.Equal(cloudProviderSnapshot, expected); diff != nil { + t.Error(diff) + } +} diff --git a/mongodbatlas/continuous_snaphots.go b/mongodbatlas/continuous_snaphots.go index acb479200..e12b5ed96 100644 --- a/mongodbatlas/continuous_snaphots.go +++ b/mongodbatlas/continuous_snaphots.go @@ -46,6 +46,13 @@ type ContinuousSnapshot struct { } type Part struct { + ReplicaSetName string `json:"replicaSetName"` + TypeName string `json:"typeName"` + SnapshotPart + CheckpointPart +} + +type SnapshotPart struct { ClusterID string `json:"clusterId"` CompressionSetting string `json:"compressionSetting"` DataSizeBytes int64 `json:"dataSizeBytes"` @@ -53,9 +60,7 @@ type Part struct { FileSizeBytes int64 `json:"fileSizeBytes"` MasterKeyUUID string `json:"masterKeyUUID,omitempty"` MongodVersion string `json:"mongodVersion"` - ReplicaSetName string `json:"replicaSetName"` StorageSizeBytes int64 `json:"storageSizeBytes"` - TypeName string `json:"typeName"` } type NamespaceFilter struct { diff --git a/mongodbatlas/continuous_snapshots_test.go b/mongodbatlas/continuous_snapshots_test.go index 97de0c8f8..bbcc7099e 100644 --- a/mongodbatlas/continuous_snapshots_test.go +++ b/mongodbatlas/continuous_snapshots_test.go @@ -3,10 +3,11 @@ package mongodbatlas import ( "encoding/json" "fmt" - "github.com/mwielbut/pointy" "net/http" "testing" + "github.com/mwielbut/pointy" + "github.com/go-test/deep" ) @@ -97,15 +98,16 @@ func TestContinuousSnapshots_List(t *testing.T) { }, Parts: []*Part{ { - ClusterID: "7c2487d833e9e75286093696", - CompressionSetting: "GZIP", - DataSizeBytes: 4502, - EncryptionEnabled: false, - FileSizeBytes: 324760, - MongodVersion: "3.6.10", - ReplicaSetName: "Cluster0-shard-0", - StorageSizeBytes: 53248, - TypeName: "REPLICA_SET", + ReplicaSetName: "Cluster0-shard-0", + TypeName: "REPLICA_SET", + SnapshotPart: SnapshotPart{ + ClusterID: "7c2487d833e9e75286093696", + CompressionSetting: "GZIP", + DataSizeBytes: 4502, + EncryptionEnabled: false, + FileSizeBytes: 324760, + MongodVersion: "3.6.10", + StorageSizeBytes: 53248}, }, }, }, @@ -190,15 +192,16 @@ func TestContinuousSnapshots_Get(t *testing.T) { }, Parts: []*Part{ { - ClusterID: "7c2487d833e9e75286093696", - CompressionSetting: "GZIP", - DataSizeBytes: 4502, - EncryptionEnabled: false, - FileSizeBytes: 324760, - MongodVersion: "3.6.10", - ReplicaSetName: "Cluster0-shard-0", - StorageSizeBytes: 53248, - TypeName: "REPLICA_SET", + ReplicaSetName: "Cluster0-shard-0", + TypeName: "REPLICA_SET", + SnapshotPart: SnapshotPart{ + ClusterID: "7c2487d833e9e75286093696", + CompressionSetting: "GZIP", + DataSizeBytes: 4502, + EncryptionEnabled: false, + FileSizeBytes: 324760, + MongodVersion: "3.6.10", + StorageSizeBytes: 53248}, }, }, } @@ -302,16 +305,17 @@ func TestContinuousSnapshots_ChangeExpiry(t *testing.T) { }, Parts: []*Part{ { - ClusterID: "57c2487d833e9e75286093696", - CompressionSetting: "GZIP", - DataSizeBytes: 4502, - EncryptionEnabled: false, - FileSizeBytes: 324760, - MongodVersion: "3.6.10", - ReplicaSetName: "Cluster0-shard-0", - StorageSizeBytes: 53248, - TypeName: "REPLICA_SET", - }, + ReplicaSetName: "Cluster0-shard-0", + TypeName: "REPLICA_SET", + SnapshotPart: SnapshotPart{ + ClusterID: "57c2487d833e9e75286093696", + CompressionSetting: "GZIP", + DataSizeBytes: 4502, + EncryptionEnabled: false, + FileSizeBytes: 324760, + MongodVersion: "3.6.10", + StorageSizeBytes: 53248, + }}, }, } diff --git a/mongodbatlas/mongodbatlas.go b/mongodbatlas/mongodbatlas.go index 1f090c9ce..91a332bee 100644 --- a/mongodbatlas/mongodbatlas.go +++ b/mongodbatlas/mongodbatlas.go @@ -62,6 +62,7 @@ type Client struct { X509AuthDBUsers X509AuthDBUsersService ContinuousSnapshots ContinuousSnapshotsService ContinuousRestoreJobs ContinuousRestoreJobsService + Checkpoints CheckpointsService onRequestCompleted RequestCompletionCallback } @@ -176,7 +177,9 @@ func NewClient(httpClient *http.Client) *Client { c.PrivateEndpoints = &PrivateEndpointsServiceOp{Client: c} c.X509AuthDBUsers = &X509AuthDBUsersServiceOp{Client: c} c.ContinuousRestoreJobs = &ContinuousRestoreJobsServiceOp{Client: c} + c.Checkpoints = &CheckpointsServiceOp{Client: c} c.Alerts = &AlertsServiceOp{Client: c} + return c }