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

astronomer / astro-cli / ad3bf534-ee25-491b-9886-17cbc3b80c2e

08 Apr 2026 04:33PM UTC coverage: 39.38% (+0.04%) from 39.344%
ad3bf534-ee25-491b-9886-17cbc3b80c2e

Pull #2075

circleci

jeremybeard
Support local spec files for --spec-url flag

Allow --spec-url to accept local file paths (absolute, relative, ~/,
file://) in addition to HTTP URLs. Local specs are read fresh on every
Load() call with no caching, which is ideal for development workflows.
Pull Request #2075: Support local spec files for --spec-url

48 of 52 new or added lines in 2 files covered. (92.31%)

6 existing lines in 1 file now uncovered.

24885 of 63192 relevant lines covered (39.38%)

9.51 hits per line

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

64.34
/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
                        // specCache is initialized lazily when the domain is known
12✔
36
                },
12✔
37
        }
12✔
38

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

12✔
145
        return cmd
12✔
146
}
147

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

299
        return result, nil
4✔
300
}
301

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

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

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

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

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

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

×
334
        return nil
×
335
}
336

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

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

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

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

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

371
        return result, nil
6✔
372
}
373

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

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

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

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

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

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

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

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

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

12✔
450
  # Filter endpoints
12✔
451
  astro api cloud ls deployments
12✔
452

12✔
453
  # List POST endpoints
12✔
454
  astro api cloud ls POST
12✔
455

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

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

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

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

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

489
        cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show additional details like summaries and tags")
12✔
490
        cmd.Flags().BoolVar(&refresh, "refresh", false, "Force refresh of the OpenAPI specification cache")
12✔
491

12✔
492
        return cmd
12✔
493
}
494

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

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

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

12✔
514
  # Describe a POST endpoint specifically
12✔
515
  astro api cloud describe /organizations/{organizationId}/deployments -X POST
12✔
516

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

525
                        ctx, err := context.GetCurrentContext()
×
526
                        if err != nil {
×
527
                                return fmt.Errorf("getting current context: %w", err)
×
528
                        }
×
529

530
                        if err := initCloudSpecCache(parentOpts, &ctx); err != nil {
×
531
                                return fmt.Errorf("initializing API spec: %w", err)
×
532
                        }
×
533

534
                        descOpts := &DescribeOptions{
×
535
                                Out:       out,
×
536
                                specCache: parentOpts.specCache,
×
537
                                Endpoint:  args[0],
×
538
                                Method:    method,
×
539
                                Refresh:   refresh,
×
540
                                Verbose:   verbose,
×
541
                        }
×
542
                        return runDescribe(descOpts)
×
543
                },
544
        }
545

546
        cmd.Flags().StringVarP(&method, "method", "X", "", "HTTP method (GET, POST, PUT, PATCH, DELETE)")
12✔
547
        cmd.Flags().BoolVar(&refresh, "refresh", false, "Force refresh of the OpenAPI specification cache")
12✔
548
        cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show spec URL and additional details")
12✔
549

12✔
550
        return cmd
12✔
551
}
552

553
// buildURL constructs the full URL from base and path.
554
func buildURL(baseURL, path string) string {
6✔
555
        // Ensure path starts with /
6✔
556
        if !strings.HasPrefix(path, "/") {
8✔
557
                path = "/" + path
2✔
558
        }
2✔
559
        return strings.TrimSuffix(baseURL, "/") + path
6✔
560
}
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