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

astronomer / astro-cli / 716c27ab-af54-4fbb-904c-9fa0a6da08dc

26 May 2026 02:32PM UTC coverage: 44.676% (+5.0%) from 39.653%
716c27ab-af54-4fbb-904c-9fa0a6da08dc

push

circleci

web-flow
Migrate CLI to v1 public API; retire v1beta1 and v1alpha1 (except IDE) (#2093)

1848 of 18362 new or added lines in 58 files covered. (10.06%)

925 existing lines in 15 files now uncovered.

24957 of 55862 relevant lines covered (44.68%)

7.74 hits per line

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

65.15
/cloud/env/objects.go
1
package env
2

3
import (
4
        httpcontext "context"
5
        "errors"
6
        "fmt"
7
        "net/http"
8

9
        "github.com/astronomer/astro-cli/astro-client-v1"
10
        "github.com/astronomer/astro-cli/cloud/organization"
11
        "github.com/astronomer/astro-cli/config"
12
)
13

14
var (
15
        ErrScopeNotSpecified = errors.New("--workspace-id or --deployment-id must be specified")
16
        ErrScopeAmbiguous    = errors.New("--workspace-id and --deployment-id are mutually exclusive")
17
        ErrNotFound          = errors.New("environment object not found")
18
)
19

20
const (
21
        // defaultListLimit is the page size requested from list endpoints.
22
        defaultListLimit = 1000
23
        // maxListPages caps a single list call at maxListPages*defaultListLimit
24
        // rows as a safety bound against a server that mis-reports TotalCount.
25
        maxListPages = 100
26
)
27

28
// Scope captures the target of an env-object operation. Exactly one of
29
// WorkspaceID / DeploymentID is set.
30
type Scope struct {
31
        WorkspaceID  string
32
        DeploymentID string
33
}
34

35
func (s Scope) Validate() error {
22✔
36
        switch {
22✔
37
        case s.WorkspaceID == "" && s.DeploymentID == "":
2✔
38
                return ErrScopeNotSpecified
2✔
39
        case s.WorkspaceID != "" && s.DeploymentID != "":
2✔
40
                return ErrScopeAmbiguous
2✔
41
        }
42
        return nil
18✔
43
}
44

45
// ScopeFromIDs builds a Scope from raw workspace/deployment IDs, applying the
46
// "deployment wins when both are set" precedence used by the local-dev path.
47
// Callers that want strict mutual-exclusion should construct Scope directly
48
// and call Validate.
49
func ScopeFromIDs(workspaceID, deploymentID string) Scope {
×
50
        if deploymentID != "" {
×
51
                return Scope{DeploymentID: deploymentID}
×
52
        }
×
53
        return Scope{WorkspaceID: workspaceID}
×
54
}
55

56
// listObjects returns env-objects of the given type within the scope.
57
// resolveLinked includes inherited workspace objects when listing at deployment scope.
58
// includeSecrets requests secret values from the server (subject to org policy).
59
//
60
// Pages through the list endpoint when the server reports more rows than fit
61
// in a single response. Stops when the accumulated count reaches TotalCount,
62
// when a short page is returned, or when maxListPages is hit (safety bound).
63
func listObjects(scope Scope, objectType astrov1.ListEnvironmentObjectsParamsObjectType, resolveLinked, includeSecrets bool, astroV1Client astrov1.APIClient) ([]astrov1.EnvironmentObject, error) {
7✔
64
        if err := scope.Validate(); err != nil {
9✔
65
                return nil, err
2✔
66
        }
2✔
67
        c, err := config.GetCurrentContext()
5✔
68
        if err != nil {
5✔
69
                return nil, err
×
70
        }
×
71

72
        var (
5✔
73
                all    []astrov1.EnvironmentObject
5✔
74
                offset int
5✔
75
        )
5✔
76
        for page := 0; page < maxListPages; page++ {
11✔
77
                params := buildListParams(scope, objectType, nil, resolveLinked, includeSecrets, defaultListLimit)
6✔
78
                params.Offset = &offset
6✔
79

6✔
80
                resp, err := astroV1Client.ListEnvironmentObjectsWithResponse(httpcontext.Background(), c.Organization, params)
6✔
81
                if err != nil {
6✔
82
                        return nil, err
×
83
                }
×
84
                if err := normalizeListErr(resp.HTTPResponse, resp.Body); err != nil {
6✔
85
                        return nil, err
×
86
                }
×
87
                batch := resp.JSON200.EnvironmentObjects
6✔
88
                all = append(all, batch...)
6✔
89
                if len(batch) < defaultListLimit || len(all) >= resp.JSON200.TotalCount {
11✔
90
                        return all, nil
5✔
91
                }
5✔
92
                offset = len(all)
1✔
93
        }
94
        return all, fmt.Errorf("aborted listing %s objects after %d pages", objectType, maxListPages)
×
95
}
96

97
// getObject fetches a single env-object by ID or key.
98
//
99
// The platform has no GET-by-key endpoint; for keys, we filter the list endpoint
100
// server-side via ObjectKey and force resolveLinked=false so the returned ID is
101
// addressable in subsequent CRUD calls.
102
func getObject(idOrKey string, scope Scope, objectType astrov1.ListEnvironmentObjectsParamsObjectType, includeSecrets bool, astroV1Client astrov1.APIClient) (*astrov1.EnvironmentObject, error) {
4✔
103
        c, err := config.GetCurrentContext()
4✔
104
        if err != nil {
4✔
105
                return nil, err
×
106
        }
×
107
        if organization.IsCUID(idOrKey) {
4✔
NEW
108
                resp, err := astroV1Client.GetEnvironmentObjectWithResponse(httpcontext.Background(), c.Organization, idOrKey)
×
109
                if err != nil {
×
110
                        return nil, err
×
111
                }
×
NEW
112
                if err := astrov1.NormalizeAPIError(resp.HTTPResponse, resp.Body); err != nil {
×
113
                        return nil, err
×
114
                }
×
115
                return resp.JSON200, nil
×
116
        }
117

118
        if err := scope.Validate(); err != nil {
4✔
119
                return nil, err
×
120
        }
×
121
        // ObjectKey filtering returns at most one row per (scope, objectType);
122
        // limit=2 catches server-side anomalies without forcing pagination.
123
        params := buildListParams(scope, objectType, &idOrKey, false, includeSecrets, 2)
4✔
124

4✔
125
        resp, err := astroV1Client.ListEnvironmentObjectsWithResponse(httpcontext.Background(), c.Organization, params)
4✔
126
        if err != nil {
4✔
127
                return nil, err
×
128
        }
×
129
        if err := normalizeListErr(resp.HTTPResponse, resp.Body); err != nil {
4✔
130
                return nil, err
×
131
        }
×
132
        for i := range resp.JSON200.EnvironmentObjects {
7✔
133
                if resp.JSON200.EnvironmentObjects[i].ObjectKey == idOrKey {
6✔
134
                        return &resp.JSON200.EnvironmentObjects[i], nil
3✔
135
                }
3✔
136
        }
137
        return nil, fmt.Errorf("%w: %s", ErrNotFound, idOrKey)
1✔
138
}
139

140
// deleteObject deletes an env-object by ID or key.
141
func deleteObject(idOrKey string, scope Scope, objectType astrov1.ListEnvironmentObjectsParamsObjectType, astroV1Client astrov1.APIClient) error {
2✔
142
        id, err := resolveID(idOrKey, scope, objectType, astroV1Client)
2✔
143
        if err != nil {
2✔
144
                return err
×
145
        }
×
146
        c, err := config.GetCurrentContext()
2✔
147
        if err != nil {
2✔
148
                return err
×
149
        }
×
150
        resp, err := astroV1Client.DeleteEnvironmentObjectWithResponse(httpcontext.Background(), c.Organization, id)
2✔
151
        if err != nil {
2✔
152
                return err
×
153
        }
×
154
        return astrov1.NormalizeAPIError(resp.HTTPResponse, resp.Body)
2✔
155
}
156

157
// resolveID returns idOrKey unchanged when it's already a CUID (no network call),
158
// otherwise looks up the ID by key. Used by Update / Delete paths so callers
159
// passing an ID don't pay for a redundant GET.
160
func resolveID(idOrKey string, scope Scope, objectType astrov1.ListEnvironmentObjectsParamsObjectType, astroV1Client astrov1.APIClient) (string, error) {
3✔
161
        if organization.IsCUID(idOrKey) {
4✔
162
                return idOrKey, nil
1✔
163
        }
1✔
164
        existing, err := getObject(idOrKey, scope, objectType, false, astroV1Client)
2✔
165
        if err != nil {
2✔
166
                return "", err
×
167
        }
×
168
        if existing.Id == nil || *existing.Id == "" {
2✔
169
                return "", fmt.Errorf("environment object %q has no id", idOrKey)
×
170
        }
×
171
        return *existing.Id, nil
2✔
172
}
173

174
// scopeRequest converts a Scope into the create-request scope enum + entity ID.
175
func scopeRequest(scope Scope) (scopeType astrov1.CreateEnvironmentObjectRequestScope, scopeEntityID string) {
4✔
176
        if scope.DeploymentID != "" {
4✔
NEW
177
                return astrov1.CreateEnvironmentObjectRequestScopeDEPLOYMENT, scope.DeploymentID
×
178
        }
×
179
        return astrov1.CreateEnvironmentObjectRequestScopeWORKSPACE, scope.WorkspaceID
4✔
180
}
181

182
// ErrAutoLinkRequiresWorkspace is returned when a caller asks to auto-link an
183
// object to all deployments while creating it at deployment scope. Auto-link
184
// is a workspace-scope concept; deployment-scope objects are already pinned
185
// to a single deployment.
186
var ErrAutoLinkRequiresWorkspace = errors.New("--auto-link applies only to workspace-scoped objects")
187

188
// validateAutoLink rejects auto-link=true on a deployment-scoped object. nil
189
// (flag unset) and false are no-ops.
190
func validateAutoLink(scope Scope, autoLink *bool) error {
7✔
191
        if autoLink != nil && *autoLink && scope.DeploymentID != "" {
7✔
192
                return ErrAutoLinkRequiresWorkspace
×
193
        }
×
194
        return nil
7✔
195
}
196

197
func buildListParams(scope Scope, objectType astrov1.ListEnvironmentObjectsParamsObjectType, objectKey *string, resolveLinked, includeSecrets bool, limit int) *astrov1.ListEnvironmentObjectsParams {
10✔
198
        params := &astrov1.ListEnvironmentObjectsParams{
10✔
199
                ObjectType:    &objectType,
10✔
200
                ObjectKey:     objectKey,
10✔
201
                ShowSecrets:   &includeSecrets,
10✔
202
                ResolveLinked: &resolveLinked,
10✔
203
                Limit:         &limit,
10✔
204
        }
10✔
205
        if scope.WorkspaceID != "" {
19✔
206
                params.WorkspaceId = &scope.WorkspaceID
9✔
207
        } else if scope.DeploymentID != "" {
11✔
208
                params.DeploymentId = &scope.DeploymentID
1✔
209
        }
1✔
210
        return params
10✔
211
}
212

213
// normalizeListErr substitutes the friendlier org-level secrets-fetching
214
// guidance when applicable.
215
func normalizeListErr(httpResp *http.Response, body []byte) error {
10✔
216
        err := astrov1.NormalizeAPIError(httpResp, body)
10✔
217
        if err == nil {
20✔
218
                return nil
10✔
219
        }
10✔
220
        if IsSecretsFetchingNotAllowedError(err) {
×
221
                return errors.New(SecretsFetchingNotAllowedErrMsg)
×
222
        }
×
223
        return err
×
224
}
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