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

astronomer / astro-cli / d56d0e5b-516f-451f-8c66-a8b3a30417bb

05 Feb 2026 02:21PM UTC coverage: 32.742% (-0.04%) from 32.783%
d56d0e5b-516f-451f-8c66-a8b3a30417bb

push

circleci

jeremybeard
Add endpoint calling via operation id

0 of 92 new or added lines in 3 files covered. (0.0%)

3 existing lines in 2 files now uncovered.

21211 of 64782 relevant lines covered (32.74%)

8.32 hits per line

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

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

3
import (
4
        "fmt"
5
        "io"
6
        "regexp"
7
        "strings"
8
        "time"
9

10
        "github.com/astronomer/astro-cli/config"
11
        "github.com/astronomer/astro-cli/context"
12
        "github.com/astronomer/astro-cli/pkg/ansi"
13
        "github.com/astronomer/astro-cli/pkg/openapi"
14
        "github.com/spf13/cobra"
15
)
16

17
const (
18
        cloudAPIBaseURL = "https://api.astronomer.io/v1"
19
)
20

21
// CloudOptions holds all options for the cloud api command.
22
type CloudOptions struct {
23
        Out io.Writer
24

25
        // Request options
26
        RequestMethod       string
27
        RequestMethodPassed bool
28
        RequestPath         string
29
        RequestInputFile    string
30
        MagicFields         []string
31
        RawFields           []string
32
        RequestHeaders      []string
33
        PathParams          []string // For providing path parameters when using operation ID
34

35
        // Output options
36
        ShowResponseHeaders bool
37
        Paginate            bool
38
        Slurp               bool
39
        Silent              bool
40
        Template            string
41
        FilterOutput        string
42
        Verbose             bool
43
        CacheTTL            time.Duration
44

45
        // Other options
46
        GenerateCurl bool
47

48
        // Internal
49
        specCache *openapi.Cache
50
}
51

52
// NewCloudCmd creates the 'astro api cloud' command.
53
func NewCloudCmd(out io.Writer) *cobra.Command {
×
54
        opts := &CloudOptions{
×
55
                Out:       out,
×
56
                specCache: openapi.NewCache(),
×
57
        }
×
58

×
59
        cmd := &cobra.Command{
×
NEW
60
                Use:   "cloud <endpoint | operation-id>",
×
61
                Short: "Make authenticated requests to the Astro Cloud API",
×
62
                Long: `Make authenticated HTTP requests to the Astro Cloud API (api.astronomer.io).
×
63

×
NEW
64
The argument can be either:
×
NEW
65
  - A path of an Astro Cloud API endpoint (e.g., /organizations/{organizationId})
×
NEW
66
  - An operation ID from the API spec (e.g., GetDeployment, ListOrganizations)
×
NEW
67

×
68
Placeholder values {organizationId} and {workspaceId} will be replaced
×
NEW
69
with values from the current context. Other path parameters can be provided
×
NEW
70
using the -p/--path-param flag.
×
71

×
72
The default HTTP request method is GET normally and POST if any parameters
×
NEW
73
were added. Override the method with --method. When using an operation ID,
×
NEW
74
the method is auto-detected from the API spec.
×
75

×
76
Pass one or more -f/--raw-field values in key=value format to add static string
×
77
parameters to the request payload. To add non-string or placeholder-determined
×
78
values, see -F/--field below.
×
79

×
80
The -F/--field flag has magic type conversion based on the format of the value:
×
81
  - literal values true, false, null, and integer numbers get converted to
×
82
    appropriate JSON types;
×
83
  - if the value starts with @, the rest of the value is interpreted as a
×
84
    filename to read the value from. Pass - to read from standard input.
×
85

×
86
To pass nested parameters in the request payload, use key[subkey]=value syntax.
×
87
To pass nested values as arrays, declare multiple fields with key[]=value1.`,
×
88
                Example: `  # List all Cloud API endpoints
×
89
  astro api cloud ls
×
90

×
91
  # Get organization details (auto-injects organizationId)
×
92
  astro api cloud /organizations/{organizationId}
×
93

×
NEW
94
  # Use operation ID with path parameters
×
NEW
95
  astro api cloud GetDeployment -p deploymentId=abc123
×
NEW
96

×
NEW
97
  # Use operation ID (organizationId auto-injected from context)
×
NEW
98
  astro api cloud ListDeployments
×
NEW
99

×
100
  # List deployments with jq filter
×
101
  astro api cloud /organizations/{organizationId}/deployments --jq '.[].name'
×
102

×
103
  # Create a resource with typed fields
×
104
  astro api cloud -X POST /organizations/{organizationId}/workspaces \
×
105
    -F name=my-workspace \
×
106
    -F description="My new workspace"
×
107

×
108
  # Use Go template for output
×
109
  astro api cloud /organizations/{organizationId}/deployments \
×
110
    --template '{{range .}}{{.name}} ({{.status}}){{"\n"}}{{end}}'
×
111

×
112
  # Generate curl command instead of executing
×
113
  astro api cloud /organizations/{organizationId}/deployments --generate
×
114

×
115
  # Include response headers
×
116
  astro api cloud /organizations/{organizationId} -i
×
117

×
118
  # Verbose mode showing full request/response
×
119
  astro api cloud /organizations/{organizationId} --verbose`,
×
120
                Args: cobra.MaximumNArgs(1),
×
121
                PreRunE: func(cmd *cobra.Command, args []string) error {
×
122
                        opts.RequestMethodPassed = cmd.Flags().Changed("method")
×
123
                        return nil
×
124
                },
×
125
                RunE: func(cmd *cobra.Command, args []string) error {
×
126
                        if len(args) == 0 {
×
127
                                // Interactive mode - show endpoint selection
×
128
                                return runCloudInteractive(opts)
×
129
                        }
×
130
                        opts.RequestPath = args[0]
×
131
                        return runCloud(opts)
×
132
                },
133
        }
134

135
        // Request flags
136
        cmd.Flags().StringVarP(&opts.RequestMethod, "method", "X", "GET", "The HTTP method for the request")
×
137
        cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a typed parameter in key=value format")
×
138
        cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter in key=value format")
×
139
        cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add a HTTP request header in key:value format")
×
140
        cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The file to use as body for the HTTP request (use \"-\" for stdin)")
×
NEW
141
        cmd.Flags().StringArrayVarP(&opts.PathParams, "path-param", "p", nil, "Path parameter in key=value format (for use with operation IDs)")
×
142

×
143
        // Output flags
×
144
        cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response status line and headers in the output")
×
145
        cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results")
×
146
        cmd.Flags().BoolVar(&opts.Slurp, "slurp", false, "Use with --paginate to return an array of all pages")
×
147
        cmd.Flags().BoolVar(&opts.Silent, "silent", false, "Do not print the response body")
×
148
        cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format JSON output using a Go template")
×
149
        cmd.Flags().StringVarP(&opts.FilterOutput, "jq", "q", "", "Query to select values from the response using jq syntax")
×
150
        cmd.Flags().BoolVar(&opts.Verbose, "verbose", false, "Include full HTTP request and response in the output")
×
151
        cmd.Flags().DurationVar(&opts.CacheTTL, "cache", 0, "Cache the response, e.g. \"3600s\", \"60m\", \"1h\"")
×
152

×
153
        // Other flags
×
154
        cmd.Flags().BoolVar(&opts.GenerateCurl, "generate", false, "Output a curl command instead of executing the request")
×
155

×
156
        // Add list subcommand
×
157
        cmd.AddCommand(NewListCmd(out, opts.specCache))
×
158
        cmd.AddCommand(NewDescribeCmd(out, opts.specCache))
×
159

×
160
        return cmd
×
161
}
162

163
// runCloud executes the cloud API request.
164
func runCloud(opts *CloudOptions) error {
×
165
        // Check if we're in a cloud context
×
166
        if !context.IsCloudContext() {
×
167
                return fmt.Errorf("the 'astro api cloud' command is only available in cloud context. Run 'astro login' to connect to Astro Cloud")
×
168
        }
×
169

170
        // Get current context for auth and placeholders
171
        ctx, err := context.GetCurrentContext()
×
172
        if err != nil {
×
173
                return fmt.Errorf("getting current context: %w", err)
×
174
        }
×
175

176
        // Check for token
177
        if ctx.Token == "" {
×
178
                return fmt.Errorf("not authenticated. Run 'astro login' to authenticate")
×
179
        }
×
180

181
        // Resolve operation ID to path if needed
NEW
182
        requestPath := opts.RequestPath
×
NEW
183
        method := opts.RequestMethod
×
NEW
184
        methodFromSpec := false
×
NEW
185

×
NEW
186
        if isOperationID(requestPath) {
×
NEW
187
                endpoint, err := resolveOperationID(opts.specCache, requestPath)
×
NEW
188
                if err != nil {
×
NEW
189
                        return err
×
NEW
190
                }
×
NEW
191
                requestPath = endpoint.Path
×
NEW
192
                if !opts.RequestMethodPassed {
×
NEW
193
                        method = endpoint.Method
×
NEW
194
                        methodFromSpec = true
×
NEW
195
                }
×
196
        }
197

198
        // Apply path params from flags
NEW
199
        requestPath, err = applyPathParams(requestPath, opts.PathParams)
×
NEW
200
        if err != nil {
×
NEW
201
                return fmt.Errorf("applying path params: %w", err)
×
NEW
202
        }
×
203

204
        // Fill context placeholders in the path
NEW
205
        requestPath, err = fillPlaceholders(requestPath, &ctx)
×
206
        if err != nil {
×
207
                return fmt.Errorf("filling placeholders: %w", err)
×
208
        }
×
209

210
        // Check for any remaining unfilled path parameters
NEW
211
        if missing := findMissingPathParams(requestPath); len(missing) > 0 {
×
NEW
212
                return fmt.Errorf("missing path parameter(s): %s. Use -p/--path-param to provide them (e.g., -p %s=value)",
×
NEW
213
                        strings.Join(missing, ", "), missing[0])
×
NEW
214
        }
×
215

216
        // Parse fields into request body
217
        params, err := parseFields(opts.MagicFields, opts.RawFields)
×
218
        if err != nil {
×
219
                return fmt.Errorf("parsing fields: %w", err)
×
220
        }
×
221

222
        // Determine HTTP method (only override if not from spec and not explicitly passed)
NEW
223
        if !methodFromSpec && !opts.RequestMethodPassed && (len(params) > 0 || opts.RequestInputFile != "") {
×
224
                method = "POST"
×
225
        }
×
226

227
        // Build the full URL
228
        url := buildURL(cloudAPIBaseURL, requestPath)
×
229

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

235
        // Build and execute the request
236
        return executeRequest(opts, method, url, ctx.Token, params)
×
237
}
238

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

245
// resolveOperationID looks up an operation ID in the OpenAPI spec and returns the endpoint.
NEW
246
func resolveOperationID(specCache *openapi.Cache, operationID string) (*openapi.Endpoint, error) {
×
NEW
247
        if err := specCache.Load(false); err != nil {
×
NEW
248
                return nil, fmt.Errorf("loading OpenAPI spec: %w", err)
×
NEW
249
        }
×
250

NEW
251
        endpoint := openapi.FindEndpointByOperationID(specCache.GetEndpoints(), operationID)
×
NEW
252
        if endpoint == nil {
×
NEW
253
                return nil, fmt.Errorf("operation ID '%s' not found. Use 'astro api cloud ls' to see available endpoints", operationID)
×
NEW
254
        }
×
255

NEW
256
        return endpoint, nil
×
257
}
258

259
// applyPathParams replaces path placeholders with values from --path-param flags.
NEW
260
func applyPathParams(path string, pathParams []string) (string, error) {
×
NEW
261
        if len(pathParams) == 0 {
×
NEW
262
                return path, nil
×
NEW
263
        }
×
264

265
        // Parse path params into a map
NEW
266
        params := make(map[string]string)
×
NEW
267
        for _, p := range pathParams {
×
NEW
268
                parts := strings.SplitN(p, "=", 2)
×
NEW
269
                if len(parts) != 2 {
×
NEW
270
                        return "", fmt.Errorf("invalid path param format '%s', expected key=value", p)
×
NEW
271
                }
×
NEW
272
                params[parts[0]] = parts[1]
×
273
        }
274

275
        // Replace placeholders in the path
NEW
276
        result := placeholderRE.ReplaceAllStringFunc(path, func(match string) string {
×
NEW
277
                name := match[1 : len(match)-1]
×
NEW
278
                if val, ok := params[name]; ok {
×
NEW
279
                        return val
×
NEW
280
                }
×
NEW
281
                return match
×
282
        })
283

NEW
284
        return result, nil
×
285
}
286

287
// runCloudInteractive runs the cloud API command in interactive mode.
288
func runCloudInteractive(opts *CloudOptions) error {
×
289
        // Check if we're in a cloud context
×
290
        if !context.IsCloudContext() {
×
291
                return fmt.Errorf("the 'astro api cloud' command is only available in cloud context. Run 'astro login' to connect to Astro Cloud")
×
292
        }
×
293

294
        // Load OpenAPI spec
295
        if err := opts.specCache.Load(false); err != nil {
×
296
                return fmt.Errorf("loading OpenAPI spec: %w", err)
×
297
        }
×
298

299
        endpoints := opts.specCache.GetEndpoints()
×
300
        if len(endpoints) == 0 {
×
301
                return fmt.Errorf("no endpoints found in API specification")
×
302
        }
×
303

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

×
308
        return nil
×
309
}
310

311
// placeholderRE matches placeholders like {organizationId}, {workspaceId}, etc.
312
var placeholderRE = regexp.MustCompile(`\{([a-zA-Z][a-zA-Z0-9]*)\}`)
313

314
// fillPlaceholders replaces placeholders with values from the context.
315
func fillPlaceholders(path string, ctx *config.Context) (string, error) {
×
316
        var errs []string
×
317

×
318
        result := placeholderRE.ReplaceAllStringFunc(path, func(match string) string {
×
319
                // Extract the name without braces
×
320
                name := match[1 : len(match)-1]
×
321

×
322
                switch strings.ToLower(name) {
×
323
                case "organizationid":
×
324
                        if ctx.Organization == "" {
×
325
                                errs = append(errs, "organizationId not set in context (run 'astro organization switch')")
×
326
                                return match
×
327
                        }
×
328
                        return ctx.Organization
×
329
                case "workspaceid":
×
330
                        if ctx.Workspace == "" {
×
331
                                errs = append(errs, "workspaceId not set in context (run 'astro workspace switch')")
×
332
                                return match
×
333
                        }
×
334
                        return ctx.Workspace
×
335
                default:
×
336
                        // Keep unknown placeholders as-is (user might provide them)
×
337
                        return match
×
338
                }
339
        })
340

341
        if len(errs) > 0 {
×
342
                return result, fmt.Errorf("placeholder error: %s", strings.Join(errs, "; "))
×
343
        }
×
344

345
        return result, nil
×
346
}
347

348
// findMissingPathParams returns any unfilled path parameters in the path.
NEW
349
func findMissingPathParams(path string) []string {
×
NEW
350
        matches := placeholderRE.FindAllStringSubmatch(path, -1)
×
NEW
351
        if len(matches) == 0 {
×
NEW
352
                return nil
×
NEW
353
        }
×
354

NEW
355
        missing := make([]string, 0, len(matches))
×
NEW
356
        for _, match := range matches {
×
NEW
357
                if len(match) > 1 {
×
NEW
358
                        missing = append(missing, match[1])
×
NEW
359
                }
×
360
        }
NEW
361
        return missing
×
362
}
363

364
// buildURL constructs the full URL from base and path.
365
func buildURL(baseURL, path string) string {
×
366
        // Ensure path starts with /
×
367
        if !strings.HasPrefix(path, "/") {
×
368
                path = "/" + path
×
369
        }
×
370
        return strings.TrimSuffix(baseURL, "/") + path
×
371
}
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