|
1 | 1 | package list |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "cmp" |
4 | 5 | "context" |
5 | 6 | "fmt" |
| 7 | + "path" |
| 8 | + "slices" |
| 9 | + "sync" |
6 | 10 | "time" |
7 | 11 |
|
8 | 12 | "github.com/stackitcloud/stackit-cli/internal/pkg/types" |
| 13 | + "golang.org/x/sync/errgroup" |
9 | 14 |
|
10 | 15 | "github.com/spf13/cobra" |
11 | 16 | "github.com/stackitcloud/stackit-cli/internal/pkg/args" |
12 | | - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" |
13 | 17 | "github.com/stackitcloud/stackit-cli/internal/pkg/errors" |
14 | 18 | "github.com/stackitcloud/stackit-cli/internal/pkg/examples" |
15 | 19 | "github.com/stackitcloud/stackit-cli/internal/pkg/flags" |
16 | 20 | "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" |
17 | 21 | "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" |
19 | 25 | "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" |
21 | 27 | "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" |
22 | 28 | ) |
23 | 29 |
|
@@ -64,20 +70,24 @@ func NewCmd(params *types.CmdParams) *cobra.Command { |
64 | 70 | "$ stackit project list --member example@email.com"), |
65 | 71 | ), |
66 | 72 | RunE: func(cmd *cobra.Command, args []string) error { |
67 | | - ctx := context.Background() |
68 | 73 | model, err := parseInput(params.Printer, cmd, args) |
69 | 74 | if err != nil { |
70 | 75 | return err |
71 | 76 | } |
72 | 77 |
|
73 | 78 | // 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) |
75 | 85 | if err != nil { |
76 | 86 | return err |
77 | 87 | } |
78 | 88 |
|
79 | 89 | // Fetch projects |
80 | | - projects, err := fetchProjects(ctx, model, apiClient) |
| 90 | + projects, err := fetchProjects(cmd.Context(), model, resourceClient, authClient) |
81 | 91 | if err != nil { |
82 | 92 | return err |
83 | 93 | } |
@@ -143,90 +153,97 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, |
143 | 153 | return &model, nil |
144 | 154 | } |
145 | 155 |
|
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 | +} |
160 | 162 |
|
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 | + }) |
167 | 190 | } |
168 | | - req = req.Limit(float32(model.PageSize)) |
169 | | - req = req.Offset(float32(offset)) |
170 | | - return req, nil |
| 191 | + return g.Wait() |
171 | 192 | } |
172 | 193 |
|
173 | 194 | type resourceManagerClient interface { |
174 | 195 | ListProjects(ctx context.Context) resourcemanager.ApiListProjectsRequest |
175 | 196 | } |
176 | 197 |
|
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 |
180 | 202 | } |
181 | 203 |
|
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) |
202 | 223 | } |
| 224 | + }() |
203 | 225 |
|
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 |
208 | 229 | } |
209 | | - offset += int(model.PageSize) |
210 | 230 | } |
211 | | - return projects, nil |
| 231 | + close(projChan) |
| 232 | + wg.Wait() |
| 233 | + return projs, nil |
212 | 234 | } |
213 | 235 |
|
214 | | -func outputResult(p *print.Printer, outputFormat string, projects []resourcemanager.Project) error { |
| 236 | +func outputResult(p *print.Printer, outputFormat string, projects []project) error { |
215 | 237 | return p.OutputResult(outputFormat, projects, func() error { |
216 | 238 | table := tables.NewTable() |
217 | | - table.SetHeader("ID", "NAME", "STATE", "PARENT ID") |
| 239 | + table.SetHeader("ORGANIZATION", "FOLDER", "NAME", "ID") |
218 | 240 | for i := range projects { |
219 | 241 | p := projects[i] |
220 | | - |
221 | | - var parentId *string |
222 | | - if p.Parent != nil { |
223 | | - parentId = p.Parent.Id |
224 | | - } |
225 | 242 | 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, |
230 | 247 | ) |
231 | 248 | } |
232 | 249 |
|
|
0 commit comments