Skip to content

Commit 8e02522

Browse files
GokceGKjoaopalet
andauthored
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 <joao.palet@outlook.com> --------- Co-authored-by: João Palet <joao.palet@outlook.com>
1 parent 3b90267 commit 8e02522

File tree

6 files changed

+777
-1
lines changed

6 files changed

+777
-1
lines changed

docs/stackit_postgresflex_instance.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ stackit postgresflex instance [flags]
2828
### SEE ALSO
2929

3030
* [stackit postgresflex](./stackit_postgresflex.md) - Provides functionality for PostgreSQL Flex
31+
* [stackit postgresflex instance clone](./stackit_postgresflex_instance_clone.md) - Clones a PostgreSQL Flex instance
3132
* [stackit postgresflex instance create](./stackit_postgresflex_instance_create.md) - Creates a PostgreSQL Flex instance
3233
* [stackit postgresflex instance delete](./stackit_postgresflex_instance_delete.md) - Deletes a PostgreSQL Flex instance
3334
* [stackit postgresflex instance describe](./stackit_postgresflex_instance_describe.md) - Shows details of a PostgreSQL Flex instance
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## stackit postgresflex instance clone
2+
3+
Clones a PostgreSQL Flex instance
4+
5+
### Synopsis
6+
7+
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.
8+
9+
```
10+
stackit postgresflex instance clone INSTANCE_ID [flags]
11+
```
12+
13+
### Examples
14+
15+
```
16+
Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp.
17+
$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00
18+
19+
Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage class.
20+
$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-class premium-perf6-stackit
21+
22+
Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage size.
23+
$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-size 10
24+
```
25+
26+
### Options
27+
28+
```
29+
-h, --help Help for "stackit postgresflex instance clone"
30+
--recovery-timestamp string Recovery timestamp for the instance, specified in UTC time following the format, e.g. 2024-03-12T09:28:00+00:00
31+
--storage-class string Storage class. If not specified, storage class from the existing instance will be used.
32+
--storage-size int Storage size (in GB). If not specified, storage size from the existing instance will be used.
33+
```
34+
35+
### Options inherited from parent commands
36+
37+
```
38+
-y, --assume-yes If set, skips all confirmation prompts
39+
--async If set, runs the command asynchronously
40+
-o, --output-format string Output format, one of ["json" "pretty"]
41+
-p, --project-id string Project ID
42+
```
43+
44+
### SEE ALSO
45+
46+
* [stackit postgresflex instance](./stackit_postgresflex_instance.md) - Provides functionality for PostgreSQL Flex instances
47+
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package clone
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/confirm"
10+
cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client"
15+
postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
18+
19+
"github.com/spf13/cobra"
20+
"github.com/stackitcloud/stackit-sdk-go/services/postgresflex"
21+
"github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait"
22+
)
23+
24+
const (
25+
instanceIdArg = "INSTANCE_ID"
26+
27+
storageClassFlag = "storage-class"
28+
storageSizeFlag = "storage-size"
29+
recoveryTimestampFlag = "recovery-timestamp"
30+
recoveryDateFormat = time.RFC3339
31+
)
32+
33+
type inputModel struct {
34+
*globalflags.GlobalFlagModel
35+
36+
InstanceId string
37+
StorageClass *string
38+
StorageSize *int64
39+
RecoveryDate *string
40+
}
41+
42+
func NewCmd() *cobra.Command {
43+
cmd := &cobra.Command{
44+
Use: fmt.Sprintf("clone %s", instanceIdArg),
45+
Short: "Clones a PostgreSQL Flex instance",
46+
Long: "Clones a PostgreSQL Flex instance from a selected point in time. " +
47+
"The new cloned instance will be an independent instance with the same settings as the original instance unless the flags are specified.",
48+
Example: examples.Build(
49+
examples.NewExample(
50+
`Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp.`,
51+
`$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00`),
52+
examples.NewExample(
53+
`Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage class.`,
54+
`$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-class premium-perf6-stackit`),
55+
examples.NewExample(
56+
`Clone a PostgreSQL Flex instance with ID "xxx" from a selected recovery timestamp and specify storage size.`,
57+
`$ stackit postgresflex instance clone xxx --recovery-timestamp 2023-04-17T09:28:00+00:00 --storage-size 10`),
58+
),
59+
Args: args.SingleArg(instanceIdArg, utils.ValidateUUID),
60+
RunE: func(cmd *cobra.Command, args []string) error {
61+
ctx := context.Background()
62+
63+
model, err := parseInput(cmd, args)
64+
if err != nil {
65+
return err
66+
}
67+
68+
// Configure API client
69+
apiClient, err := client.ConfigureClient(cmd)
70+
if err != nil {
71+
return err
72+
}
73+
74+
instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId)
75+
if err != nil {
76+
instanceLabel = model.InstanceId
77+
}
78+
79+
if !model.AssumeYes {
80+
prompt := fmt.Sprintf("Are you sure you want to clone instance %q?", instanceLabel)
81+
err = confirm.PromptForConfirmation(cmd, prompt)
82+
if err != nil {
83+
return err
84+
}
85+
}
86+
87+
// Call API
88+
req, err := buildRequest(ctx, model, apiClient)
89+
if err != nil {
90+
return err
91+
}
92+
resp, err := req.Execute()
93+
if err != nil {
94+
return fmt.Errorf("clone PostgreSQL Flex instance: %w", err)
95+
}
96+
instanceId := *resp.InstanceId
97+
98+
// Wait for async operation, if async mode not enabled
99+
if !model.Async {
100+
s := spinner.New(cmd)
101+
s.Start("Cloning instance")
102+
_, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx)
103+
if err != nil {
104+
return fmt.Errorf("wait for PostgreSQL Flex instance cloning: %w", err)
105+
}
106+
s.Stop()
107+
}
108+
109+
operationState := "Cloned"
110+
if model.Async {
111+
operationState = "Triggered cloning of"
112+
}
113+
114+
cmd.Printf("%s instance from instance %q. New Instance ID: %s\n", operationState, instanceLabel, instanceId)
115+
return nil
116+
},
117+
}
118+
configureFlags(cmd)
119+
return cmd
120+
}
121+
122+
func configureFlags(cmd *cobra.Command) {
123+
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")
124+
cmd.Flags().String(storageClassFlag, "", "Storage class. If not specified, storage class from the existing instance will be used.")
125+
cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB). If not specified, storage size from the existing instance will be used.")
126+
127+
err := flags.MarkFlagsRequired(cmd, recoveryTimestampFlag)
128+
cobra.CheckErr(err)
129+
}
130+
131+
func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
132+
instanceId := inputArgs[0]
133+
134+
globalFlags := globalflags.Parse(cmd)
135+
if globalFlags.ProjectId == "" {
136+
return nil, &cliErr.ProjectIdError{}
137+
}
138+
139+
recoveryTimestamp, err := flags.FlagToDateTimePointer(cmd, recoveryTimestampFlag, recoveryDateFormat)
140+
if err != nil {
141+
return nil, &cliErr.FlagValidationError{
142+
Flag: recoveryTimestampFlag,
143+
Details: err.Error(),
144+
}
145+
}
146+
recoveryTimestampString := recoveryTimestamp.Format(recoveryDateFormat)
147+
148+
return &inputModel{
149+
GlobalFlagModel: globalFlags,
150+
InstanceId: instanceId,
151+
StorageClass: flags.FlagToStringPointer(cmd, storageClassFlag),
152+
StorageSize: flags.FlagToInt64Pointer(cmd, storageSizeFlag),
153+
RecoveryDate: utils.Ptr(recoveryTimestampString),
154+
}, nil
155+
}
156+
157+
type PostgreSQLFlexClient interface {
158+
CloneInstance(ctx context.Context, projectId, instanceId string) postgresflex.ApiCloneInstanceRequest
159+
GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*postgresflex.InstanceResponse, error)
160+
ListStoragesExecute(ctx context.Context, projectId, flavorId string) (*postgresflex.ListStoragesResponse, error)
161+
}
162+
163+
func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFlexClient) (postgresflex.ApiCloneInstanceRequest, error) {
164+
req := apiClient.CloneInstance(ctx, model.ProjectId, model.InstanceId)
165+
166+
var storages *postgresflex.ListStoragesResponse
167+
if model.StorageClass != nil || model.StorageSize != nil {
168+
currentInstance, err := apiClient.GetInstanceExecute(ctx, model.ProjectId, model.InstanceId)
169+
if err != nil {
170+
return req, fmt.Errorf("get PostgreSQL Flex instance: %w", err)
171+
}
172+
validationFlavorId := currentInstance.Item.Flavor.Id
173+
currentInstanceStorageClass := currentInstance.Item.Storage.Class
174+
currentInstanceStorageSize := currentInstance.Item.Storage.Size
175+
176+
storages, err = apiClient.ListStoragesExecute(ctx, model.ProjectId, *validationFlavorId)
177+
if err != nil {
178+
return req, fmt.Errorf("get PostgreSQL Flex storages: %w", err)
179+
}
180+
181+
if model.StorageClass == nil {
182+
err = postgresflexUtils.ValidateStorage(currentInstanceStorageClass, model.StorageSize, storages, *validationFlavorId)
183+
} else if model.StorageSize == nil {
184+
err = postgresflexUtils.ValidateStorage(model.StorageClass, currentInstanceStorageSize, storages, *validationFlavorId)
185+
} else {
186+
err = postgresflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, *validationFlavorId)
187+
}
188+
if err != nil {
189+
return req, err
190+
}
191+
}
192+
193+
req = req.CloneInstancePayload(postgresflex.CloneInstancePayload{
194+
Class: model.StorageClass,
195+
Size: model.StorageSize,
196+
Timestamp: model.RecoveryDate,
197+
})
198+
return req, nil
199+
}

0 commit comments

Comments
 (0)