Skip to content

Commit f06e1ca

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 98a09dd commit f06e1ca

File tree

4 files changed

+258
-71
lines changed

4 files changed

+258
-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: 87 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
package list
22

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

12+
"golang.org/x/sync/errgroup"
13+
814
"github.com/spf13/cobra"
915
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
1016
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
11-
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
1217
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
1318
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
1419
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
1520
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
1621
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
17-
"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"
1825
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
19-
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
26+
"github.com/stackitcloud/stackit-sdk-go/services/authorization"
2027
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
2128
)
2229

@@ -63,20 +70,24 @@ func NewCmd(params *params.CmdParams) *cobra.Command {
6370
"$ stackit project list --member example@email.com"),
6471
),
6572
RunE: func(cmd *cobra.Command, args []string) error {
66-
ctx := context.Background()
6773
model, err := parseInput(params.Printer, cmd, args)
6874
if err != nil {
6975
return err
7076
}
7177

7278
// Configure API client
73-
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)
7485
if err != nil {
7586
return err
7687
}
7788

7889
// Fetch projects
79-
projects, err := fetchProjects(ctx, model, apiClient)
90+
projects, err := fetchProjects(cmd.Context(), model, resourceClient, authClient)
8091
if err != nil {
8192
return err
8293
}
@@ -142,90 +153,97 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel,
142153
return &model, nil
143154
}
144155

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

160-
if model.ParentId == nil && model.ProjectIdLike == nil && model.Member == nil {
161-
email, err := auth.GetAuthEmail()
162-
if err != nil {
163-
return req, fmt.Errorf("get email of authenticated user: %w", err)
164-
}
165-
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+
})
166190
}
167-
req = req.Limit(float32(model.PageSize))
168-
req = req.Offset(float32(offset))
169-
return req, nil
191+
return g.Wait()
170192
}
171193

172194
type resourceManagerClient interface {
173195
ListProjects(ctx context.Context) resourcemanager.ApiListProjectsRequest
174196
}
175197

176-
func fetchProjects(ctx context.Context, model *inputModel, apiClient resourceManagerClient) ([]resourcemanager.Project, error) {
177-
if model.Limit != nil && *model.Limit < model.PageSize {
178-
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
179202
}
180203

181-
offset := 0
182-
projects := []resourcemanager.Project{}
183-
for {
184-
// Call API
185-
req, err := buildRequest(ctx, model, apiClient, offset)
186-
if err != nil {
187-
return nil, fmt.Errorf("build list projects request: %w", err)
188-
}
189-
resp, err := req.Execute()
190-
if err != nil {
191-
return nil, fmt.Errorf("get projects: %w", err)
192-
}
193-
respProjects := *resp.Items
194-
if len(respProjects) == 0 {
195-
break
196-
}
197-
projects = append(projects, respProjects...)
198-
// Stop if no more pages
199-
if len(respProjects) < int(model.PageSize) {
200-
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)
201223
}
224+
}()
202225

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

213-
func outputResult(p *print.Printer, outputFormat string, projects []resourcemanager.Project) error {
236+
func outputResult(p *print.Printer, outputFormat string, projects []project) error {
214237
return p.OutputResult(outputFormat, projects, func() error {
215238
table := tables.NewTable()
216-
table.SetHeader("ID", "NAME", "STATE", "PARENT ID")
239+
table.SetHeader("ORGANIZATION", "FOLDER", "NAME", "ID")
217240
for i := range projects {
218241
p := projects[i]
219-
220-
var parentId *string
221-
if p.Parent != nil {
222-
parentId = p.Parent.Id
223-
}
224242
table.AddRow(
225-
utils.PtrString(p.ProjectId),
226-
utils.PtrString(p.Name),
227-
utils.PtrString(p.LifecycleState),
228-
utils.PtrString(parentId),
243+
p.Organization,
244+
p.FolderPath(),
245+
p.Name,
246+
p.ID,
229247
)
230248
}
231249

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)