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

astronomer / astro-cli / 609b6753-92a4-41d8-923c-0f23002261ab

10 Apr 2026 04:52PM UTC coverage: 39.387% (+0.04%) from 39.344%
609b6753-92a4-41d8-923c-0f23002261ab

Pull #2077

circleci

ashb
fixup! Store auth credentials in the OS keychain (macOS + Linux)
Pull Request #2077: Store auth credentials in the OS keychain (macOS + Linux)

366 of 500 new or added lines in 43 files covered. (73.2%)

43 existing lines in 7 files now uncovered.

24949 of 63343 relevant lines covered (39.39%)

9.5 hits per line

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

64.13
/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/credentials"
17
        "github.com/astronomer/astro-cli/pkg/domainutil"
18
        "github.com/astronomer/astro-cli/pkg/openapi"
19
)
20

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

29
// NewCloudCmd creates the 'astro api cloud' command.
30
//
31
//nolint:dupl
32
func NewCloudCmd(out io.Writer, creds *credentials.CurrentCredentials) *cobra.Command {
12✔
33
        opts := &CloudOptions{
12✔
34
                RequestOptions: RequestOptions{
12✔
35
                        Out:    out,
12✔
36
                        ErrOut: os.Stderr,
12✔
37
                },
12✔
38
                creds: creds,
12✔
39
        }
12✔
40

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

12✔
147
        return cmd
12✔
148
}
149

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

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

163
        // Check for token
NEW
164
        if opts.creds == nil || opts.creds.Get() == "" {
×
165
                return fmt.Errorf("not authenticated. Run 'astro login' to authenticate")
×
166
        }
×
167

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

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

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

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

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

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

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

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

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

×
235
        // Generate curl command if requested
×
236
        if opts.GenerateCurl {
×
NEW
237
                return generateCurl(opts.Out, method, url, opts.creds.Get(), opts.RequestHeaders, params, opts.RequestInputFile)
×
238
        }
×
239

240
        // Build and execute the request
NEW
241
        return executeRequest(&opts.RequestOptions, method, url, opts.creds.Get(), params)
×
242
}
243

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

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

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

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

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

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

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

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

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

301
        return result, nil
4✔
302
}
303

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

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

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

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

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

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

×
336
        return nil
×
337
}
338

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

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

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

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

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

373
        return result, nil
6✔
374
}
375

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

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

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

399
        // When --spec-url is provided, use it. If --spec-token-env-var is also set,
400
        // read the token from that env var and send it as auth when fetching the spec.
401
        if opts.SpecURL != "" {
12✔
402
                cachePath := filepath.Join(config.HomeConfigPath, openapi.SpecCacheFileName(opts.SpecURL))
5✔
403
                if opts.SpecTokenEnvVar != "" {
8✔
404
                        token := os.Getenv(opts.SpecTokenEnvVar)
3✔
405
                        if token == "" {
4✔
406
                                return fmt.Errorf("environment variable %q is not set", opts.SpecTokenEnvVar)
1✔
407
                        }
1✔
408
                        opts.specCache = openapi.NewCacheWithAuth(opts.SpecURL, cachePath, token)
2✔
409
                } else {
2✔
410
                        opts.specCache = openapi.NewCacheWithOptions(opts.SpecURL, cachePath)
2✔
411
                }
2✔
412
                return nil
4✔
413
        }
414

415
        domain := domainutil.FormatDomain(ctx.Domain)
2✔
416
        specURL := ctx.GetPublicRESTAPIURL("spec/v1.0")
2✔
417
        if specURL == "" {
2✔
418
                return fmt.Errorf("could not determine API spec URL for domain %q. Check your login context", ctx.Domain)
×
419
        }
×
420
        cachePath := filepath.Join(config.HomeConfigPath, openapi.CloudCacheFileNameForDomain(domain))
2✔
421
        opts.specCache = openapi.NewCacheWithOptions(specURL, cachePath)
2✔
422
        return nil
2✔
423
}
424

425
// NewCloudListCmd creates the 'astro api cloud ls' command.
426
// It lazily initializes the spec cache so the correct domain-specific spec URL is used.
427
func NewCloudListCmd(out io.Writer, parentOpts *CloudOptions) *cobra.Command {
12✔
428
        var verbose bool
12✔
429
        var refresh bool
12✔
430

12✔
431
        cmd := &cobra.Command{
12✔
432
                Use:     "ls [filter]",
12✔
433
                Aliases: []string{"list"},
12✔
434
                Short:   "List available Astro Cloud API endpoints",
12✔
435
                Long: `List all available endpoints from the Astro Cloud API.
12✔
436

12✔
437
You can optionally provide a filter to search for specific endpoints.
12✔
438
The filter matches against endpoint paths, methods, operation IDs, summaries, and tags.`,
12✔
439
                Example: `  # List all endpoints
12✔
440
  astro api cloud ls
12✔
441

12✔
442
  # Filter endpoints
12✔
443
  astro api cloud ls deployments
12✔
444

12✔
445
  # List POST endpoints
12✔
446
  astro api cloud ls POST
12✔
447

12✔
448
  # Show verbose output with descriptions
12✔
449
  astro api cloud ls --verbose`,
12✔
450
                Args: cobra.MaximumNArgs(1),
12✔
451
                RunE: func(cmd *cobra.Command, args []string) error {
12✔
452
                        var filter string
×
453
                        if len(args) > 0 {
×
454
                                filter = args[0]
×
455
                        }
×
456

457
                        if !context.IsCloudContext() {
×
458
                                return fmt.Errorf("the 'astro api cloud' command is only available in cloud context. Run 'astro login' to connect to Astro Cloud")
×
459
                        }
×
460

461
                        ctx, err := context.GetCurrentContext()
×
462
                        if err != nil {
×
463
                                return fmt.Errorf("getting current context: %w", err)
×
464
                        }
×
465

466
                        if err := initCloudSpecCache(parentOpts, &ctx); err != nil {
×
467
                                return fmt.Errorf("initializing API spec: %w", err)
×
468
                        }
×
469

470
                        listOpts := &ListOptions{
×
471
                                Out:       out,
×
472
                                specCache: parentOpts.specCache,
×
473
                                Filter:    filter,
×
474
                                Verbose:   verbose,
×
475
                                Refresh:   refresh,
×
476
                        }
×
477
                        return runList(listOpts)
×
478
                },
479
        }
480

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

12✔
484
        return cmd
12✔
485
}
486

487
// NewCloudDescribeCmd creates the 'astro api cloud describe' command.
488
// It lazily initializes the spec cache so the correct domain-specific spec URL is used.
489
func NewCloudDescribeCmd(out io.Writer, parentOpts *CloudOptions) *cobra.Command {
12✔
490
        var method string
12✔
491
        var refresh bool
12✔
492
        var verbose bool
12✔
493

12✔
494
        cmd := &cobra.Command{
12✔
495
                Use:   "describe <endpoint>",
12✔
496
                Short: "Describe an Astro Cloud API endpoint's request and response schema",
12✔
497
                Long: `Show detailed information about an Astro Cloud API endpoint, including:
12✔
498
- Path and query parameters
12✔
499
- Request body schema (for POST/PUT/PATCH)
12✔
500
- Response schema
12✔
501

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

12✔
506
  # Describe a POST endpoint specifically
12✔
507
  astro api cloud describe /organizations/{organizationId}/deployments -X POST
12✔
508

12✔
509
  # Describe by operation ID
12✔
510
  astro api cloud describe CreateDeployment`,
12✔
511
                Args: cobra.ExactArgs(1),
12✔
512
                RunE: func(cmd *cobra.Command, args []string) error {
12✔
513
                        if !context.IsCloudContext() {
×
514
                                return fmt.Errorf("the 'astro api cloud' command is only available in cloud context. Run 'astro login' to connect to Astro Cloud")
×
515
                        }
×
516

517
                        ctx, err := context.GetCurrentContext()
×
518
                        if err != nil {
×
519
                                return fmt.Errorf("getting current context: %w", err)
×
520
                        }
×
521

522
                        if err := initCloudSpecCache(parentOpts, &ctx); err != nil {
×
523
                                return fmt.Errorf("initializing API spec: %w", err)
×
524
                        }
×
525

526
                        descOpts := &DescribeOptions{
×
527
                                Out:       out,
×
528
                                specCache: parentOpts.specCache,
×
529
                                Endpoint:  args[0],
×
530
                                Method:    method,
×
531
                                Refresh:   refresh,
×
532
                                Verbose:   verbose,
×
533
                        }
×
534
                        return runDescribe(descOpts)
×
535
                },
536
        }
537

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

12✔
542
        return cmd
12✔
543
}
544

545
// buildURL constructs the full URL from base and path.
546
func buildURL(baseURL, path string) string {
6✔
547
        // Ensure path starts with /
6✔
548
        if !strings.HasPrefix(path, "/") {
8✔
549
                path = "/" + path
2✔
550
        }
2✔
551
        return strings.TrimSuffix(baseURL, "/") + path
6✔
552
}
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