Skip to content

Commit 9103656

Browse files
committed
feat(resourcemanager): List and describe organizations
relates to STACKITCLI-325
1 parent f9e97d0 commit 9103656

File tree

5 files changed

+643
-0
lines changed

5 files changed

+643
-0
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package describe
2+
3+
import (
4+
"context"
5+
6+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
16+
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
17+
)
18+
19+
const (
20+
organizationIdArg = "ORGANIZATION_ID"
21+
)
22+
23+
type inputModel struct {
24+
*globalflags.GlobalFlagModel
25+
OrganizationId string
26+
}
27+
28+
func NewCmd(params *types.CmdParams) *cobra.Command {
29+
cmd := &cobra.Command{
30+
Use: "describe",
31+
Short: "Show a organization",
32+
Long: "Show a organization.",
33+
// the arg can be the organization uuid or the container id, which is not a uuid, so no validation needed
34+
Args: args.SingleArg(organizationIdArg, nil),
35+
Example: examples.Build(
36+
examples.NewExample(
37+
`Describe the organization with the organization uuid "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"`,
38+
"$ stackit organization describe xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
39+
),
40+
examples.NewExample(
41+
`Describe the organization with the container id "foo-bar-organization"`,
42+
"$ stackit organization describe foo-bar-organization",
43+
),
44+
),
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
ctx := context.Background()
47+
model, err := parseInput(params.Printer, cmd, args)
48+
if err != nil {
49+
return err
50+
}
51+
52+
// Configure API client
53+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
54+
if err != nil {
55+
return err
56+
}
57+
58+
// Call API
59+
req := buildRequest(ctx, model, apiClient)
60+
resp, err := req.Execute()
61+
if err != nil {
62+
return err
63+
}
64+
65+
return outputResult(params.Printer, model.OutputFormat, resp)
66+
},
67+
}
68+
return cmd
69+
}
70+
71+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
72+
organizationId := inputArgs[0]
73+
globalFlags := globalflags.Parse(p, cmd)
74+
75+
model := inputModel{
76+
GlobalFlagModel: globalFlags,
77+
OrganizationId: organizationId,
78+
}
79+
80+
p.DebugInputModel(model)
81+
return &model, nil
82+
}
83+
84+
func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) resourcemanager.ApiGetOrganizationRequest {
85+
req := apiClient.GetOrganization(ctx, model.OrganizationId)
86+
return req
87+
}
88+
89+
func outputResult(p *print.Printer, outputFormat string, organization *resourcemanager.OrganizationResponse) error {
90+
return p.OutputResult(outputFormat, organization, func() error {
91+
if organization == nil {
92+
p.Error("show organization: empty response")
93+
return nil
94+
}
95+
96+
table := tables.NewTable()
97+
98+
table.AddRow("ORGANIZATION ID", utils.PtrString(organization.OrganizationId))
99+
table.AddSeparator()
100+
table.AddRow("NAME", utils.PtrString(organization.Name))
101+
table.AddSeparator()
102+
table.AddRow("CONTAINER ID", utils.PtrString(organization.ContainerId))
103+
table.AddSeparator()
104+
table.AddRow("STATUS", utils.PtrString(organization.LifecycleState))
105+
table.AddSeparator()
106+
table.AddRow("CREATION TIME", utils.PtrString(organization.CreationTime))
107+
table.AddSeparator()
108+
table.AddRow("UPDATE TIME", utils.PtrString(organization.UpdateTime))
109+
table.AddSeparator()
110+
table.AddRow("LABELS", utils.JoinStringMap(utils.PtrValue(organization.Labels), ": ", ", "))
111+
112+
p.Outputln(table.Render())
113+
return nil
114+
})
115+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package describe
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
10+
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
14+
15+
"github.com/google/go-cmp/cmp"
16+
"github.com/google/go-cmp/cmp/cmpopts"
17+
"github.com/google/uuid"
18+
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
19+
)
20+
21+
type testCtxKey struct{}
22+
23+
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
24+
var testClient = &resourcemanager.APIClient{}
25+
26+
var (
27+
testOrganizationId = uuid.NewString()
28+
)
29+
30+
func fixtureArgValues(mods ...func(argValues []string)) []string {
31+
argValues := []string{
32+
testOrganizationId,
33+
}
34+
for _, mod := range mods {
35+
mod(argValues)
36+
}
37+
return argValues
38+
}
39+
40+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
41+
model := &inputModel{
42+
GlobalFlagModel: &globalflags.GlobalFlagModel{
43+
Verbosity: globalflags.VerbosityDefault,
44+
},
45+
OrganizationId: testOrganizationId,
46+
}
47+
for _, mod := range mods {
48+
mod(model)
49+
}
50+
return model
51+
}
52+
53+
func fixtureRequest(mods ...func(request *resourcemanager.ApiGetOrganizationRequest)) resourcemanager.ApiGetOrganizationRequest {
54+
request := testClient.GetOrganization(testCtx, testOrganizationId)
55+
for _, mod := range mods {
56+
mod(&request)
57+
}
58+
return request
59+
}
60+
61+
func TestParseInput(t *testing.T) {
62+
tests := []struct {
63+
description string
64+
argValues []string
65+
flagValues map[string]string
66+
isValid bool
67+
expectedModel *inputModel
68+
}{
69+
{
70+
description: "base",
71+
argValues: fixtureArgValues(),
72+
isValid: true,
73+
expectedModel: fixtureInputModel(),
74+
},
75+
{
76+
description: "uuid as example for an organization id",
77+
argValues: []string{"12345678-90ab-cdef-1234-1234567890ab"},
78+
isValid: true,
79+
expectedModel: fixtureInputModel(func(model *inputModel) {
80+
model.OrganizationId = "12345678-90ab-cdef-1234-1234567890ab"
81+
}),
82+
},
83+
{
84+
description: "non uuid string as example for a container id",
85+
argValues: []string{"foo-bar-organization"},
86+
isValid: true,
87+
expectedModel: fixtureInputModel(func(model *inputModel) {
88+
model.OrganizationId = "foo-bar-organization"
89+
}),
90+
},
91+
{
92+
description: "no args",
93+
argValues: []string{},
94+
isValid: false,
95+
},
96+
}
97+
98+
for _, tt := range tests {
99+
t.Run(tt.description, func(t *testing.T) {
100+
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
101+
})
102+
}
103+
}
104+
105+
func TestBuildRequest(t *testing.T) {
106+
tests := []struct {
107+
description string
108+
model *inputModel
109+
expectedRequest resourcemanager.ApiGetOrganizationRequest
110+
}{
111+
{
112+
description: "base",
113+
model: fixtureInputModel(),
114+
expectedRequest: fixtureRequest(),
115+
},
116+
}
117+
118+
for _, tt := range tests {
119+
t.Run(tt.description, func(t *testing.T) {
120+
request := buildRequest(testCtx, tt.model, testClient)
121+
122+
diff := cmp.Diff(request, tt.expectedRequest,
123+
cmp.AllowUnexported(tt.expectedRequest),
124+
cmpopts.EquateComparable(testCtx),
125+
)
126+
if diff != "" {
127+
t.Fatalf("Data does not match: %s", diff)
128+
}
129+
})
130+
}
131+
}
132+
133+
func TestOutputResult(t *testing.T) {
134+
type args struct {
135+
outputFormat string
136+
organization *resourcemanager.OrganizationResponse
137+
}
138+
tests := []struct {
139+
name string
140+
args args
141+
wantErr bool
142+
}{
143+
{
144+
name: "empty",
145+
args: args{},
146+
wantErr: false,
147+
},
148+
{
149+
name: "nil pointer as organization",
150+
args: args{
151+
organization: nil,
152+
},
153+
wantErr: false,
154+
},
155+
{
156+
name: "empty organization",
157+
args: args{
158+
organization: utils.Ptr(resourcemanager.OrganizationResponse{}),
159+
},
160+
wantErr: false,
161+
},
162+
{
163+
name: "full response",
164+
args: args{
165+
organization: utils.Ptr(resourcemanager.OrganizationResponse{
166+
OrganizationId: utils.Ptr(uuid.NewString()),
167+
Name: utils.Ptr("foo bar"),
168+
LifecycleState: utils.Ptr(resourcemanager.LIFECYCLESTATE_ACTIVE),
169+
ContainerId: utils.Ptr("foo-bar-organization"),
170+
CreationTime: utils.Ptr(time.Now()),
171+
UpdateTime: utils.Ptr(time.Now()),
172+
Labels: utils.Ptr(map[string]string{
173+
"foo": "true",
174+
"bar": "false",
175+
}),
176+
}),
177+
},
178+
wantErr: false,
179+
},
180+
}
181+
p := print.NewPrinter()
182+
p.Cmd = NewCmd(&types.CmdParams{Printer: p})
183+
for _, tt := range tests {
184+
t.Run(tt.name, func(t *testing.T) {
185+
if err := outputResult(p, tt.args.outputFormat, tt.args.organization); (err != nil) != tt.wantErr {
186+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
187+
}
188+
})
189+
}
190+
}

0 commit comments

Comments
 (0)