Skip to content

Commit 76e378d

Browse files
committed
add snapshots describe subcommand
1 parent 1561c90 commit 76e378d

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package describe
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/globalflags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
19+
20+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
21+
)
22+
23+
const (
24+
snapshotIdArg = "SNAPSHOT_ID"
25+
)
26+
27+
type inputModel struct {
28+
*globalflags.GlobalFlagModel
29+
SnapshotId string
30+
}
31+
32+
func NewCmd(params *params.CmdParams) *cobra.Command {
33+
cmd := &cobra.Command{
34+
Use: fmt.Sprintf("describe %s", snapshotIdArg),
35+
Short: "Describes a snapshot",
36+
Long: "Describes a snapshot by its ID.",
37+
Args: args.SingleArg(snapshotIdArg, utils.ValidateUUID),
38+
Example: examples.Build(
39+
examples.NewExample(
40+
`Get details of a snapshot`,
41+
"$ stackit volume snapshot describe xxx-xxx-xxx"),
42+
examples.NewExample(
43+
`Get details of a snapshot in JSON format`,
44+
"$ stackit volume snapshot describe xxx-xxx-xxx --output-format json"),
45+
),
46+
RunE: func(cmd *cobra.Command, args []string) error {
47+
ctx := context.Background()
48+
model, err := parseInput(params.Printer, cmd, args)
49+
if err != nil {
50+
return err
51+
}
52+
53+
// Configure API client
54+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
55+
if err != nil {
56+
return err
57+
}
58+
59+
// Call API
60+
req := buildRequest(ctx, model, apiClient)
61+
resp, err := req.Execute()
62+
if err != nil {
63+
return fmt.Errorf("get snapshot details: %w", err)
64+
}
65+
66+
return outputResult(params.Printer, model.OutputFormat, resp)
67+
},
68+
}
69+
return cmd
70+
}
71+
72+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
73+
snapshotId := inputArgs[0]
74+
75+
globalFlags := globalflags.Parse(p, cmd)
76+
if globalFlags.ProjectId == "" {
77+
return nil, &errors.ProjectIdError{}
78+
}
79+
80+
model := inputModel{
81+
GlobalFlagModel: globalFlags,
82+
SnapshotId: snapshotId,
83+
}
84+
85+
if p.IsVerbosityDebug() {
86+
modelStr, err := print.BuildDebugStrFromInputModel(model)
87+
if err != nil {
88+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
89+
} else {
90+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
91+
}
92+
}
93+
94+
return &model, nil
95+
}
96+
97+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetSnapshotRequest {
98+
return apiClient.GetSnapshot(ctx, model.ProjectId, model.SnapshotId)
99+
}
100+
101+
func outputResult(p *print.Printer, outputFormat string, snapshot *iaas.Snapshot) error {
102+
if snapshot == nil {
103+
return fmt.Errorf("get snapshot response is empty")
104+
}
105+
106+
switch outputFormat {
107+
case print.JSONOutputFormat:
108+
details, err := json.MarshalIndent(snapshot, "", " ")
109+
if err != nil {
110+
return fmt.Errorf("marshal snapshot: %w", err)
111+
}
112+
p.Outputln(string(details))
113+
return nil
114+
115+
case print.YAMLOutputFormat:
116+
details, err := yaml.MarshalWithOptions(snapshot, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
117+
if err != nil {
118+
return fmt.Errorf("marshal snapshot: %w", err)
119+
}
120+
p.Outputln(string(details))
121+
return nil
122+
123+
default:
124+
table := tables.NewTable()
125+
table.SetHeader("ID", "NAME", "SIZE", "STATUS", "VOLUME ID", "LABELS", "CREATED AT", "UPDATED AT")
126+
127+
table.AddRow(
128+
utils.PtrString(snapshot.Id),
129+
utils.PtrString(snapshot.Name),
130+
utils.PtrByteSizeDefault((*int64)(snapshot.Size), ""),
131+
utils.PtrString(snapshot.Status),
132+
utils.PtrString(snapshot.VolumeId),
133+
utils.PtrStringDefault(snapshot.Labels, ""),
134+
utils.ConvertTimePToDateTimeString(snapshot.CreatedAt),
135+
utils.ConvertTimePToDateTimeString(snapshot.UpdatedAt),
136+
)
137+
138+
err := table.Display(p)
139+
if err != nil {
140+
return fmt.Errorf("render table: %w", err)
141+
}
142+
143+
return nil
144+
}
145+
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package describe
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
14+
"github.com/google/uuid"
15+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
16+
)
17+
18+
type testCtxKey struct{}
19+
20+
var (
21+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
22+
testClient = &iaas.APIClient{}
23+
testProjectId = uuid.NewString()
24+
testSnapshotId = uuid.NewString()
25+
)
26+
27+
func fixtureArgValues(mods ...func(argValues []string)) []string {
28+
argValues := []string{
29+
testSnapshotId,
30+
}
31+
for _, mod := range mods {
32+
mod(argValues)
33+
}
34+
return argValues
35+
}
36+
37+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
38+
flagValues := map[string]string{
39+
globalflags.ProjectIdFlag: testProjectId,
40+
}
41+
for _, mod := range mods {
42+
mod(flagValues)
43+
}
44+
return flagValues
45+
}
46+
47+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
48+
model := &inputModel{
49+
GlobalFlagModel: &globalflags.GlobalFlagModel{
50+
ProjectId: testProjectId,
51+
Verbosity: globalflags.VerbosityDefault,
52+
},
53+
SnapshotId: testSnapshotId,
54+
}
55+
for _, mod := range mods {
56+
mod(model)
57+
}
58+
return model
59+
}
60+
61+
func fixtureRequest(mods ...func(request *iaas.ApiGetSnapshotRequest)) iaas.ApiGetSnapshotRequest {
62+
request := testClient.GetSnapshot(testCtx, testProjectId, testSnapshotId)
63+
for _, mod := range mods {
64+
mod(&request)
65+
}
66+
return request
67+
}
68+
69+
func TestParseInput(t *testing.T) {
70+
tests := []struct {
71+
description string
72+
argValues []string
73+
flagValues map[string]string
74+
isValid bool
75+
expectedModel *inputModel
76+
}{
77+
{
78+
description: "base",
79+
argValues: fixtureArgValues(),
80+
flagValues: fixtureFlagValues(),
81+
isValid: true,
82+
expectedModel: fixtureInputModel(),
83+
},
84+
{
85+
description: "no values",
86+
argValues: []string{},
87+
flagValues: map[string]string{},
88+
isValid: false,
89+
},
90+
{
91+
description: "no arg values",
92+
argValues: []string{},
93+
flagValues: fixtureFlagValues(),
94+
isValid: false,
95+
},
96+
{
97+
description: "no flag values",
98+
argValues: fixtureArgValues(),
99+
flagValues: map[string]string{},
100+
isValid: false,
101+
},
102+
{
103+
description: "project id missing",
104+
argValues: fixtureArgValues(),
105+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
106+
delete(flagValues, globalflags.ProjectIdFlag)
107+
}),
108+
isValid: false,
109+
},
110+
{
111+
description: "project id invalid",
112+
argValues: fixtureArgValues(),
113+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
114+
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
115+
}),
116+
isValid: false,
117+
},
118+
{
119+
description: "snapshot id invalid",
120+
argValues: []string{"invalid-uuid"},
121+
flagValues: fixtureFlagValues(),
122+
isValid: false,
123+
},
124+
}
125+
126+
for _, tt := range tests {
127+
t.Run(tt.description, func(t *testing.T) {
128+
p := print.NewPrinter()
129+
cmd := NewCmd(&params.CmdParams{Printer: p})
130+
err := globalflags.Configure(cmd.Flags())
131+
if err != nil {
132+
t.Fatalf("configure global flags: %v", err)
133+
}
134+
135+
for flag, value := range tt.flagValues {
136+
err := cmd.Flags().Set(flag, value)
137+
if err != nil {
138+
if !tt.isValid {
139+
return
140+
}
141+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
142+
}
143+
}
144+
145+
err = cmd.ValidateArgs(tt.argValues)
146+
if err != nil {
147+
if !tt.isValid {
148+
return
149+
}
150+
t.Fatalf("error validating args: %v", err)
151+
}
152+
153+
model, err := parseInput(p, cmd, tt.argValues)
154+
if err != nil {
155+
if !tt.isValid {
156+
return
157+
}
158+
t.Fatalf("error parsing input: %v", err)
159+
}
160+
161+
if !tt.isValid {
162+
t.Fatalf("did not fail on invalid input")
163+
}
164+
diff := cmp.Diff(model, tt.expectedModel)
165+
if diff != "" {
166+
t.Fatalf("Data does not match: %s", diff)
167+
}
168+
})
169+
}
170+
}
171+
172+
func TestBuildRequest(t *testing.T) {
173+
tests := []struct {
174+
description string
175+
model *inputModel
176+
expectedRequest iaas.ApiGetSnapshotRequest
177+
}{
178+
{
179+
description: "base",
180+
model: fixtureInputModel(),
181+
expectedRequest: fixtureRequest(),
182+
},
183+
}
184+
185+
for _, tt := range tests {
186+
t.Run(tt.description, func(t *testing.T) {
187+
request := buildRequest(testCtx, tt.model, testClient)
188+
189+
diff := cmp.Diff(request, tt.expectedRequest,
190+
cmp.AllowUnexported(tt.expectedRequest),
191+
cmpopts.EquateComparable(testCtx),
192+
)
193+
if diff != "" {
194+
t.Fatalf("Data does not match: %s", diff)
195+
}
196+
})
197+
}
198+
}
199+
200+
func TestOutputResult(t *testing.T) {
201+
type args struct {
202+
outputFormat string
203+
snapshot *iaas.Snapshot
204+
}
205+
tests := []struct {
206+
name string
207+
args args
208+
wantErr bool
209+
}{
210+
{
211+
name: "empty",
212+
args: args{},
213+
wantErr: true,
214+
},
215+
{
216+
name: "empty snapshot",
217+
args: args{
218+
snapshot: &iaas.Snapshot{},
219+
},
220+
wantErr: false,
221+
},
222+
{
223+
name: "snapshot with values",
224+
args: args{
225+
snapshot: &iaas.Snapshot{
226+
Id: utils.Ptr("snapshot-1"),
227+
Name: utils.Ptr("test-snapshot"),
228+
},
229+
},
230+
wantErr: false,
231+
},
232+
}
233+
p := print.NewPrinter()
234+
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
235+
for _, tt := range tests {
236+
t.Run(tt.name, func(t *testing.T) {
237+
if err := outputResult(p, tt.args.outputFormat, tt.args.snapshot); (err != nil) != tt.wantErr {
238+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
239+
}
240+
})
241+
}
242+
}

0 commit comments

Comments
 (0)