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

astronomer / astro-cli / 29108020872

10 Jul 2026 04:37PM UTC coverage: 43.778% (+0.1%) from 43.672%
29108020872

Pull #2205

github

web-flow
Merge 515fbf36e into 12c10591a
Pull Request #2205: Expand `astro api`: full `describe` fields + `--json` for describe & ls

190 of 245 new or added lines in 10 files covered. (77.55%)

1 existing line in 1 file now uncovered.

25748 of 58815 relevant lines covered (43.78%)

8.3 hits per line

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

64.45
/cmd/api/cloud.go
1
package api
2

3
import (
4
        "fmt"
5
        "io"
6
        "os"
7
        "path/filepath"
8
        "regexp"
9
        "strings"
10

11
        "github.com/spf13/cobra"
12

13
        "github.com/astronomer/astro-cli/config"
14
        "github.com/astronomer/astro-cli/context"
15
        "github.com/astronomer/astro-cli/pkg/ansi"
16
        "github.com/astronomer/astro-cli/pkg/domainutil"
17
        "github.com/astronomer/astro-cli/pkg/openapi"
18
)
19

20
// CloudOptions holds all options for the cloud api command.
21
type CloudOptions struct {
22
        RequestOptions
23
        SpecURL         string // hidden flag: alternative OpenAPI spec URL
24
        SpecTokenEnvVar string // hidden flag: env var name containing auth token for spec fetch
25
}
26

27
// NewCloudCmd creates the 'astro api cloud' command.
28
//
29
//nolint:dupl
30
func NewCloudCmd(out io.Writer) *cobra.Command {
12✔
31
        opts := &CloudOptions{
12✔
32
                RequestOptions: RequestOptions{
12✔
33
                        Out:             out,
12✔
34
                        ErrOut:          os.Stderr,
12✔
35
                        TotalCountField: "totalCount",
12✔
36
                        // specCache is initialized lazily when the domain is known
12✔
37
                },
12✔
38
        }
12✔
39

12✔
40
        cmd := &cobra.Command{
12✔
41
                Use:   "cloud <endpoint | operation-id>",
12✔
42
                Short: "Make authenticated requests to the Astro Cloud API",
12✔
43
                Long: `Make authenticated HTTP requests to the Astro Cloud API (api.astronomer.io).
12✔
44

12✔
45
The argument can be either:
12✔
46
  - A path of an Astro Cloud API endpoint (e.g., /organizations/{organizationId})
12✔
47
  - An operation ID from the API spec (e.g., GetDeployment, ListOrganizations)
12✔
48

12✔
49
Placeholder values {organizationId} and {workspaceId} will be replaced
12✔
50
with values from the current context. Other path parameters can be provided
12✔
51
using the -p/--path-param flag.
12✔
52

12✔
53
The default HTTP request method is GET normally and POST if any parameters
12✔
54
were added. Override the method with --method. When using an operation ID,
12✔
55
the method is auto-detected from the API spec.
12✔
56

12✔
57
Pass one or more -f/--raw-field values in key=value format to add static string
12✔
58
parameters to the request payload. To add non-string or placeholder-determined
12✔
59
values, see -F/--field below.
12✔
60

12✔
61
The -F/--field flag has magic type conversion based on the format of the value:
12✔
62
  - literal values true, false, null, and integer numbers get converted to
12✔
63
    appropriate JSON types;
12✔
64
  - if the value starts with @, the rest of the value is interpreted as a
12✔
65
    filename to read the value from. Pass - to read from standard input.
12✔
66

12✔
67
To pass nested parameters in the request payload, use key[subkey]=value syntax.
12✔
68
To pass nested values as arrays, declare multiple fields with key[]=value1.`,
12✔
69
                Example: `  # List all Cloud API endpoints
12✔
70
  astro api cloud ls
12✔
71

12✔
72
  # Get organization details (auto-injects organizationId)
12✔
73
  astro api cloud /organizations/{organizationId}
12✔
74

12✔
75
  # Use operation ID with path parameters
12✔
76
  astro api cloud GetDeployment -p deploymentId=abc123
12✔
77

12✔
78
  # Use operation ID (organizationId auto-injected from context)
12✔
79
  astro api cloud ListDeployments
12✔
80

12✔
81
  # List deployments with jq filter
12✔
82
  astro api cloud /organizations/{organizationId}/deployments --jq '.[].name'
12✔
83

12✔
84
  # Create a resource with typed fields
12✔
85
  astro api cloud -X POST /organizations/{organizationId}/workspaces \
12✔
86
    -F name=my-workspace \
12✔
87
    -F description="My new workspace"
12✔
88

12✔
89
  # Use Go template for output
12✔
90
  astro api cloud /organizations/{organizationId}/deployments \
12✔
91
    --template '{{range .}}{{.name}} ({{.status}}){{"\n"}}{{end}}'
12✔
92

12✔
93
  # Generate curl command instead of executing
12✔
94
  astro api cloud /organizations/{organizationId}/deployments --generate
12✔
95

12✔
96
  # Include response headers
12✔
97
  astro api cloud /organizations/{organizationId} -i
12✔
98

12✔
99
  # Verbose mode showing full request/response
12✔
100
  astro api cloud /organizations/{organizationId} --verbose`,
12✔
101
                Args: cobra.MaximumNArgs(1),
12✔
102
                PreRunE: func(cmd *cobra.Command, args []string) error {
12✔
103
                        opts.RequestMethodPassed = cmd.Flags().Changed("method")
×
104
                        return nil
×
105
                },
×
106
                RunE: func(cmd *cobra.Command, args []string) error {
×
107
                        if len(args) == 0 {
×
108
                                // Interactive mode - show endpoint selection
×
109
                                return runCloudInteractive(opts)
×
110
                        }
×
111
                        opts.RequestPath = args[0]
×
112
                        return runCloud(opts)
×
113
                },
114
        }
115

116
        // Request flags
117
        cmd.Flags().StringVarP(&opts.RequestMethod, "method", "X", "GET", "The HTTP method for the request")
12✔
118
        cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a typed parameter in key=value format")
12✔
119
        cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter in key=value format")
12✔
120
        cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add a HTTP request header in key:value format")
12✔
121
        cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The file to use as body for the HTTP request (use \"-\" for stdin)")
12✔
122
        cmd.Flags().StringArrayVarP(&opts.PathParams, "path-param", "p", nil, "Path parameter in key=value format (for use with operation IDs)")
12✔
123

12✔
124
        // Output flags
12✔
125
        cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response status line and headers in the output")
12✔
126
        cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results")
12✔
127
        cmd.Flags().BoolVar(&opts.Slurp, "slurp", false, "Use with --paginate to return an array of all pages")
12✔
128
        cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Do not print the response body")
12✔
129
        cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format JSON output using a Go template")
12✔
130
        cmd.Flags().StringVarP(&opts.FilterOutput, "jq", "q", "", "Query to select values from the response using jq syntax")
12✔
131
        cmd.Flags().BoolVar(&opts.Verbose, "verbose", false, "Include full HTTP request and response in the output")
12✔
132

12✔
133
        // Other flags
12✔
134
        cmd.Flags().BoolVar(&opts.GenerateCurl, "generate", false, "Output a curl command instead of executing the request")
12✔
135
        cmd.PersistentFlags().StringVar(&opts.SpecURL, "spec-url", "", "OpenAPI spec URL or file path (overrides default Cloud API spec)")
12✔
136
        cmd.PersistentFlags().StringVar(&opts.SpecTokenEnvVar, "spec-token-env-var", "", "Environment variable containing auth token for fetching the spec")
12✔
137
        //nolint:errcheck
12✔
138
        cmd.PersistentFlags().MarkHidden("spec-url")
12✔
139
        //nolint:errcheck
12✔
140
        cmd.PersistentFlags().MarkHidden("spec-token-env-var")
12✔
141

12✔
142
        // Add list and describe subcommands (cloud-specific to support lazy cache init)
12✔
143
        cmd.AddCommand(NewCloudListCmd(out, opts))
12✔
144
        cmd.AddCommand(NewCloudDescribeCmd(out, opts))
12✔
145

12✔
146
        return cmd
12✔
147
}
148

149
// runCloud executes the cloud API request.
150
func runCloud(opts *CloudOptions) error {
×
151
        // Check if we're in a cloud context
×
152
        if !context.IsCloudContext() {
×
153
                return fmt.Errorf("the 'astro api cloud' command is only available in cloud context. Run 'astro login' to connect to Astro Cloud")
×
154
        }
×
155

156
        // Get current context for auth and placeholders
157
        ctx, err := context.GetCurrentContext()
×
158
        if err != nil {
×
159
                return fmt.Errorf("getting current context: %w", err)
×
160
        }
×
161

162
        // Check for token
163
        if ctx.Token == "" {
×
164
                return fmt.Errorf("not authenticated. Run 'astro login' to authenticate")
×
165
        }
×
166

167
        // Initialize the spec cache for this domain
168
        if err := initCloudSpecCache(opts, &ctx); err != nil {
×
169
                return fmt.Errorf("initializing API spec: %w", err)
×
170
        }
×
171

172
        // Resolve operation ID to path if needed
173
        requestPath := opts.RequestPath
×
174
        method := opts.RequestMethod
×
175
        methodFromSpec := false
×
176

×
177
        if isOperationID(requestPath) {
×
178
                endpoint, err := resolveOperationID(opts.specCache, requestPath, "cloud")
×
179
                if err != nil {
×
180
                        return err
×
181
                }
×
182
                requestPath = endpoint.Path
×
183
                if !opts.RequestMethodPassed {
×
184
                        method = endpoint.Method
×
185
                        methodFromSpec = true
×
186
                }
×
187
        }
188

189
        // Apply path params from flags
190
        requestPath, err = applyPathParams(requestPath, opts.PathParams)
×
191
        if err != nil {
×
192
                return fmt.Errorf("applying path params: %w", err)
×
193
        }
×
194

195
        // Fill context placeholders in the path
196
        requestPath, err = fillPlaceholders(requestPath, &ctx)
×
197
        if err != nil {
×
198
                return fmt.Errorf("filling placeholders: %w", err)
×
199
        }
×
200

201
        // Check for any remaining unfilled path parameters
202
        if missing := findMissingPathParams(requestPath); len(missing) > 0 {
×
203
                return fmt.Errorf("missing path parameter(s): %s. Use -p/--path-param to provide them (e.g., -p %s=value)",
×
204
                        strings.Join(missing, ", "), missing[0])
×
205
        }
×
206

207
        // Parse fields into request body
208
        params, err := parseFields(opts.MagicFields, opts.RawFields)
×
209
        if err != nil {
×
210
                return fmt.Errorf("parsing fields: %w", err)
×
211
        }
×
212

213
        // Determine HTTP method (only override if not from spec and not explicitly passed)
214
        if !methodFromSpec && !opts.RequestMethodPassed && (len(params) > 0 || opts.RequestInputFile != "") {
×
215
                method = "POST"
×
216
        }
×
217

218
        // Build the full URL using the domain-derived base URL
219
        var baseURL string
×
220
        if opts.SpecURL != "" {
×
221
                if err := opts.specCache.Load(false); err != nil {
×
222
                        return fmt.Errorf("loading spec: %w", err)
×
223
                }
×
224
                serverPath := opts.specCache.GetServerPath()
×
225
                if serverPath == "" {
×
226
                        return fmt.Errorf("spec has no servers/basePath; cannot determine base URL")
×
227
                }
×
228
                baseURL = ctx.GetPublicRESTAPIURL(strings.TrimPrefix(serverPath, "/"))
×
229
        } else {
×
230
                baseURL = ctx.GetPublicRESTAPIURL("v1")
×
231
        }
×
232
        url := buildURL(baseURL, requestPath)
×
233

×
234
        // Generate curl command if requested
×
235
        if opts.GenerateCurl {
×
236
                return generateCurl(opts.Out, method, url, ctx.Token, opts.RequestHeaders, params, opts.RequestInputFile)
×
237
        }
×
238

239
        // Build and execute the request
240
        return executeRequest(&opts.RequestOptions, method, url, ctx.Token, params)
×
241
}
242

243
// isOperationID checks if the input looks like an operation ID rather than a path.
244
// Operation IDs don't contain "/" and typically are CamelCase or camelCase.
245
func isOperationID(input string) bool {
23✔
246
        return !strings.Contains(input, "/")
23✔
247
}
23✔
248

249
// resolveOperationID looks up an operation ID in the OpenAPI spec and returns the endpoint.
250
// If no exact operation ID match is found, it falls back to trying the input as a path
251
// (with "/" prepended) to handle bare path segments like "version" or "health".
252
// cmdName is used only in the error message (e.g. "cloud", "airflow").
253
func resolveOperationID(specCache *openapi.Cache, operationID, cmdName string) (*openapi.Endpoint, error) {
8✔
254
        if err := specCache.Load(false); err != nil {
8✔
255
                return nil, fmt.Errorf("loading OpenAPI spec: %w", err)
×
256
        }
×
257

258
        endpoints := specCache.GetEndpoints()
8✔
259

8✔
260
        // Try as operation ID first
8✔
261
        endpoint := openapi.FindEndpointByOperationID(endpoints, operationID)
8✔
262
        if endpoint != nil {
13✔
263
                return endpoint, nil
5✔
264
        }
5✔
265

266
        // Fall back to trying as a path (e.g., "version" -> "/version")
267
        pathMatches := openapi.FindEndpointByPath(endpoints, "/"+operationID)
3✔
268
        if len(pathMatches) > 0 {
4✔
269
                return &pathMatches[0], nil
1✔
270
        }
1✔
271

272
        return nil, fmt.Errorf("'%s' not found as operation ID or path. Use 'astro api %s ls' to see available endpoints", operationID, cmdName)
2✔
273
}
274

275
// applyPathParams replaces path placeholders with values from --path-param flags.
276
func applyPathParams(path string, pathParams []string) (string, error) {
21✔
277
        if len(pathParams) == 0 {
37✔
278
                return path, nil
16✔
279
        }
16✔
280

281
        // Parse path params into a map
282
        params := make(map[string]string)
5✔
283
        for _, p := range pathParams {
11✔
284
                parts := strings.SplitN(p, "=", 2)
6✔
285
                if len(parts) != 2 {
7✔
286
                        return "", fmt.Errorf("invalid path param format '%s', expected key=value", p)
1✔
287
                }
1✔
288
                params[parts[0]] = parts[1]
5✔
289
        }
290

291
        // Replace placeholders in the path
292
        result := placeholderRE.ReplaceAllStringFunc(path, func(match string) string {
10✔
293
                name := match[1 : len(match)-1]
6✔
294
                if val, ok := params[name]; ok {
11✔
295
                        return val
5✔
296
                }
5✔
297
                return match
1✔
298
        })
299

300
        return result, nil
4✔
301
}
302

303
// runCloudInteractive runs the cloud API command in interactive mode.
304
func runCloudInteractive(opts *CloudOptions) error {
×
305
        // Check if we're in a cloud context
×
306
        if !context.IsCloudContext() {
×
307
                return fmt.Errorf("the 'astro api cloud' command is only available in cloud context. Run 'astro login' to connect to Astro Cloud")
×
308
        }
×
309

310
        // Get current context for cache initialization
311
        ctx, err := context.GetCurrentContext()
×
312
        if err != nil {
×
313
                return fmt.Errorf("getting current context: %w", err)
×
314
        }
×
315

316
        // Initialize the spec cache for this domain
317
        if err := initCloudSpecCache(opts, &ctx); err != nil {
×
318
                return fmt.Errorf("initializing API spec: %w", err)
×
319
        }
×
320

321
        // Load OpenAPI spec
322
        if err := opts.specCache.Load(false); err != nil {
×
323
                return fmt.Errorf("loading OpenAPI spec: %w", err)
×
324
        }
×
325

326
        endpoints := opts.specCache.GetEndpoints()
×
327
        if len(endpoints) == 0 {
×
328
                return fmt.Errorf("no endpoints found in API specification")
×
329
        }
×
330

331
        // Show endpoint selection
332
        fmt.Fprintf(opts.Out, "\nFound %d endpoints. Use '%s' to list them.\n", len(endpoints), ansi.Bold("astro api cloud ls"))
×
333
        fmt.Fprintf(opts.Out, "Run '%s' to make a request.\n\n", ansi.Bold("astro api cloud <endpoint>"))
×
334

×
335
        return nil
×
336
}
337

338
// placeholderRE matches placeholders like {organizationId}, {workspaceId}, {dag_id}, etc.
339
var placeholderRE = regexp.MustCompile(`\{([a-zA-Z][a-zA-Z0-9_]*)\}`)
340

341
// fillPlaceholders replaces placeholders with values from the context.
342
func fillPlaceholders(path string, ctx *config.Context) (string, error) {
8✔
343
        var errs []string
8✔
344

8✔
345
        result := placeholderRE.ReplaceAllStringFunc(path, func(match string) string {
16✔
346
                // Extract the name without braces
8✔
347
                name := match[1 : len(match)-1]
8✔
348

8✔
349
                switch strings.ToLower(name) {
8✔
350
                case "organizationid":
4✔
351
                        if ctx.Organization == "" {
5✔
352
                                errs = append(errs, "organizationId not set in context (run 'astro organization switch')")
1✔
353
                                return match
1✔
354
                        }
1✔
355
                        return ctx.Organization
3✔
356
                case "workspaceid":
3✔
357
                        if ctx.Workspace == "" {
4✔
358
                                errs = append(errs, "workspaceId not set in context (run 'astro workspace switch')")
1✔
359
                                return match
1✔
360
                        }
1✔
361
                        return ctx.Workspace
2✔
362
                default:
1✔
363
                        // Keep unknown placeholders as-is (user might provide them)
1✔
364
                        return match
1✔
365
                }
366
        })
367

368
        if len(errs) > 0 {
10✔
369
                return result, fmt.Errorf("placeholder error: %s", strings.Join(errs, "; "))
2✔
370
        }
2✔
371

372
        return result, nil
6✔
373
}
374

375
// findMissingPathParams returns any unfilled path parameters in the path.
376
func findMissingPathParams(path string) []string {
20✔
377
        matches := placeholderRE.FindAllStringSubmatch(path, -1)
20✔
378
        if len(matches) == 0 {
37✔
379
                return nil
17✔
380
        }
17✔
381

382
        missing := make([]string, 0, len(matches))
3✔
383
        for _, match := range matches {
7✔
384
                if len(match) > 1 {
8✔
385
                        missing = append(missing, match[1])
4✔
386
                }
4✔
387
        }
388
        return missing
3✔
389
}
390

391
// initCloudSpecCache initializes the OpenAPI spec cache for the current cloud
392
// context's domain. If the cache is already initialized, this is a no-op.
393
func initCloudSpecCache(opts *CloudOptions, ctx *config.Context) error {
11✔
394
        if opts.specCache != nil {
12✔
395
                return nil
1✔
396
        }
1✔
397

398
        // When --spec-url is provided, use it. It can be a remote URL or a local file path.
399
        if opts.SpecURL != "" {
18✔
400
                // Local file: read directly from disk, no caching or auth needed.
8✔
401
                if openapi.IsLocalSpec(opts.SpecURL) {
11✔
402
                        localPath, err := openapi.ResolveLocalPath(opts.SpecURL)
3✔
403
                        if err != nil {
3✔
404
                                return fmt.Errorf("resolving spec path: %w", err)
×
405
                        }
×
406
                        opts.specCache = openapi.NewCacheForLocalFile(localPath)
3✔
407
                        return nil
3✔
408
                }
409

410
                // Remote URL: cache with optional auth via --spec-token-env-var.
411
                cachePath := filepath.Join(config.HomeConfigPath, openapi.SpecCacheFileName(opts.SpecURL))
5✔
412
                if opts.SpecTokenEnvVar != "" {
8✔
413
                        token := os.Getenv(opts.SpecTokenEnvVar)
3✔
414
                        if token == "" {
4✔
415
                                return fmt.Errorf("environment variable %q is not set", opts.SpecTokenEnvVar)
1✔
416
                        }
1✔
417
                        opts.specCache = openapi.NewCacheWithAuth(opts.SpecURL, cachePath, token)
2✔
418
                } else {
2✔
419
                        opts.specCache = openapi.NewCacheWithOptions(opts.SpecURL, cachePath)
2✔
420
                }
2✔
421
                return nil
4✔
422
        }
423

424
        domain := domainutil.FormatDomain(ctx.Domain)
2✔
425
        specURL := ctx.GetPublicRESTAPIURL("spec/v1.0")
2✔
426
        if specURL == "" {
2✔
427
                return fmt.Errorf("could not determine API spec URL for domain %q. Check your login context", ctx.Domain)
×
428
        }
×
429
        cachePath := filepath.Join(config.HomeConfigPath, openapi.CloudCacheFileNameForDomain(domain))
2✔
430
        opts.specCache = openapi.NewCacheWithOptions(specURL, cachePath)
2✔
431
        return nil
2✔
432
}
433

434
// NewCloudListCmd creates the 'astro api cloud ls' command.
435
// It lazily initializes the spec cache so the correct domain-specific spec URL is used.
436
func NewCloudListCmd(out io.Writer, parentOpts *CloudOptions) *cobra.Command {
12✔
437
        var verbose bool
12✔
438
        var refresh bool
12✔
439
        var jsonOut bool
12✔
440

12✔
441
        cmd := &cobra.Command{
12✔
442
                Use:     "ls [filter]",
12✔
443
                Aliases: []string{"list"},
12✔
444
                Short:   "List available Astro Cloud API endpoints",
12✔
445
                Long: `List all available endpoints from the Astro Cloud API.
12✔
446

12✔
447
You can optionally provide a filter to search for specific endpoints.
12✔
448
The filter matches against endpoint paths, methods, operation IDs, summaries, and tags.`,
12✔
449
                Example: `  # List all endpoints
12✔
450
  astro api cloud ls
12✔
451

12✔
452
  # Filter endpoints
12✔
453
  astro api cloud ls deployments
12✔
454

12✔
455
  # List POST endpoints
12✔
456
  astro api cloud ls POST
12✔
457

12✔
458
  # Show verbose output with descriptions
12✔
459
  astro api cloud ls --verbose`,
12✔
460
                Args: cobra.MaximumNArgs(1),
12✔
461
                RunE: func(cmd *cobra.Command, args []string) error {
12✔
462
                        var filter string
×
463
                        if len(args) > 0 {
×
464
                                filter = args[0]
×
465
                        }
×
466

467
                        if !context.IsCloudContext() {
×
468
                                return fmt.Errorf("the 'astro api cloud' command is only available in cloud context. Run 'astro login' to connect to Astro Cloud")
×
469
                        }
×
470

471
                        ctx, err := context.GetCurrentContext()
×
472
                        if err != nil {
×
473
                                return fmt.Errorf("getting current context: %w", err)
×
474
                        }
×
475

476
                        if err := initCloudSpecCache(parentOpts, &ctx); err != nil {
×
477
                                return fmt.Errorf("initializing API spec: %w", err)
×
478
                        }
×
479

480
                        listOpts := &ListOptions{
×
481
                                Out:       out,
×
482
                                specCache: parentOpts.specCache,
×
483
                                Filter:    filter,
×
484
                                Verbose:   verbose,
×
485
                                Refresh:   refresh,
×
NEW
486
                                JSON:      jsonOut,
×
487
                        }
×
488
                        return runList(listOpts)
×
489
                },
490
        }
491

492
        cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show additional details like summaries and tags")
12✔
493
        cmd.Flags().BoolVar(&refresh, "refresh", false, "Force refresh of the OpenAPI specification cache")
12✔
494
        cmd.Flags().BoolVar(&jsonOut, "json", false, "Output the endpoint list as JSON")
12✔
495

12✔
496
        return cmd
12✔
497
}
498

499
// NewCloudDescribeCmd creates the 'astro api cloud describe' command.
500
// It lazily initializes the spec cache so the correct domain-specific spec URL is used.
501
func NewCloudDescribeCmd(out io.Writer, parentOpts *CloudOptions) *cobra.Command {
12✔
502
        var method string
12✔
503
        var refresh bool
12✔
504
        var verbose bool
12✔
505
        var jsonOut bool
12✔
506

12✔
507
        cmd := &cobra.Command{
12✔
508
                Use:   "describe <endpoint>",
12✔
509
                Short: "Describe an Astro Cloud API endpoint's request and response schema",
12✔
510
                Long: `Show detailed information about an Astro Cloud API endpoint, including:
12✔
511
- Path and query parameters
12✔
512
- Request body schema (for POST/PUT/PATCH)
12✔
513
- Response schema
12✔
514

12✔
515
The endpoint can be specified as a path or as an operation ID.`,
12✔
516
                Example: `  # Describe an endpoint by path
12✔
517
  astro api cloud describe /organizations/{organizationId}/deployments
12✔
518

12✔
519
  # Describe a POST endpoint specifically
12✔
520
  astro api cloud describe /organizations/{organizationId}/deployments -X POST
12✔
521

12✔
522
  # Describe by operation ID
12✔
523
  astro api cloud describe CreateDeployment`,
12✔
524
                Args: cobra.ExactArgs(1),
12✔
525
                RunE: func(cmd *cobra.Command, args []string) error {
12✔
526
                        if !context.IsCloudContext() {
×
527
                                return fmt.Errorf("the 'astro api cloud' command is only available in cloud context. Run 'astro login' to connect to Astro Cloud")
×
528
                        }
×
529

530
                        ctx, err := context.GetCurrentContext()
×
531
                        if err != nil {
×
532
                                return fmt.Errorf("getting current context: %w", err)
×
533
                        }
×
534

535
                        if err := initCloudSpecCache(parentOpts, &ctx); err != nil {
×
536
                                return fmt.Errorf("initializing API spec: %w", err)
×
537
                        }
×
538

539
                        descOpts := &DescribeOptions{
×
540
                                Out:       out,
×
541
                                specCache: parentOpts.specCache,
×
542
                                Endpoint:  args[0],
×
543
                                Method:    method,
×
544
                                Refresh:   refresh,
×
545
                                Verbose:   verbose,
×
NEW
546
                                JSON:      jsonOut,
×
547
                        }
×
548
                        return runDescribe(descOpts)
×
549
                },
550
        }
551

552
        cmd.Flags().StringVarP(&method, "method", "X", "", "HTTP method (GET, POST, PUT, PATCH, DELETE)")
12✔
553
        cmd.Flags().BoolVar(&refresh, "refresh", false, "Force refresh of the OpenAPI specification cache")
12✔
554
        cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show spec URL and additional details")
12✔
555
        cmd.Flags().BoolVar(&jsonOut, "json", false, "Output the endpoint schema as JSON")
12✔
556

12✔
557
        return cmd
12✔
558
}
559

560
// buildURL constructs the full URL from base and path.
561
func buildURL(baseURL, path string) string {
6✔
562
        // Ensure path starts with /
6✔
563
        if !strings.HasPrefix(path, "/") {
8✔
564
                path = "/" + path
2✔
565
        }
2✔
566
        return strings.TrimSuffix(baseURL, "/") + path
6✔
567
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc