Skip to content

Commit 3b53bef

Browse files
committed
working tree
Signed-off-by: Lukas Hoehl <lukas.hoehl@stackit.cloud> split into files Signed-off-by: Lukas Hoehl <lukas.hoehl@stackit.cloud>
1 parent fb6cfca commit 3b53bef

File tree

4 files changed

+257
-71
lines changed

4 files changed

+257
-71
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package list
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
7+
oapiError "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
8+
)
9+
10+
func isForbiddenError(err error) bool {
11+
var oAPIError *oapiError.GenericOpenAPIError
12+
if ok := errors.As(err, &oAPIError); !ok {
13+
return false
14+
}
15+
if oAPIError.StatusCode != http.StatusForbidden {
16+
return false
17+
}
18+
return true
19+
}

internal/cmd/project/list/list.go

Lines changed: 86 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
package list
22

33
import (
4+
"cmp"
45
"context"
56
"fmt"
7+
"path"
8+
"slices"
9+
"sync"
610
"time"
711

812
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
13+
"golang.org/x/sync/errgroup"
914

1015
"github.com/spf13/cobra"
1116
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
12-
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
1317
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
1418
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
1519
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
1620
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
1721
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
18-
"github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client"
22+
23+
authclient "github.com/stackitcloud/stackit-cli/internal/pkg/services/authorization/client"
24+
resourceclient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client"
1925
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
20-
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
26+
"github.com/stackitcloud/stackit-sdk-go/services/authorization"
2127
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
2228
)
2329

@@ -64,20 +70,24 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
6470
"$ stackit project list --member example@email.com"),
6571
),
6672
RunE: func(cmd *cobra.Command, args []string) error {
67-
ctx := context.Background()
6873
model, err := parseInput(params.Printer, cmd, args)
6974
if err != nil {
7075
return err
7176
}
7277

7378
// Configure API client
74-
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
79+
resourceClient, err := resourceclient.ConfigureClient(params.Printer, params.CliVersion)
80+
if err != nil {
81+
return err
82+
}
83+
84+
authClient, err := authclient.ConfigureClient(params.Printer, params.CliVersion)
7585
if err != nil {
7686
return err
7787
}
7888

7989
// Fetch projects
80-
projects, err := fetchProjects(ctx, model, apiClient)
90+
projects, err := fetchProjects(cmd.Context(), model, resourceClient, authClient)
8191
if err != nil {
8292
return err
8393
}
@@ -143,90 +153,97 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel,
143153
return &model, nil
144154
}
145155

146-
func buildRequest(ctx context.Context, model *inputModel, apiClient resourceManagerClient, offset int) (resourcemanager.ApiListProjectsRequest, error) {
147-
req := apiClient.ListProjects(ctx)
148-
if model.ParentId != nil {
149-
req = req.ContainerParentId(*model.ParentId)
150-
}
151-
if model.ProjectIdLike != nil {
152-
req = req.ContainerIds(model.ProjectIdLike)
153-
}
154-
if model.Member != nil {
155-
req = req.Member(*model.Member)
156-
}
157-
if model.CreationTimeAfter != nil {
158-
req = req.CreationTimeStart(*model.CreationTimeAfter)
159-
}
156+
type project struct {
157+
Name string
158+
ID string
159+
Organization string
160+
Folder []string
161+
}
160162

161-
if model.ParentId == nil && model.ProjectIdLike == nil && model.Member == nil {
162-
email, err := auth.GetAuthEmail()
163-
if err != nil {
164-
return req, fmt.Errorf("get email of authenticated user: %w", err)
165-
}
166-
req = req.Member(email)
163+
func (p project) FolderPath() string {
164+
return path.Join(p.Folder...)
165+
}
166+
167+
func getProjects(ctx context.Context, parent *node, org string, projChan chan<- project) error {
168+
g, ctx := errgroup.WithContext(ctx)
169+
for _, child := range parent.children {
170+
g.Go(func() error {
171+
if child.typ != resourceTypeProject {
172+
return getProjects(ctx, child, org, projChan)
173+
}
174+
parent := child.parent
175+
folderName := []string{}
176+
for parent != nil {
177+
if parent.typ == resourceTypeFolder {
178+
folderName = append([]string{parent.name}, folderName...)
179+
}
180+
parent = parent.parent
181+
}
182+
projChan <- project{
183+
Name: child.name,
184+
ID: child.resourceID,
185+
Organization: org,
186+
Folder: folderName,
187+
}
188+
return nil
189+
})
167190
}
168-
req = req.Limit(float32(model.PageSize))
169-
req = req.Offset(float32(offset))
170-
return req, nil
191+
return g.Wait()
171192
}
172193

173194
type resourceManagerClient interface {
174195
ListProjects(ctx context.Context) resourcemanager.ApiListProjectsRequest
175196
}
176197

177-
func fetchProjects(ctx context.Context, model *inputModel, apiClient resourceManagerClient) ([]resourcemanager.Project, error) {
178-
if model.Limit != nil && *model.Limit < model.PageSize {
179-
model.PageSize = *model.Limit
198+
func fetchProjects(ctx context.Context, model *inputModel, resourceClient *resourcemanager.APIClient, authClient *authorization.APIClient) ([]project, error) {
199+
tree, err := newResourceTree(resourceClient, authClient, model)
200+
if err != nil {
201+
return nil, err
180202
}
181203

182-
offset := 0
183-
projects := []resourcemanager.Project{}
184-
for {
185-
// Call API
186-
req, err := buildRequest(ctx, model, apiClient, offset)
187-
if err != nil {
188-
return nil, fmt.Errorf("build list projects request: %w", err)
189-
}
190-
resp, err := req.Execute()
191-
if err != nil {
192-
return nil, fmt.Errorf("get projects: %w", err)
193-
}
194-
respProjects := *resp.Items
195-
if len(respProjects) == 0 {
196-
break
197-
}
198-
projects = append(projects, respProjects...)
199-
// Stop if no more pages
200-
if len(respProjects) < int(model.PageSize) {
201-
break
204+
if err := tree.Fill(ctx); err != nil {
205+
return nil, err
206+
}
207+
208+
var projs []project
209+
projChan := make(chan project)
210+
211+
var wg sync.WaitGroup
212+
go func() {
213+
wg.Add(1)
214+
defer wg.Done()
215+
for p := range projChan {
216+
i, _ := slices.BinarySearchFunc(projs, p, func(e project, target project) int {
217+
if orgCmp := cmp.Compare(e.Organization, target.Organization); orgCmp != 0 {
218+
return orgCmp
219+
}
220+
return cmp.Compare(e.FolderPath(), p.FolderPath())
221+
})
222+
projs = slices.Insert(projs, i, p)
202223
}
224+
}()
203225

204-
// Stop and truncate if limit is reached
205-
if model.Limit != nil && len(projects) >= int(*model.Limit) {
206-
projects = projects[:*model.Limit]
207-
break
226+
for _, root := range tree.roots {
227+
if err := getProjects(ctx, root, root.name, projChan); err != nil {
228+
return nil, err
208229
}
209-
offset += int(model.PageSize)
210230
}
211-
return projects, nil
231+
close(projChan)
232+
wg.Wait()
233+
return projs, nil
212234
}
213235

214-
func outputResult(p *print.Printer, outputFormat string, projects []resourcemanager.Project) error {
236+
func outputResult(p *print.Printer, outputFormat string, projects []project) error {
215237
return p.OutputResult(outputFormat, projects, func() error {
216238
table := tables.NewTable()
217-
table.SetHeader("ID", "NAME", "STATE", "PARENT ID")
239+
table.SetHeader("ORGANIZATION", "FOLDER", "NAME", "ID")
218240
for i := range projects {
219241
p := projects[i]
220-
221-
var parentId *string
222-
if p.Parent != nil {
223-
parentId = p.Parent.Id
224-
}
225242
table.AddRow(
226-
utils.PtrString(p.ProjectId),
227-
utils.PtrString(p.Name),
228-
utils.PtrString(p.LifecycleState),
229-
utils.PtrString(parentId),
243+
p.Organization,
244+
p.FolderPath(),
245+
p.Name,
246+
p.ID,
230247
)
231248
}
232249

internal/cmd/project/list/tree.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package list
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
8+
"golang.org/x/sync/errgroup"
9+
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
11+
12+
"github.com/stackitcloud/stackit-sdk-go/services/authorization"
13+
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
14+
)
15+
16+
type node struct {
17+
resourceID string
18+
name string
19+
20+
typ resourceType
21+
parent *node
22+
children []*node
23+
}
24+
25+
type resourceType string
26+
27+
const (
28+
resourceTypeOrg resourceType = "organization"
29+
resourceTypeFolder resourceType = "folder"
30+
resourceTypeProject resourceType = "project"
31+
)
32+
33+
type resourceTree struct {
34+
mu sync.Mutex
35+
36+
authClient *authorization.APIClient
37+
resourceClient *resourcemanager.APIClient
38+
member string
39+
roots map[string]*node
40+
}
41+
42+
func newResourceTree(resourceClient *resourcemanager.APIClient, authClient *authorization.APIClient, model *inputModel) (*resourceTree, error) {
43+
var member string
44+
if model.Member == nil {
45+
var err error
46+
member, err = auth.GetAuthEmail()
47+
if err != nil {
48+
return nil, fmt.Errorf("get email of authenticated user: %w", err)
49+
}
50+
} else {
51+
member = *model.Member
52+
}
53+
tree := &resourceTree{
54+
member: member,
55+
resourceClient: resourceClient,
56+
authClient: authClient,
57+
roots: map[string]*node{},
58+
}
59+
return tree, nil
60+
}
61+
62+
func (r *resourceTree) Fill(ctx context.Context) error {
63+
resp, err := r.authClient.ListUserMemberships(ctx, r.member).ResourceType("organization").Execute()
64+
if err != nil {
65+
return err
66+
}
67+
68+
g, ctx := errgroup.WithContext(ctx)
69+
for _, orgMembership := range resp.GetItems() {
70+
g.Go(func() error {
71+
org, err := r.resourceClient.GetOrganizationExecute(ctx, orgMembership.GetResourceId())
72+
if err != nil {
73+
return err
74+
}
75+
orgNode := &node{
76+
resourceID: org.GetOrganizationId(),
77+
name: org.GetName(),
78+
typ: resourceTypeOrg,
79+
}
80+
r.mu.Lock()
81+
r.roots[orgNode.resourceID] = orgNode
82+
r.mu.Unlock()
83+
if err := r.fillNode(ctx, orgNode); err != nil {
84+
return err
85+
}
86+
return nil
87+
})
88+
}
89+
return g.Wait()
90+
}
91+
92+
func (r *resourceTree) fillNode(ctx context.Context, parent *node) error {
93+
if err := r.getNodeProjects(ctx, parent); err != nil {
94+
return err
95+
}
96+
req := r.resourceClient.ListFolders(ctx).ContainerParentId(parent.resourceID)
97+
resp, err := req.Execute()
98+
if err != nil {
99+
if !isForbiddenError(err) {
100+
return err
101+
}
102+
// listing folder for parent was forbidden, trying with member
103+
resp, err = req.Member(r.member).Execute()
104+
if err != nil {
105+
return err
106+
}
107+
}
108+
g, ctx := errgroup.WithContext(ctx)
109+
for _, folder := range resp.GetItems() {
110+
g.Go(func() error {
111+
newFolderNode := &node{
112+
resourceID: folder.GetFolderId(),
113+
parent: parent,
114+
typ: resourceTypeFolder,
115+
name: folder.GetName(),
116+
}
117+
parent.children = append(parent.children, newFolderNode)
118+
return r.fillNode(ctx, newFolderNode)
119+
})
120+
}
121+
return g.Wait()
122+
}
123+
124+
func (r *resourceTree) getNodeProjects(ctx context.Context, parent *node) error {
125+
req := r.resourceClient.ListProjects(ctx).ContainerParentId(parent.resourceID)
126+
resp, err := req.Execute()
127+
if err != nil {
128+
if !isForbiddenError(err) {
129+
return err
130+
}
131+
// listing projects for parent was forbidden, trying with member
132+
resp, err = req.Member(r.member).Execute()
133+
if err != nil {
134+
return err
135+
}
136+
}
137+
for _, proj := range resp.GetItems() {
138+
projNode := &node{
139+
resourceID: proj.GetProjectId(),
140+
typ: resourceTypeProject,
141+
name: proj.GetName(),
142+
parent: parent,
143+
}
144+
parent.children = append(parent.children, projNode)
145+
}
146+
return nil
147+
}

0 commit comments

Comments
 (0)