Skip to content

Commit ab287eb

Browse files
committed
wip
1 parent b556241 commit ab287eb

File tree

3 files changed

+224
-15
lines changed

3 files changed

+224
-15
lines changed

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ module github.com/stackitcloud/stackit-cli
22

33
go 1.24
44

5+
replace (
6+
github.com/stackitcloud/stackit-sdk-go/services/iaas => /Users/bfuertsch/work/StackIT/stackit-sdk-go/services/iaas
7+
)
8+
59
require (
610
github.com/fatih/color v1.18.0
711
github.com/goccy/go-yaml v1.17.1

internal/cmd/volume/backup/create/create.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import (
1515
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
1616
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
1717
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
1819

1920
"github.com/spf13/cobra"
2021
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
22+
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
2123
)
2224

2325
const (
@@ -79,7 +81,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command {
7981
}
8082
}
8183

82-
// TODO: necessary? utils for each service seperately?
84+
// TODO: why not necessary here? utils for each service seperately?
8385
// Check if the project is enabled before trying to create
8486
// enabled, err := utils.ProjectEnabled(ctx, apiClient, model.ProjectId)
8587
// if err != nil {
@@ -98,21 +100,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command {
98100
return fmt.Errorf("create volume backup: %w", err)
99101
}
100102

101-
// TODO: How to check source-name exists?
103+
// TODO: How to check if "source-name" exists?
102104
// Get source label (use ID if name not available)
103105
// sourceLabel := model.SourceID
104106

105107
// TODO: SDK needs to be updated/released to support this async operation
106108
// 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-
// }
109+
if !model.Async {
110+
s := spinner.New(params.Printer)
111+
s.Start("Creating backup")
112+
_, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, model.SourceID).WaitWithContext(ctx)
113+
if err != nil {
114+
return fmt.Errorf("wait for volume backup creation: %w", err)
115+
}
116+
s.Stop()
117+
}
116118

117119
return outputResult(params.Printer, model.OutputFormat, model.Async, model.SourceID, projectLabel, resp)
118120
},
@@ -178,8 +180,8 @@ func buildRequest(model *inputModel, apiClient *iaas.APIClient, ctx context.Cont
178180
return req
179181
}
180182

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+
// TODO: create(volume)BackupResponse or createBackupResponse needs to be created
184+
func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel, projectLabel string, resp *iaas.Backup) error {
183185
if resp == nil {
184186
return fmt.Errorf("create backup response is empty")
185187
}
@@ -203,9 +205,9 @@ func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel
203205

204206
default:
205207
if async {
206-
p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Backup.Id)
208+
p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id)
207209
} else {
208-
p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Backup.Id)
210+
p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, *resp.Id)
209211
}
210212
return nil
211213
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package list
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/goccy/go-yaml"
9+
"github.com/spf13/cobra"
10+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
19+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
20+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
21+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
22+
)
23+
24+
const (
25+
limitFlag = "limit"
26+
labelSelectorFlag = "label-selector"
27+
)
28+
29+
type inputModel struct {
30+
*globalflags.GlobalFlagModel
31+
Limit *int64
32+
LabelSelector *string
33+
}
34+
35+
func NewCmd(params *params.CmdParams) *cobra.Command {
36+
cmd := &cobra.Command{
37+
Use: "list",
38+
Short: "Lists all backups",
39+
Long: "Lists all backups in a project.",
40+
Args: args.NoArgs,
41+
Example: examples.Build(
42+
examples.NewExample(
43+
`List all volume backups`,
44+
"$ stackit volume backup list"),
45+
examples.NewExample(
46+
`List all volume backups in JSON format`,
47+
"$ stackit volume backup list --output-format json"),
48+
examples.NewExample(
49+
`List up to 10 volume backups`,
50+
"$ stackit volume backup list --limit 10"),
51+
examples.NewExample(
52+
`List volume backups with specific labels`,
53+
"$ stackit volume backup list --label-selector key1=value1,key2=value2"),
54+
),
55+
RunE: func(cmd *cobra.Command, _ []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+
// Call API
69+
req := buildRequest(ctx, model, apiClient)
70+
resp, err := req.Execute()
71+
if err != nil {
72+
return fmt.Errorf("get volume backups: %w", err)
73+
}
74+
if resp.Items == nil || len(*resp.Items) == 0 {
75+
projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd)
76+
if err != nil {
77+
params.Printer.Debug(print.ErrorLevel, "get project name: %v", err)
78+
projectLabel = model.ProjectId
79+
}
80+
params.Printer.Info("No backups found for project %s\n", projectLabel)
81+
return nil
82+
}
83+
backups := *resp.Items
84+
85+
// Truncate output
86+
if model.Limit != nil && len(backups) > int(*model.Limit) {
87+
backups = backups[:*model.Limit]
88+
}
89+
90+
return outputResult(params.Printer, model.OutputFormat, backups)
91+
},
92+
}
93+
94+
configureFlags(cmd)
95+
return cmd
96+
}
97+
98+
func configureFlags(cmd *cobra.Command) {
99+
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
100+
cmd.Flags().String(labelSelectorFlag, "", "Filter backups by labels (comma-separated key=value pairs)")
101+
}
102+
103+
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
104+
globalFlags := globalflags.Parse(p, cmd)
105+
if globalFlags.ProjectId == "" {
106+
return nil, &errors.ProjectIdError{}
107+
}
108+
109+
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
110+
if limit != nil && *limit < 1 {
111+
return nil, &errors.FlagValidationError{
112+
Flag: limitFlag,
113+
Details: "must be greater than 0",
114+
}
115+
}
116+
117+
labelSelector := flags.FlagToStringPointer(p, cmd, labelSelectorFlag)
118+
119+
model := inputModel{
120+
GlobalFlagModel: globalFlags,
121+
Limit: limit,
122+
LabelSelector: labelSelector,
123+
}
124+
125+
if p.IsVerbosityDebug() {
126+
modelStr, err := print.BuildDebugStrFromInputModel(model)
127+
if err != nil {
128+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
129+
} else {
130+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
131+
}
132+
}
133+
134+
return &model, nil
135+
}
136+
137+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListBackupsRequest {
138+
req := apiClient.ListBackups(ctx, model.ProjectId)
139+
140+
if model.LabelSelector != nil {
141+
req = req.LabelSelector(*model.LabelSelector)
142+
}
143+
144+
return req
145+
}
146+
147+
func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) error {
148+
if backups == nil {
149+
return fmt.Errorf("backups is empty")
150+
}
151+
152+
switch outputFormat {
153+
case print.JSONOutputFormat:
154+
details, err := json.MarshalIndent(backups, "", " ")
155+
if err != nil {
156+
return fmt.Errorf("marshal volume backup list: %w", err)
157+
}
158+
p.Outputln(string(details))
159+
return nil
160+
161+
case print.YAMLOutputFormat:
162+
details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
163+
if err != nil {
164+
return fmt.Errorf("marshal volume backup list: %w", err)
165+
}
166+
p.Outputln(string(details))
167+
return nil
168+
169+
default:
170+
table := tables.NewTable()
171+
table.SetHeader("ID", "NAME", "SIZE", "STATUS", "SNAPSHOT ID", "VOLUME ID", "AVAILABILITY ZONE", "LABELS", "CREATED AT", "UPDATED AT")
172+
173+
for i := range backups {
174+
backup := backups[i]
175+
176+
// Format labels as a string
177+
labelsStr := ""
178+
if backup.Labels != nil {
179+
labelsStr = utils.FormatLabelsAsString(*backup.Labels)
180+
}
181+
182+
table.AddRow(
183+
utils.PtrString(backup.Id),
184+
utils.PtrString(backup.Name),
185+
utils.FormatSize(backup.Size),
186+
utils.PtrString(backup.Status),
187+
utils.PtrString(backup.SnapshotId),
188+
utils.PtrString(backup.VolumeId),
189+
utils.PtrString(backup.AvailabilityZone),
190+
labelsStr,
191+
utils.FormatTimestamp(backup.CreatedAt),
192+
utils.FormatTimestamp(backup.UpdatedAt),
193+
)
194+
}
195+
196+
err := table.Display(p)
197+
if err != nil {
198+
return fmt.Errorf("render table: %w", err)
199+
}
200+
201+
return nil
202+
}
203+
}

0 commit comments

Comments
 (0)