Skip to content

Commit 386982b

Browse files
UX Improvements: fixes and improvements (#190)
* add default to project list * project list: improve implementation, add testing * minor improvements to code * fix linting * change keyring testing * address comments in PR * address PR comments * generate docs * Update internal/pkg/args/args.go Co-authored-by: João Palet <joao.palet@outlook.com> * fix linting --------- Co-authored-by: João Palet <joao.palet@outlook.com>
1 parent a0e351d commit 386982b

File tree

8 files changed

+180
-13
lines changed

8 files changed

+180
-13
lines changed

docs/stackit_project_list.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ stackit project list [flags]
1313
### Examples
1414

1515
```
16-
List all STACKIT projects that authenticated user is a member of
16+
List all STACKIT projects that the authenticated user or service account is a member of
1717
$ stackit project list
1818
1919
List all STACKIT projects that are children of a specific parent

internal/cmd/dns/record-set/describe/describe.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func outputResult(cmd *cobra.Command, outputFormat string, recordSet *dns.Record
107107
for _, r := range *recordSet.Records {
108108
recordsData = append(recordsData, *r.Content)
109109
}
110-
recordsDataJoin := strings.Join(recordsData, ",")
110+
recordsDataJoin := strings.Join(recordsData, ", ")
111111

112112
table := tables.NewTable()
113113
table.AddRow("ID", *recordSet.Id)

internal/cmd/project/describe/describe.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,26 @@ import (
66
"fmt"
77

88
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
9-
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
109
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
1110
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
1211
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
1312
"github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client"
1413
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
1515

1616
"github.com/spf13/cobra"
1717
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
1818
)
1919

2020
const (
2121
includeParentsFlag = "include-parents"
22+
23+
projectIdArg = "PROJECT_ID"
2224
)
2325

2426
type inputModel struct {
2527
*globalflags.GlobalFlagModel
28+
ArgProjectId string
2629
IncludeParents bool
2730
}
2831

@@ -31,7 +34,7 @@ func NewCmd() *cobra.Command {
3134
Use: "describe",
3235
Short: "Shows details of a STACKIT project",
3336
Long: "Shows details of a STACKIT project.",
34-
Args: args.NoArgs,
37+
Args: args.SingleOptionalArg(projectIdArg, utils.ValidateUUID),
3538
Example: examples.Build(
3639
examples.NewExample(
3740
`Get the details of the configured STACKIT project`,
@@ -45,7 +48,7 @@ func NewCmd() *cobra.Command {
4548
),
4649
RunE: func(cmd *cobra.Command, args []string) error {
4750
ctx := context.Background()
48-
model, err := parseInput(cmd)
51+
model, err := parseInput(cmd, args)
4952
if err != nil {
5053
return err
5154
}
@@ -74,20 +77,30 @@ func configureFlags(cmd *cobra.Command) {
7477
cmd.Flags().Bool(includeParentsFlag, false, "When true, the details of the parent resources will be included in the output")
7578
}
7679

77-
func parseInput(cmd *cobra.Command) (*inputModel, error) {
80+
func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
81+
var projectId string
82+
if len(inputArgs) > 0 {
83+
projectId = inputArgs[0]
84+
}
85+
7886
globalFlags := globalflags.Parse(cmd)
79-
if globalFlags.ProjectId == "" {
80-
return nil, &errors.ProjectIdError{}
87+
if globalFlags.ProjectId == "" && projectId == "" {
88+
return nil, fmt.Errorf("Project ID needs to be provided either as an argument or as a flag")
89+
}
90+
91+
if projectId == "" {
92+
projectId = globalFlags.ProjectId
8193
}
8294

8395
return &inputModel{
8496
GlobalFlagModel: globalFlags,
97+
ArgProjectId: projectId,
8598
IncludeParents: flags.FlagToBoolValue(cmd, includeParentsFlag),
8699
}, nil
87100
}
88101

89102
func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) resourcemanager.ApiGetProjectRequest {
90-
req := apiClient.GetProject(ctx, model.ProjectId)
103+
req := apiClient.GetProject(ctx, model.ArgProjectId)
91104
req.IncludeParents(model.IncludeParents)
92105
return req
93106
}

internal/cmd/project/describe/describe_test.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,20 @@ type testCtxKey struct{}
1919
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
2020
var testClient = &resourcemanager.APIClient{}
2121
var testProjectId = uuid.NewString()
22+
var testProjectId2 = uuid.NewString()
23+
24+
func fixtureArgValues(mods ...func(argValues []string)) []string {
25+
argValues := []string{
26+
testProjectId,
27+
}
28+
for _, mod := range mods {
29+
mod(argValues)
30+
}
31+
return argValues
32+
}
2233

2334
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
2435
flagValues := map[string]string{
25-
projectIdFlag: testProjectId,
2636
includeParentsFlag: "false",
2737
}
2838
for _, mod := range mods {
@@ -34,9 +44,10 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st
3444
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
3545
model := &inputModel{
3646
GlobalFlagModel: &globalflags.GlobalFlagModel{
37-
ProjectId: testProjectId,
47+
ProjectId: "",
3848
},
3949
IncludeParents: false,
50+
ArgProjectId: testProjectId,
4051
}
4152
for _, mod := range mods {
4253
mod(model)
@@ -55,24 +66,50 @@ func fixtureRequest(mods ...func(request *resourcemanager.ApiGetProjectRequest))
5566
func TestParseInput(t *testing.T) {
5667
tests := []struct {
5768
description string
69+
argValues []string
5870
flagValues map[string]string
5971
labelValues []string
6072
isValid bool
6173
expectedModel *inputModel
6274
}{
6375
{
6476
description: "base",
77+
argValues: fixtureArgValues(),
6578
flagValues: fixtureFlagValues(),
6679
isValid: true,
6780
expectedModel: fixtureInputModel(),
6881
},
6982
{
7083
description: "no values",
84+
argValues: []string{},
7185
flagValues: map[string]string{},
7286
isValid: false,
7387
},
88+
{
89+
description: "project id arg takes precedence",
90+
argValues: fixtureArgValues(),
91+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
92+
flagValues[projectIdFlag] = testProjectId2
93+
}),
94+
isValid: true,
95+
expectedModel: fixtureInputModel(func(model *inputModel) {
96+
model.ProjectId = testProjectId2
97+
}),
98+
},
99+
{
100+
description: "project id arg missing",
101+
argValues: []string{},
102+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
103+
flagValues[projectIdFlag] = testProjectId
104+
}),
105+
isValid: true,
106+
expectedModel: fixtureInputModel(func(model *inputModel) {
107+
model.ProjectId = testProjectId
108+
}),
109+
},
74110
{
75111
description: "project id missing",
112+
argValues: []string{},
76113
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
77114
delete(flagValues, projectIdFlag)
78115
}),
@@ -112,6 +149,14 @@ func TestParseInput(t *testing.T) {
112149
}
113150
}
114151

152+
err = cmd.ValidateArgs(tt.argValues)
153+
if err != nil {
154+
if !tt.isValid {
155+
return
156+
}
157+
t.Fatalf("error validating args: %v", err)
158+
}
159+
115160
err = cmd.ValidateRequiredFlags()
116161
if err != nil {
117162
if !tt.isValid {
@@ -120,7 +165,7 @@ func TestParseInput(t *testing.T) {
120165
t.Fatalf("error validating flags: %v", err)
121166
}
122167

123-
model, err := parseInput(cmd)
168+
model, err := parseInput(cmd, tt.argValues)
124169
if err != nil {
125170
if !tt.isValid {
126171
return

internal/cmd/project/list/list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func NewCmd() *cobra.Command {
4949
Args: args.NoArgs,
5050
Example: examples.Build(
5151
examples.NewExample(
52-
`List all STACKIT projects that the authenticated user is a member of`,
52+
`List all STACKIT projects that the authenticated user or service account is a member of`,
5353
"$ stackit project list"),
5454
examples.NewExample(
5555
`List all STACKIT projects that are children of a specific parent`,

internal/pkg/args/args.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,28 @@ func SingleArg(argName string, validate func(value string) error) cobra.Position
4242
return nil
4343
}
4444
}
45+
46+
// SingleOptionalArg checks if one or no arguments were provided and validates it if provided
47+
// using the validate function. It returns an error if more than one argument is provided, or if
48+
// the argument is invalid. For no validation, you can pass a nil validate function
49+
func SingleOptionalArg(argName string, validate func(value string) error) cobra.PositionalArgs {
50+
return func(cmd *cobra.Command, args []string) error {
51+
if len(args) > 1 {
52+
return &errors.SingleOptionalArgExpectedError{
53+
Cmd: cmd,
54+
Expected: argName,
55+
Count: len(args),
56+
}
57+
}
58+
if len(args) == 1 && validate != nil {
59+
err := validate(args[0])
60+
if err != nil {
61+
return &errors.ArgValidationError{
62+
Arg: argName,
63+
Details: err.Error(),
64+
}
65+
}
66+
}
67+
return nil
68+
}
69+
}

internal/pkg/args/args_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,74 @@ func TestSingleArg(t *testing.T) {
107107
})
108108
}
109109
}
110+
111+
func TestSingleOptionalArg(t *testing.T) {
112+
tests := []struct {
113+
description string
114+
args []string
115+
validateFunc func(value string) error
116+
isValid bool
117+
}{
118+
{
119+
description: "valid",
120+
args: []string{"arg"},
121+
validateFunc: func(value string) error {
122+
return nil
123+
},
124+
isValid: true,
125+
},
126+
{
127+
description: "no_arg",
128+
args: []string{},
129+
isValid: true,
130+
},
131+
{
132+
description: "more_than_one_arg",
133+
args: []string{"arg", "arg2"},
134+
isValid: false,
135+
},
136+
{
137+
description: "empty_arg",
138+
args: []string{""},
139+
isValid: true,
140+
},
141+
{
142+
description: "invalid_arg",
143+
args: []string{"arg"},
144+
validateFunc: func(value string) error {
145+
return fmt.Errorf("error")
146+
},
147+
isValid: false,
148+
},
149+
{
150+
description: "nil validation function",
151+
args: []string{"arg"},
152+
validateFunc: nil,
153+
isValid: true,
154+
},
155+
{
156+
description: "nil validation function, no args",
157+
args: []string{},
158+
validateFunc: nil,
159+
isValid: true,
160+
},
161+
}
162+
for _, tt := range tests {
163+
t.Run(tt.description, func(t *testing.T) {
164+
cmd := &cobra.Command{
165+
Use: "test",
166+
Short: "Test command",
167+
}
168+
169+
argFunction := SingleOptionalArg("test", tt.validateFunc)
170+
err := argFunction(cmd, tt.args)
171+
172+
if tt.isValid && err != nil {
173+
t.Fatalf("should not have failed: %v", err)
174+
}
175+
if !tt.isValid && err == nil {
176+
t.Fatalf("should have failed")
177+
}
178+
})
179+
}
180+
}

internal/pkg/errors/errors.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ For more details on the available storages for the configured flavor (%[3]s), ru
107107

108108
SINGLE_ARG_EXPECTED = `expected 1 argument %q, %d were provided`
109109

110+
SINGLE_OPTIONAL_ARG_EXPECTED = `expected no more than 1 argument %q, %d were provided`
111+
110112
SUBCOMMAND_UNKNOWN = `unknown subcommand %q`
111113

112114
SUBCOMMAND_MISSING = `missing subcommand`
@@ -260,6 +262,17 @@ func (e *SingleArgExpectedError) Error() string {
260262
return AppendUsageTip(err, e.Cmd).Error()
261263
}
262264

265+
type SingleOptionalArgExpectedError struct {
266+
Cmd *cobra.Command
267+
Expected string
268+
Count int
269+
}
270+
271+
func (e *SingleOptionalArgExpectedError) Error() string {
272+
err := fmt.Errorf(SINGLE_OPTIONAL_ARG_EXPECTED, e.Expected, e.Count)
273+
return AppendUsageTip(err, e.Cmd).Error()
274+
}
275+
263276
// Used when an unexpected non-flag input (either arg or subcommand) is found
264277
type InputUnknownError struct {
265278
ProvidedInput string

0 commit comments

Comments
 (0)