Skip to content

Commit b556241

Browse files
committed
add volume backup create
1 parent c05dbcf commit b556241

File tree

3 files changed

+250
-0
lines changed

3 files changed

+250
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package backup
2+
3+
import (
4+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
5+
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/create"
6+
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/delete"
7+
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/describe"
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/list"
9+
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/restore"
10+
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/update"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
13+
14+
"github.com/spf13/cobra"
15+
)
16+
17+
func NewCmd(params *params.CmdParams) *cobra.Command {
18+
cmd := &cobra.Command{
19+
Use: "backup",
20+
Short: "Provides functionality for volume backups",
21+
Long: "Provides functionality for volume backups.",
22+
Args: args.NoArgs,
23+
Run: utils.CmdHelp,
24+
}
25+
addSubcommands(cmd, params)
26+
return cmd
27+
}
28+
29+
func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
30+
cmd.AddCommand(create.NewCmd(params))
31+
cmd.AddCommand(list.NewCmd(params))
32+
cmd.AddCommand(update.NewCmd(params))
33+
cmd.AddCommand(delete.NewCmd(params))
34+
cmd.AddCommand(describe.NewCmd(params))
35+
cmd.AddCommand(restore.NewCmd(params))
36+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package create
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/goccy/go-yaml"
9+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
18+
19+
"github.com/spf13/cobra"
20+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
21+
)
22+
23+
const (
24+
sourceIdFlag = "source-id"
25+
sourceTypeFlag = "source-type"
26+
nameFlag = "name"
27+
labelsFlag = "labels"
28+
)
29+
30+
type inputModel struct {
31+
*globalflags.GlobalFlagModel
32+
SourceID string
33+
SourceType string
34+
Name *string
35+
Labels map[string]string
36+
}
37+
38+
func NewCmd(params *params.CmdParams) *cobra.Command {
39+
cmd := &cobra.Command{
40+
Use: "create",
41+
Short: "Creates a backup from a specific source",
42+
Long: "Creates a backup from a specific source (volume or snapshot).",
43+
Args: args.NoArgs,
44+
Example: examples.Build(
45+
examples.NewExample(
46+
`Create a backup from a volume`,
47+
"$ stackit volume backup create --source-id xxx --source-type volume --project-id xxx"),
48+
examples.NewExample(
49+
`Create a backup from a snapshot with a name`,
50+
"$ stackit volume backup create --source-id xxx --source-type snapshot --name my-backup --project-id xxx"),
51+
examples.NewExample(
52+
`Create a backup with labels`,
53+
"$ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2 --project-id xxx"),
54+
),
55+
RunE: func(cmd *cobra.Command, args []string) error {
56+
ctx := context.Background()
57+
model, err := parseInput(params.Printer, cmd)
58+
if err != nil {
59+
return err
60+
}
61+
62+
// Configure API client
63+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
64+
if err != nil {
65+
return err
66+
}
67+
68+
projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd)
69+
if err != nil {
70+
params.Printer.Debug(print.ErrorLevel, "get project name: %v", err)
71+
projectLabel = model.ProjectId
72+
}
73+
74+
if !model.AssumeYes {
75+
prompt := fmt.Sprintf("Are you sure you want to create backup from %s? (This cannot be undone)", model.SourceID)
76+
err = params.Printer.PromptForConfirmation(prompt)
77+
if err != nil {
78+
return err
79+
}
80+
}
81+
82+
// TODO: necessary? utils for each service seperately?
83+
// Check if the project is enabled before trying to create
84+
// enabled, err := utils.ProjectEnabled(ctx, apiClient, model.ProjectId)
85+
// if err != nil {
86+
// return fmt.Errorf("check if IaaS is enabled: %w", err)
87+
// }
88+
// if !enabled {
89+
// return &errors.ServiceDisabledError{
90+
// Service: "iaas",
91+
// }
92+
// }
93+
94+
// Call API
95+
req := buildRequest(model, apiClient, ctx)
96+
resp, err := req.Execute()
97+
if err != nil {
98+
return fmt.Errorf("create volume backup: %w", err)
99+
}
100+
101+
// TODO: How to check source-name exists?
102+
// Get source label (use ID if name not available)
103+
// sourceLabel := model.SourceID
104+
105+
// TODO: SDK needs to be updated/released to support this async operation
106+
// Wait for async operation, if async mode not enabled
107+
// if !model.Async {
108+
// s := spinner.New(params.Printer)
109+
// s.Start("Creating backup")
110+
// _, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, model.SourceID).WaitWithContext(ctx)
111+
// if err != nil {
112+
// return fmt.Errorf("wait for volume backup creation: %w", err)
113+
// }
114+
// s.Stop()
115+
// }
116+
117+
return outputResult(params.Printer, model.OutputFormat, model.Async, model.SourceID, projectLabel, resp)
118+
},
119+
}
120+
121+
configureFlags(cmd)
122+
return cmd
123+
}
124+
125+
func configureFlags(cmd *cobra.Command) {
126+
cmd.Flags().String(sourceIdFlag, "", "ID of the source from which a backup should be created")
127+
cmd.Flags().String(sourceTypeFlag, "", "Source type of the backup (volume or snapshot)")
128+
cmd.Flags().String(nameFlag, "", "Name of the backup")
129+
cmd.Flags().StringToString(labelsFlag, nil, "Key-value string pairs as labels")
130+
131+
err := flags.MarkFlagsRequired(cmd, sourceIdFlag, sourceTypeFlag)
132+
cobra.CheckErr(err)
133+
}
134+
135+
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
136+
globalFlags := globalflags.Parse(p, cmd)
137+
if globalFlags.ProjectId == "" {
138+
return nil, &errors.ProjectIdError{}
139+
}
140+
141+
sourceID := flags.FlagToStringValue(p, cmd, sourceIdFlag)
142+
if sourceID == "" {
143+
return nil, fmt.Errorf("source-id is required")
144+
}
145+
146+
sourceType := flags.FlagToStringValue(p, cmd, sourceTypeFlag)
147+
if sourceType != "volume" && sourceType != "snapshot" {
148+
return nil, fmt.Errorf("source-type must be either 'volume' or 'snapshot'")
149+
}
150+
151+
name := flags.FlagToStringValue(p, cmd, nameFlag)
152+
labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag)
153+
154+
model := inputModel{
155+
GlobalFlagModel: globalFlags,
156+
SourceID: sourceID,
157+
SourceType: sourceType,
158+
Name: &name,
159+
Labels: *labels,
160+
}
161+
162+
if p.IsVerbosityDebug() {
163+
modelStr, err := print.BuildDebugStrFromInputModel(model)
164+
if err != nil {
165+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
166+
} else {
167+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
168+
}
169+
}
170+
171+
return &model, nil
172+
}
173+
174+
// TODO: Enough?
175+
func buildRequest(model *inputModel, apiClient *iaas.APIClient, ctx context.Context) iaas.ApiCreateBackupRequest {
176+
// TODO: doc says if createeBackup func provides snapshot-id but isnt in the func-signature?
177+
req := apiClient.CreateBackup(ctx, model.ProjectId)
178+
return req
179+
}
180+
181+
// TODO: create(volume)BackupResponse needs to be created?
182+
func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel, projectLabel string, resp *iaas.CreateVolumeBackupResponse) error {
183+
if resp == nil {
184+
return fmt.Errorf("create backup response is empty")
185+
}
186+
187+
switch outputFormat {
188+
case print.JSONOutputFormat:
189+
details, err := json.MarshalIndent(resp, "", " ")
190+
if err != nil {
191+
return fmt.Errorf("marshal backup: %w", err)
192+
}
193+
p.Outputln(string(details))
194+
return nil
195+
196+
case print.YAMLOutputFormat:
197+
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
198+
if err != nil {
199+
return fmt.Errorf("marshal backup: %w", err)
200+
}
201+
p.Outputln(string(details))
202+
return nil
203+
204+
default:
205+
if async {
206+
p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Backup.Id)
207+
} else {
208+
p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Backup.Id)
209+
}
210+
return nil
211+
}
212+
}

internal/cmd/volume/volume.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package volume
22

33
import (
44
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
5+
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup"
56
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/create"
67
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/delete"
78
"github.com/stackitcloud/stackit-cli/internal/cmd/volume/describe"
@@ -35,4 +36,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) {
3536
cmd.AddCommand(update.NewCmd(params))
3637
cmd.AddCommand(resize.NewCmd(params))
3738
cmd.AddCommand(performanceclass.NewCmd(params))
39+
cmd.AddCommand(backup.NewCmd(params))
3840
}

0 commit comments

Comments
 (0)