-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Onboard PostgreSQL Flex instance clone command (#150)
* fix typo in error message * add bytesize package * onboard clone instance command * onboard backups list command * onboard backups describe command * onboard backups update-schedule command * add docs * edit example and flag descriptions // add date validation for recovery date * add storage validation // add unit tests for build request * revert backups changes * change string formatting for recovery timestamp * remove bytesize * edit recovery timestamp flag description * extend storage validation for request * use variable in format Co-authored-by: João Palet <[email protected]> --------- Co-authored-by: João Palet <[email protected]>
- Loading branch information
Showing
6 changed files
with
777 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
## stackit postgresflex instance clone | ||
|
||
Clones a PostgreSQL Flex instance | ||
|
||
### Synopsis | ||
|
||
Clones a PostgreSQL Flex instance from a selected point in time. The new cloned instance will be an independent instance with the same settings as the original instance unless the flags are specified. | ||
|
||
``` | ||
stackit postgresflex instance clone INSTANCE_ID [flags] | ||
``` | ||
|
||
### Examples | ||
|
||
``` | ||
Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp. | ||
$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 | ||
Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage class. | ||
$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-class premium-perf6-stackit | ||
Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage size. | ||
$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-size 10 | ||
``` | ||
|
||
### Options | ||
|
||
``` | ||
-h, --help Help for "stackit postgresflex instance clone" | ||
--recovery-timestamp string Recovery timestamp for the instance, specified in UTC time following the format, e.g. 2024-03-12T09:28:00+00:00 | ||
--storage-class string Storage class. If not specified, storage class from the existing instance will be used. | ||
--storage-size int Storage size (in GB). If not specified, storage size from the existing instance will be used. | ||
``` | ||
|
||
### Options inherited from parent commands | ||
|
||
``` | ||
-y, --assume-yes If set, skips all confirmation prompts | ||
--async If set, runs the command asynchronously | ||
-o, --output-format string Output format, one of ["json" "pretty"] | ||
-p, --project-id string Project ID | ||
``` | ||
|
||
### SEE ALSO | ||
|
||
* [stackit postgresflex instance](./stackit_postgresflex_instance.md) - Provides functionality for PostgreSQL Flex instances | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
package clone | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
"github.com/stackitcloud/stackit-cli/internal/pkg/confirm" | ||
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
"github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
"github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
"github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" | ||
postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" | ||
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
"github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/stackitcloud/stackit-sdk-go/services/postgresflex" | ||
"github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" | ||
) | ||
|
||
const ( | ||
instanceIdArg = "INSTANCE_ID" | ||
|
||
storageClassFlag = "storage-class" | ||
storageSizeFlag = "storage-size" | ||
recoveryTimestampFlag = "recovery-timestamp" | ||
recoveryDateFormat = time.RFC3339 | ||
) | ||
|
||
type inputModel struct { | ||
*globalflags.GlobalFlagModel | ||
|
||
InstanceId string | ||
StorageClass *string | ||
StorageSize *int64 | ||
RecoveryDate *string | ||
} | ||
|
||
func NewCmd() *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: fmt.Sprintf("clone %s", instanceIdArg), | ||
Short: "Clones a PostgreSQL Flex instance", | ||
Long: "Clones a PostgreSQL Flex instance from a selected point in time. " + | ||
"The new cloned instance will be an independent instance with the same settings as the original instance unless the flags are specified.", | ||
Example: examples.Build( | ||
examples.NewExample( | ||
`Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp.`, | ||
`$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00`), | ||
examples.NewExample( | ||
`Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage class.`, | ||
`$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-class premium-perf6-stackit`), | ||
examples.NewExample( | ||
`Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage size.`, | ||
`$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-size 10`), | ||
), | ||
Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
ctx := context.Background() | ||
|
||
model, err := parseInput(cmd, args) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Configure API client | ||
apiClient, err := client.ConfigureClient(cmd) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) | ||
if err != nil { | ||
instanceLabel = model.InstanceId | ||
} | ||
|
||
if !model.AssumeYes { | ||
prompt := fmt.Sprintf("Are you sure you want to clone instance %q?", instanceLabel) | ||
err = confirm.PromptForConfirmation(cmd, prompt) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
// Call API | ||
req, err := buildRequest(ctx, model, apiClient) | ||
if err != nil { | ||
return err | ||
} | ||
resp, err := req.Execute() | ||
if err != nil { | ||
return fmt.Errorf("clone PostgreSQL Flex instance: %w", err) | ||
} | ||
instanceId := *resp.InstanceId | ||
|
||
// Wait for async operation, if async mode not enabled | ||
if !model.Async { | ||
s := spinner.New(cmd) | ||
s.Start("Cloning instance") | ||
_, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) | ||
if err != nil { | ||
return fmt.Errorf("wait for PostgreSQL Flex instance cloning: %w", err) | ||
} | ||
s.Stop() | ||
} | ||
|
||
operationState := "Cloned" | ||
if model.Async { | ||
operationState = "Triggered cloning of" | ||
} | ||
|
||
cmd.Printf("%s instance from instance %q. New Instance ID: %s\n", operationState, instanceLabel, instanceId) | ||
return nil | ||
}, | ||
} | ||
configureFlags(cmd) | ||
return cmd | ||
} | ||
|
||
func configureFlags(cmd *cobra.Command) { | ||
cmd.Flags().String(recoveryTimestampFlag, "", "Recovery timestamp for the instance, in a date-time with the RFC3339 layout format, e.g. 2024-01-01T00:00:00Z") | ||
cmd.Flags().String(storageClassFlag, "", "Storage class. If not specified, storage class from the existing instance will be used.") | ||
cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB). If not specified, storage size from the existing instance will be used.") | ||
|
||
err := flags.MarkFlagsRequired(cmd, recoveryTimestampFlag) | ||
cobra.CheckErr(err) | ||
} | ||
|
||
func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { | ||
instanceId := inputArgs[0] | ||
|
||
globalFlags := globalflags.Parse(cmd) | ||
if globalFlags.ProjectId == "" { | ||
return nil, &cliErr.ProjectIdError{} | ||
} | ||
|
||
recoveryTimestamp, err := flags.FlagToDateTimePointer(cmd, recoveryTimestampFlag, recoveryDateFormat) | ||
if err != nil { | ||
return nil, &cliErr.FlagValidationError{ | ||
Flag: recoveryTimestampFlag, | ||
Details: err.Error(), | ||
} | ||
} | ||
recoveryTimestampString := recoveryTimestamp.Format(recoveryDateFormat) | ||
|
||
return &inputModel{ | ||
GlobalFlagModel: globalFlags, | ||
InstanceId: instanceId, | ||
StorageClass: flags.FlagToStringPointer(cmd, storageClassFlag), | ||
StorageSize: flags.FlagToInt64Pointer(cmd, storageSizeFlag), | ||
RecoveryDate: utils.Ptr(recoveryTimestampString), | ||
}, nil | ||
} | ||
|
||
type PostgreSQLFlexClient interface { | ||
CloneInstance(ctx context.Context, projectId, instanceId string) postgresflex.ApiCloneInstanceRequest | ||
GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*postgresflex.InstanceResponse, error) | ||
ListStoragesExecute(ctx context.Context, projectId, flavorId string) (*postgresflex.ListStoragesResponse, error) | ||
} | ||
|
||
func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFlexClient) (postgresflex.ApiCloneInstanceRequest, error) { | ||
req := apiClient.CloneInstance(ctx, model.ProjectId, model.InstanceId) | ||
|
||
var storages *postgresflex.ListStoragesResponse | ||
if model.StorageClass != nil || model.StorageSize != nil { | ||
currentInstance, err := apiClient.GetInstanceExecute(ctx, model.ProjectId, model.InstanceId) | ||
if err != nil { | ||
return req, fmt.Errorf("get PostgreSQL Flex instance: %w", err) | ||
} | ||
validationFlavorId := currentInstance.Item.Flavor.Id | ||
currentInstanceStorageClass := currentInstance.Item.Storage.Class | ||
currentInstanceStorageSize := currentInstance.Item.Storage.Size | ||
|
||
storages, err = apiClient.ListStoragesExecute(ctx, model.ProjectId, *validationFlavorId) | ||
if err != nil { | ||
return req, fmt.Errorf("get PostgreSQL Flex storages: %w", err) | ||
} | ||
|
||
if model.StorageClass == nil { | ||
err = postgresflexUtils.ValidateStorage(currentInstanceStorageClass, model.StorageSize, storages, *validationFlavorId) | ||
} else if model.StorageSize == nil { | ||
err = postgresflexUtils.ValidateStorage(model.StorageClass, currentInstanceStorageSize, storages, *validationFlavorId) | ||
} else { | ||
err = postgresflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, *validationFlavorId) | ||
} | ||
if err != nil { | ||
return req, err | ||
} | ||
} | ||
|
||
req = req.CloneInstancePayload(postgresflex.CloneInstancePayload{ | ||
Class: model.StorageClass, | ||
Size: model.StorageSize, | ||
Timestamp: model.RecoveryDate, | ||
}) | ||
return req, nil | ||
} |
Oops, something went wrong.