• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

astronomer / astro-cli / 29863212487

21 Jul 2026 07:50PM UTC coverage: 43.885% (+0.002%) from 43.883%
29863212487

Pull #2210

github

web-flow
Merge 8802ef6b9 into 5d542ea0a
Pull Request #2210: fix(cli): paginate org/workspace/deployment lists past the first page

64 of 72 new or added lines in 4 files covered. (88.89%)

25900 of 59018 relevant lines covered (43.88%)

8.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.12
/cloud/workspace/workspace.go
1
package workspace
2

3
import (
4
        httpContext "context"
5
        "fmt"
6
        "io"
7
        "os"
8
        "strconv"
9

10
        "github.com/pkg/errors"
11

12
        "github.com/astronomer/astro-cli/astro-client-v1"
13
        "github.com/astronomer/astro-cli/cloud/pagination"
14
        "github.com/astronomer/astro-cli/config"
15
        "github.com/astronomer/astro-cli/context"
16
        "github.com/astronomer/astro-cli/pkg/ansi"
17
        "github.com/astronomer/astro-cli/pkg/input"
18
        "github.com/astronomer/astro-cli/pkg/output"
19
        "github.com/astronomer/astro-cli/pkg/printutil"
20
)
21

22
var (
23
        errInvalidWorkspaceKey = errors.New("invalid workspace selection")
24
        ErrInvalidName         = errors.New("no name provided for the workspace. Retry with a valid name")
25
        ErrInvalidTokenName    = errors.New("no name provided for the workspace token. Retry with a valid name")
26
        ErrWorkspaceNotFound   = errors.New("no workspace was found for the ID you provided")
27
        ErrNoWorkspaceExists   = errors.New("no workspace was found in your organization")
28
        ErrWrongEnforceInput   = errors.New("the input to the `--enforce-cicd` flag")
29
)
30

31
var workspaceTableConfig = output.BuildTableConfig(
32
        []output.Column[WorkspaceInfo]{
33
                {Header: "NAME", Value: func(w WorkspaceInfo) string { return w.Name }},
2✔
34
                {Header: "ID", Value: func(w WorkspaceInfo) string { return w.ID }},
2✔
35
        },
36
        func(d any) []WorkspaceInfo { return d.(*WorkspaceList).Workspaces },
2✔
37
        output.WithColorRow(func(w WorkspaceInfo) bool { return w.IsCurrent }, [2]string{"\033[1;32m", "\033[0m"}),
2✔
38
        output.WithPadding([]int{44, 50}),
39
)
40

41
// GetCurrentWorkspace gets the current workspace set in context config
42
// Returns a string representing the current workspace and an error if it doesn't exist
43
func GetCurrentWorkspace() (string, error) {
3✔
44
        c, err := config.GetCurrentContext()
3✔
45
        if err != nil {
4✔
46
                return "", err
1✔
47
        }
1✔
48

49
        if c.Workspace == "" {
3✔
50
                return "", errors.New("current workspace context not set, you can switch to a workspace with \n\tastro workspace switch WORKSPACEID")
1✔
51
        }
1✔
52

53
        return c.Workspace, nil
1✔
54
}
55

56
// ListData returns workspace list data for structured output
57
func ListData(client astrov1.APIClient) (*WorkspaceList, error) {
6✔
58
        c, err := config.GetCurrentContext()
6✔
59
        if err != nil {
6✔
60
                return nil, err
×
61
        }
×
62

63
        ws, err := GetWorkspaces(client)
6✔
64
        if err != nil {
8✔
65
                return nil, err
2✔
66
        }
2✔
67

68
        result := &WorkspaceList{
4✔
69
                Workspaces: make([]WorkspaceInfo, 0, len(ws)),
4✔
70
        }
4✔
71

4✔
72
        for i := range ws {
8✔
73
                isCurrent := c.Workspace == ws[i].Id
4✔
74
                result.Workspaces = append(result.Workspaces, WorkspaceInfo{
4✔
75
                        Name:      ws[i].Name,
4✔
76
                        ID:        ws[i].Id,
4✔
77
                        IsCurrent: isCurrent,
4✔
78
                })
4✔
79
        }
4✔
80

81
        return result, nil
4✔
82
}
83

84
// List all workspaces
85
func List(client astrov1.APIClient, out io.Writer) error {
2✔
86
        return ListWithFormat(client, output.FormatTable, "", out)
2✔
87
}
2✔
88

89
// ListWithFormat lists workspaces with the specified output format
90
func ListWithFormat(client astrov1.APIClient, format output.Format, tmpl string, out io.Writer) error {
4✔
91
        return output.PrintData(
4✔
92
                func() (*WorkspaceList, error) { return ListData(client) },
8✔
93
                workspaceTableConfig, format, tmpl, out,
94
        )
95
}
96

97
var GetWorkspaceSelection = func(client astrov1.APIClient, out io.Writer) (string, error) {
6✔
98
        tab := printutil.Table{
6✔
99
                Padding:        []int{5, 44, 50},
6✔
100
                DynamicPadding: true,
6✔
101
                Header:         []string{"#", "NAME", "ID"},
6✔
102
                ColorRowCode:   [2]string{"\033[1;32m", "\033[0m"},
6✔
103
        }
6✔
104

6✔
105
        var c config.Context
6✔
106
        c, err := config.GetCurrentContext()
6✔
107
        if err != nil {
7✔
108
                return "", err
1✔
109
        }
1✔
110

111
        ws, err := GetWorkspaces(client)
5✔
112
        if err != nil {
6✔
113
                return "", err
1✔
114
        }
1✔
115

116
        deployMap := map[string]astrov1.Workspace{}
4✔
117
        for i := range ws {
10✔
118
                index := i + 1
6✔
119

6✔
120
                color := c.Workspace == ws[i].Id
6✔
121
                tab.AddRow([]string{strconv.Itoa(index), ws[i].Name, ws[i].Id}, color)
6✔
122

6✔
123
                deployMap[strconv.Itoa(index)] = ws[i]
6✔
124
        }
6✔
125
        tab.Print(out)
4✔
126
        choice := input.Text("\n> ")
4✔
127
        selected, ok := deployMap[choice]
4✔
128
        if !ok {
6✔
129
                return "", errInvalidWorkspaceKey
2✔
130
        }
2✔
131

132
        return selected.Id, nil
2✔
133
}
134

135
func Switch(workspaceNameOrID string, client astrov1.APIClient, out io.Writer) error {
6✔
136
        var wsID string
6✔
137
        if workspaceNameOrID == "" {
8✔
138
                id, err := GetWorkspaceSelection(client, out)
2✔
139
                if err != nil {
3✔
140
                        return err
1✔
141
                }
1✔
142

143
                wsID = id
1✔
144
        } else {
4✔
145
                ws, err := GetWorkspaces(client)
4✔
146
                if err != nil {
6✔
147
                        return err
2✔
148
                }
2✔
149
                for i := range ws {
6✔
150
                        if ws[i].Name == workspaceNameOrID || ws[i].Id == workspaceNameOrID {
6✔
151
                                wsID = ws[i].Id
2✔
152
                        }
2✔
153
                }
154

155
                if wsID == "" {
2✔
NEW
156
                        return errors.New("workspace id/name could not be found")
×
157
                }
×
158
        }
159

160
        c, err := config.GetCurrentContext()
3✔
161
        if err != nil {
3✔
162
                return err
×
163
        }
×
164

165
        err = c.SetContextKey("workspace", wsID)
3✔
166
        if err != nil {
3✔
167
                return err
×
168
        }
×
169

170
        err = c.SetContextKey("last_used_workspace", wsID)
3✔
171
        if err != nil {
3✔
172
                return err
×
173
        }
×
174

175
        err = c.SetOrganizationContext(c.Organization, c.OrganizationProduct)
3✔
176
        if err != nil {
3✔
177
                return err
×
178
        }
×
179

180
        err = config.PrintCurrentCloudContext(out)
3✔
181
        if err != nil {
3✔
182
                return err
×
183
        }
×
184

185
        return nil
3✔
186
}
187

188
func validateEnforceCD(enforceCD string) (bool, error) {
6✔
189
        var enforce bool
6✔
190
        switch {
6✔
191
        case enforceCD == "OFF" || enforceCD == "":
×
192
                enforce = false
×
193
        case enforceCD == "ON":
5✔
194
                enforce = true
5✔
195
        default:
1✔
196
                return false, ErrWrongEnforceInput
1✔
197
        }
198
        return enforce, nil
5✔
199
}
200

201
func Create(name, description, enforceCD string, out io.Writer, client astrov1.APIClient) error {
5✔
202
        if name == "" {
5✔
203
                return ErrInvalidName
×
204
        }
×
205
        ctx, err := context.GetCurrentContext()
5✔
206
        if err != nil {
6✔
207
                return err
1✔
208
        }
1✔
209
        enforce, err := validateEnforceCD(enforceCD)
4✔
210
        if err != nil {
5✔
211
                return err
1✔
212
        }
1✔
213
        workspaceCreateRequest := astrov1.CreateWorkspaceJSONRequestBody{
3✔
214
                CicdEnforcedDefault: &enforce,
3✔
215
                Description:         &description,
3✔
216
                Name:                name,
3✔
217
        }
3✔
218
        resp, err := client.CreateWorkspaceWithResponse(httpContext.Background(), ctx.Organization, workspaceCreateRequest)
3✔
219
        if err != nil {
4✔
220
                return err
1✔
221
        }
1✔
222
        err = astrov1.NormalizeAPIError(resp.HTTPResponse, resp.Body)
2✔
223
        if err != nil {
3✔
224
                return err
1✔
225
        }
1✔
226
        fmt.Fprintf(out, "Astro Workspace %s was successfully created\n", name)
1✔
227
        return nil
1✔
228
}
229

230
func Update(id, name, description, enforceCD string, out io.Writer, client astrov1.APIClient) error {
7✔
231
        ctx, err := context.GetCurrentContext()
7✔
232
        if err != nil {
8✔
233
                return err
1✔
234
        }
1✔
235
        workspaces, err := GetWorkspaces(client)
6✔
236
        if err != nil {
6✔
237
                return err
×
238
        }
×
239
        var workspace astrov1.Workspace
6✔
240
        if id == "" {
8✔
241
                workspace, err = selectWorkspace(workspaces)
2✔
242
                if workspace.Id == "" {
3✔
243
                        return ErrNoWorkspaceExists
1✔
244
                }
1✔
245
                if err != nil {
1✔
246
                        return err
×
247
                }
×
248
        } else {
4✔
249
                for i := range workspaces {
12✔
250
                        if workspaces[i].Id == id {
12✔
251
                                workspace = workspaces[i]
4✔
252
                        }
4✔
253
                }
254
                if workspace.Id == "" {
4✔
255
                        return ErrWorkspaceNotFound
×
256
                }
×
257
        }
258
        workspaceID := workspace.Id
5✔
259

5✔
260
        workspaceUpdateRequest := astrov1.UpdateWorkspaceRequest{}
5✔
261

5✔
262
        if name == "" {
8✔
263
                workspaceUpdateRequest.Name = workspace.Name
3✔
264
        } else {
5✔
265
                workspaceUpdateRequest.Name = name
2✔
266
        }
2✔
267

268
        if description == "" {
8✔
269
                if workspace.Description != nil {
6✔
270
                        workspaceUpdateRequest.Description = *workspace.Description
3✔
271
                }
3✔
272
        } else {
2✔
273
                workspaceUpdateRequest.Description = description
2✔
274
        }
2✔
275
        if enforceCD == "" {
8✔
276
                workspaceUpdateRequest.CicdEnforcedDefault = workspace.CicdEnforcedDefault
3✔
277
        } else {
5✔
278
                enforce, err := validateEnforceCD(enforceCD)
2✔
279
                if err != nil {
2✔
280
                        return err
×
281
                }
×
282
                workspaceUpdateRequest.CicdEnforcedDefault = enforce
2✔
283
        }
284
        resp, err := client.UpdateWorkspaceWithResponse(httpContext.Background(), ctx.Organization, workspaceID, workspaceUpdateRequest)
5✔
285
        if err != nil {
6✔
286
                return err
1✔
287
        }
1✔
288
        err = astrov1.NormalizeAPIError(resp.HTTPResponse, resp.Body)
4✔
289
        if err != nil {
5✔
290
                return err
1✔
291
        }
1✔
292
        fmt.Fprintf(out, "Astro Workspace %s was successfully updated\n", workspace.Name)
3✔
293
        return nil
3✔
294
}
295

296
func Delete(id string, out io.Writer, client astrov1.APIClient) error {
6✔
297
        ctx, err := context.GetCurrentContext()
6✔
298
        if err != nil {
7✔
299
                return err
1✔
300
        }
1✔
301
        workspaces, err := GetWorkspaces(client)
5✔
302
        if err != nil {
5✔
303
                return err
×
304
        }
×
305
        var workspace astrov1.Workspace
5✔
306
        if id == "" {
7✔
307
                workspace, err = selectWorkspace(workspaces)
2✔
308
                if workspace.Id == "" {
3✔
309
                        return ErrNoWorkspaceExists
1✔
310
                }
1✔
311
                if err != nil {
1✔
312
                        return err
×
313
                }
×
314
        } else {
3✔
315
                for i := range workspaces {
6✔
316
                        if workspaces[i].Id == id {
6✔
317
                                workspace = workspaces[i]
3✔
318
                        }
3✔
319
                }
320
                if workspace.Id == "" {
3✔
321
                        return ErrWorkspaceNotFound
×
322
                }
×
323
        }
324
        workspaceID := workspace.Id
4✔
325
        resp, err := client.DeleteWorkspaceWithResponse(httpContext.Background(), ctx.Organization, workspaceID)
4✔
326
        if err != nil {
5✔
327
                return err
1✔
328
        }
1✔
329
        err = astrov1.NormalizeAPIError(resp.HTTPResponse, resp.Body)
3✔
330
        if err != nil {
4✔
331
                return err
1✔
332
        }
1✔
333
        fmt.Fprintf(out, "Astro Workspace %s was successfully deleted\n", workspace.Name)
2✔
334
        return nil
2✔
335
}
336

337
func selectWorkspace(workspaces []astrov1.Workspace) (astrov1.Workspace, error) {
4✔
338
        if len(workspaces) == 0 {
6✔
339
                return astrov1.Workspace{}, nil
2✔
340
        }
2✔
341

342
        if len(workspaces) == 1 {
3✔
343
                fmt.Println("Only one Workspace was found. Using the following Workspace by default: \n" +
1✔
344
                        fmt.Sprintf("\n Workspace Name: %s", ansi.Bold(workspaces[0].Name)) +
1✔
345
                        fmt.Sprintf("\n Workspace ID: %s\n", ansi.Bold(workspaces[0].Id)))
1✔
346

1✔
347
                return workspaces[0], nil
1✔
348
        }
1✔
349

350
        table := printutil.Table{
1✔
351
                Padding:        []int{30, 50, 10, 50, 10, 10, 10},
1✔
352
                DynamicPadding: true,
1✔
353
                Header:         []string{"#", "WORKSPACENAME", "ID", "CICD ENFORCEMENT"},
1✔
354
        }
1✔
355

1✔
356
        fmt.Println("\nPlease select the workspace you would like to update:")
1✔
357

1✔
358
        workspaceMap := map[string]astrov1.Workspace{}
1✔
359
        for i := range workspaces {
3✔
360
                index := i + 1
2✔
361
                table.AddRow([]string{
2✔
362
                        strconv.Itoa(index),
2✔
363
                        workspaces[i].Name,
2✔
364
                        workspaces[i].Id,
2✔
365
                        strconv.FormatBool(workspaces[i].CicdEnforcedDefault),
2✔
366
                }, false)
2✔
367
                workspaceMap[strconv.Itoa(index)] = workspaces[i]
2✔
368
        }
2✔
369

370
        table.Print(os.Stdout)
1✔
371
        choice := input.Text("\n> ")
1✔
372
        selected, ok := workspaceMap[choice]
1✔
373
        if !ok {
1✔
374
                return astrov1.Workspace{}, errInvalidWorkspaceKey
×
375
        }
×
376
        return selected, nil
1✔
377
}
378

379
// GetWorkspaces returns every Workspace in the current Organization, paging
380
// through the API as needed.
381
func GetWorkspaces(client astrov1.APIClient) ([]astrov1.Workspace, error) {
27✔
382
        ctx, err := context.GetCurrentContext()
27✔
383
        if err != nil {
28✔
384
                return []astrov1.Workspace{}, err
1✔
385
        }
1✔
386

387
        sorts := []astrov1.ListWorkspacesParamsSorts{"name:asc"}
26✔
388
        return pagination.Collect("workspaces", func(offset int) ([]astrov1.Workspace, int, error) {
54✔
389
                pageSize := 1000
28✔
390
                params := &astrov1.ListWorkspacesParams{Limit: &pageSize, Offset: &offset, Sorts: &sorts}
28✔
391
                resp, err := client.ListWorkspacesWithResponse(httpContext.Background(), ctx.Organization, params)
28✔
392
                if err != nil {
32✔
393
                        return nil, 0, err
4✔
394
                }
4✔
395
                if err := astrov1.NormalizeAPIError(resp.HTTPResponse, resp.Body); err != nil {
24✔
NEW
396
                        return nil, 0, err
×
NEW
397
                }
×
398
                return resp.JSON200.Workspaces, resp.JSON200.TotalCount, nil
24✔
399
        })
400
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc