diff --git a/internal/cli/atlas_backups_restores.go b/internal/cli/atlas_backups_restores.go index 6ac043ec3e..5a4e5b011b 100644 --- a/internal/cli/atlas_backups_restores.go +++ b/internal/cli/atlas_backups_restores.go @@ -26,6 +26,7 @@ func AtlasBackupsRestoresBuilder() *cobra.Command { } cmd.AddCommand(AtlasBackupsRestoresListBuilder()) + cmd.AddCommand(AtlasBackupsRestoresStartBuilder()) return cmd } diff --git a/internal/cli/atlas_backups_restores_start.go b/internal/cli/atlas_backups_restores_start.go new file mode 100644 index 0000000000..102ffc3fca --- /dev/null +++ b/internal/cli/atlas_backups_restores_start.go @@ -0,0 +1,244 @@ +// Copyright 2020 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "errors" + "fmt" + + atlas "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas" + "github.com/mongodb/mongocli/internal/config" + "github.com/mongodb/mongocli/internal/flags" + "github.com/mongodb/mongocli/internal/json" + "github.com/mongodb/mongocli/internal/store" + "github.com/mongodb/mongocli/internal/usage" + "github.com/spf13/cobra" +) + +const ( + automatedRestore = "AUTOMATED_RESTORE" + httpRestore = "HTTP" + onlyFor = "'%s' can only be used with %s" +) + +type atlasBackupsRestoresStartOpts struct { + *globalOpts + method string + clusterName string + clusterID string + targetProjectID string + targetClusterID string + targetClusterName string + checkpointID string + oplogTs string + oplogInc int64 + snapshotID string + expirationHours int64 + expires string + maxDownloads int64 + pointInTimeUTCMillis float64 + store store.ContinuousJobCreator +} + +func (opts *atlasBackupsRestoresStartOpts) init() error { + if opts.ProjectID() == "" { + return errMissingProjectID + } + + var err error + opts.store, err = store.New() + return err +} + +func (opts *atlasBackupsRestoresStartOpts) Run() error { + request := opts.newContinuousJobRequest() + + result, err := opts.store.CreateContinuousRestoreJob(opts.ProjectID(), opts.fromCluster(), request) + + if err != nil { + return err + } + + return json.PrettyPrint(result) +} + +func (opts *atlasBackupsRestoresStartOpts) newContinuousJobRequest() *atlas.ContinuousJobRequest { + request := new(atlas.ContinuousJobRequest) + request.Delivery.MethodName = opts.method + request.SnapshotID = opts.snapshotID + + if opts.isAutomatedRestore() { + request.Delivery.TargetGroupID = opts.targetProjectID + opts.setTargetCluster(request) + + if opts.oplogTs != "" && opts.oplogInc != 0 { + request.OplogTs = opts.oplogTs + request.OplogInc = opts.oplogInc + } + if opts.pointInTimeUTCMillis != 0 { + request.PointInTimeUTCMillis = opts.pointInTimeUTCMillis + } + } + + if opts.isHTTP() { + if opts.expires != "" { + request.Delivery.Expires = opts.expires + } + if opts.maxDownloads > 0 { + request.Delivery.MaxDownloads = opts.maxDownloads + } + if opts.expirationHours > 0 { + request.Delivery.ExpirationHours = opts.expirationHours + } + } + return request +} + +func (opts *atlasBackupsRestoresStartOpts) fromCluster() string { + if opts.clusterName != "" { + return opts.clusterName + } + return opts.clusterID +} + +func (opts *atlasBackupsRestoresStartOpts) setTargetCluster(out *atlas.ContinuousJobRequest) { + if opts.targetClusterID != "" { + out.Delivery.TargetClusterID = opts.targetClusterID + } else if opts.targetClusterName != "" { + out.Delivery.TargetClusterName = opts.targetClusterName + } +} + +func (opts *atlasBackupsRestoresStartOpts) isAutomatedRestore() bool { + return opts.method == automatedRestore +} + +func (opts *atlasBackupsRestoresStartOpts) isHTTP() bool { + return opts.method == httpRestore +} + +func (opts *atlasBackupsRestoresStartOpts) validateParams() error { + if (opts.clusterName == "" && opts.clusterID == "") || (opts.clusterName != "" && opts.clusterID != "") { + return errors.New("needs clusterName or clusterId") + } + + if !opts.isAutomatedRestore() { + if e := opts.automatedRestoreOnlyFlags(); e != nil { + return e + } + } + + if !opts.isHTTP() { + if e := opts.httpRestoreOnlyFlags(); e != nil { + return e + } + } + + return nil +} + +func (opts *atlasBackupsRestoresStartOpts) httpRestoreOnlyFlags() error { + if opts.expires != "" { + return fmt.Errorf(onlyFor, flags.Expires, httpRestore) + } + if opts.maxDownloads > 0 { + return fmt.Errorf(onlyFor, flags.MaxDownloads, httpRestore) + } + if opts.expirationHours > 0 { + return fmt.Errorf(onlyFor, flags.ExpirationHours, httpRestore) + } + return nil +} + +func (opts *atlasBackupsRestoresStartOpts) automatedRestoreOnlyFlags() error { + if opts.checkpointID != "" { + return fmt.Errorf(onlyFor, flags.CheckpointID, automatedRestore) + } + if opts.oplogTs != "" { + return fmt.Errorf(onlyFor, flags.OplogTs, automatedRestore) + } + if opts.oplogInc > 0 { + return fmt.Errorf(onlyFor, flags.OplogInc, automatedRestore) + } + if opts.pointInTimeUTCMillis > 0 { + return fmt.Errorf(onlyFor, flags.PointInTimeUTCMillis, automatedRestore) + } + return nil +} + +func markRequiredAutomatedRestoreFlags(cmd *cobra.Command) error { + if err := cmd.MarkFlagRequired(flags.TargetProjectID); err != nil { + return err + } + + if config.Service() == config.CloudService { + return cmd.MarkFlagRequired(flags.ClusterName) + } + return cmd.MarkFlagRequired(flags.ClusterID) +} + +// mongocli atlas backup(s) restore(s) job(s) start +func AtlasBackupsRestoresStartBuilder() *cobra.Command { + opts := &atlasBackupsRestoresStartOpts{ + globalOpts: newGlobalOpts(), + } + cmd := &cobra.Command{ + Use: "start", + Short: "Start a restore job.", + Args: cobra.ExactValidArgs(1), + ValidArgs: []string{automatedRestore, httpRestore}, + PreRunE: func(cmd *cobra.Command, args []string) error { + if opts.isAutomatedRestore() { + if err := markRequiredAutomatedRestoreFlags(cmd); err != nil { + return err + } + } + return opts.init() + }, + RunE: func(cmd *cobra.Command, args []string) error { + opts.method = args[0] + + if e := opts.validateParams(); e != nil { + return e + } + + return opts.Run() + }, + } + + cmd.Flags().StringVar(&opts.snapshotID, flags.SnapshotID, "", usage.SnapshotID) + // Atlas uses cluster name + cmd.Flags().StringVar(&opts.clusterName, flags.ClusterName, "", usage.ClusterName) + // C/OM uses cluster ID + cmd.Flags().StringVar(&opts.clusterID, flags.ClusterID, "", usage.ClusterID) + + // For Automatic restore + cmd.Flags().StringVar(&opts.targetProjectID, flags.TargetProjectID, "", usage.TargetProjectID) + cmd.Flags().StringVar(&opts.targetClusterID, flags.TargetClusterID, "", usage.TargetClusterID) + cmd.Flags().StringVar(&opts.targetClusterName, flags.TargetClusterName, "", usage.TargetClusterName) + cmd.Flags().StringVar(&opts.checkpointID, flags.CheckpointID, "", usage.CheckpointID) + cmd.Flags().StringVar(&opts.oplogTs, flags.OplogTs, "", usage.OplogTs) + cmd.Flags().Int64Var(&opts.oplogInc, flags.OplogInc, 0, usage.OplogInc) + cmd.Flags().Float64Var(&opts.pointInTimeUTCMillis, flags.PointInTimeUTCMillis, 0, usage.PointInTimeUTCMillis) + + // For http restore + cmd.Flags().StringVar(&opts.expires, flags.Expires, "", usage.Expires) + cmd.Flags().Int64Var(&opts.maxDownloads, flags.MaxDownloads, 0, usage.MaxDownloads) + cmd.Flags().Int64Var(&opts.expirationHours, flags.ExpirationHours, 0, usage.ExpirationHours) + + cmd.Flags().StringVar(&opts.projectID, flags.ProjectID, "", usage.ProjectID) + + return cmd +} diff --git a/internal/cli/atlas_backups_restores_start_test.go b/internal/cli/atlas_backups_restores_start_test.go new file mode 100644 index 0000000000..75e16a9229 --- /dev/null +++ b/internal/cli/atlas_backups_restores_start_test.go @@ -0,0 +1,72 @@ +// Copyright 2020 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/mongodb/mongocli/internal/fixtures" + "github.com/mongodb/mongocli/internal/mocks" +) + +func TestAtlasBackupsRestoresStart_Run(t *testing.T) { + ctrl := gomock.NewController(t) + mockStore := mocks.NewMockContinuousJobCreator(ctrl) + + defer ctrl.Finish() + + expected := fixtures.ContinuousJob() + + t.Run(automatedRestore, func(t *testing.T) { + listOpts := &atlasBackupsRestoresStartOpts{ + globalOpts: newGlobalOpts(), + store: mockStore, + method: automatedRestore, + clusterName: "Cluster0", + } + + mockStore. + EXPECT(). + CreateContinuousRestoreJob(listOpts.projectID, "Cluster0", listOpts.newContinuousJobRequest()). + Return(expected, nil). + Times(1) + + err := listOpts.Run() + if err != nil { + t.Fatalf("Run() unexpected error: %v", err) + } + }) + + t.Run(httpRestore, func(t *testing.T) { + listOpts := &atlasBackupsRestoresStartOpts{ + globalOpts: newGlobalOpts(), + store: mockStore, + method: httpRestore, + clusterName: "Cluster0", + } + + mockStore. + EXPECT(). + CreateContinuousRestoreJob(listOpts.projectID, "Cluster0", listOpts.newContinuousJobRequest()). + Return(expected, nil). + Times(1) + + err := listOpts.Run() + if err != nil { + t.Fatalf("Run() unexpected error: %v", err) + } + }) +} diff --git a/internal/fixtures/continuous_jobs.go b/internal/fixtures/continuous_jobs.go index 63fbfbc865..556df21cc2 100644 --- a/internal/fixtures/continuous_jobs.go +++ b/internal/fixtures/continuous_jobs.go @@ -1,25 +1,24 @@ +// Copyright 2020 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package fixtures import atlas "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas" -func AutomatedContinuousJob() *atlas.ContinuousJob { - return &atlas.ContinuousJob{ - BatchID: "", - ClusterID: "", - Created: "", - ClusterName: "", - Delivery: nil, - EncryptionEnabled: false, - GroupID: "", - Hashes: nil, - ID: "", - Links: nil, - MasterKeyUUID: "", - SnapshotID: "", - StatusName: "", - PointInTime: nil, - Timestamp: atlas.SnapshotTimestamp{}, - } +// TODO: https://github.com/mongodb/go-client-mongodb-atlas/pull/64 +func ContinuousJob() *atlas.ContinuousJob { + return &atlas.ContinuousJob{} } func ContinuousJobs() *atlas.ContinuousJobs { diff --git a/internal/fixtures/continuous_snapshots.go b/internal/fixtures/continuous_snapshots.go index eb42710c1f..9a6f30d7a3 100644 --- a/internal/fixtures/continuous_snapshots.go +++ b/internal/fixtures/continuous_snapshots.go @@ -1,3 +1,17 @@ +// Copyright 2020 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package fixtures import atlas "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas" diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 197e01ca0e..5c44b74ece 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -70,4 +70,17 @@ const ( NotificationType = "notificationType" // NotificationType flag NotificationUsername = "notificationUsername" // NotificationUsername flag NotificationVictorOpsRoutingKey = "notificationVictorOpsRoutingKey" // NotificationVictorOpsRoutingKey flag + SnapshotID = "snapshotId" // SnapshotID flag + ClusterName = "clusterName" // ClusterName flag + ClusterID = "clusterId" // ClusterID flag + TargetProjectID = "targetProjectId" // TargetProjectID flag + TargetClusterID = "targetClusterId" // TargetClusterID flag + TargetClusterName = "targetClusterName" // TargetClusterName flag + CheckpointID = "checkpointId" // CheckpointID flag + OplogTs = "oplogTs" // OplogTs flag + OplogInc = "oplogInc" // OplogInc flag + PointInTimeUTCMillis = "pointInTimeUTCMillis" // PointInTimeUTCMillis flag + Expires = "expires" // Expires flag + MaxDownloads = "maxDownloads" // MaxDownloads flag + ExpirationHours = "expirationHours" // ExpirationHours ) diff --git a/internal/usage/usage.go b/internal/usage/usage.go index caa0e63df8..b0f2d2983d 100644 --- a/internal/usage/usage.go +++ b/internal/usage/usage.go @@ -62,7 +62,29 @@ const ( NotificationType = "Type of alert notification." NotificationUsername = "Name of the Atlas user to which to send notifications." NotificationVictorOpsRoutingKey = "VictorOps routing key." - WhitelistType = `Type of whitelist entry. + SnapshotID = "Unique identifier of the snapshot to restore." + ClusterName = "Name of the cluster that contains the snapshots that you want to retrieve." + ClusterID = "Unique identifier of the cluster that the job represents." + TargetProjectID = "Unique identifier of the project that contains the destination cluster for the restore job." + TargetClusterID = `Unique identifier of the target cluster. +For use only with automated restore jobs.` + TargetClusterName = `Name of the target cluster. +For use only with automated restore jobs.` + CheckpointID = `Unique identifier for the sharded cluster checkpoint that represents the point in time to which your data will be restored. +If you set checkpointId, you cannot set oplogInc, oplogTs, snapshotId, or pointInTimeUTCMillis.` + OplogTs = `Oplog timestamp given as a timestamp in the number of seconds that have elapsed since the UNIX epoch. +When paired with oplogInc, they represent the point in time to which your data will be restored.` + OplogInc = `32-bit incrementing ordinal that represents operations within a given second. +When paired with oplogTs, they represent the point in time to which your data will be restored.` + PointInTimeUTCMillis = `Timestamp in the number of milliseconds that have elapsed since the UNIX epoch that represents the point in time to which your data will be restored. +This timestamp must be within last 24 hours of the current time.` + Expires = `Timestamp in ISO 8601 date and time format after which the URL is no longer available. +For use only with download restore jobs.` + ExpirationHours = `Number of hours the download URL is valid once the restore job is complete. +For use only with download restore jobs.` + MaxDownloads = `Number of times the download URL can be used. This must be 1 or greater. +For use only with download restore jobs.` + WhitelistType = `Type of whitelist entry. Valid values: cidrBlock|ipAddress` Service = `Type of MongoDB service. Valid values: cloud|cloud-manager|ops-manager`