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

astronomer / astro-cli / d8c087d9-3e34-4a26-9912-4e9425408752

04 Feb 2026 08:31PM UTC coverage: 32.799% (+0.005%) from 32.794%
d8c087d9-3e34-4a26-9912-4e9425408752

push

circleci

jeremybeard
Fix linting

18 of 95 new or added lines in 9 files covered. (18.95%)

5 existing lines in 1 file now uncovered.

21211 of 64670 relevant lines covered (32.8%)

8.33 hits per line

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

0.0
/pkg/openapi/cache.go
1
package openapi
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "io"
8
        "net/http"
9
        "os"
10
        "path/filepath"
11
        "strings"
12
        "time"
13

14
        "github.com/astronomer/astro-cli/config"
15
        "gopkg.in/yaml.v3"
16
)
17

18
const (
19
        // SpecURL is the URL to fetch the Astro Cloud API OpenAPI specification.
20
        SpecURL = "https://api.astronomer.io/spec/v1.0"
21
        // CacheFileName is the name of the cache file.
22
        CacheFileName = "openapi-cache.json"
23
        // CacheTTL is how long the cache is valid.
24
        CacheTTL = 24 * time.Hour
25
        // FetchTimeout is the timeout for fetching the spec.
26
        FetchTimeout = 30 * time.Second
27
        // dirPermissions is the permission mode for created directories.
28
        dirPermissions = 0o755
29
        // filePermissions is the permission mode for the cache file.
30
        filePermissions = 0o600
31
)
32

33
// CachedSpec wraps the OpenAPI spec with metadata for caching.
34
type CachedSpec struct {
35
        Spec      *OpenAPISpec `json:"spec"`
36
        FetchedAt time.Time    `json:"fetchedAt"`
37
}
38

39
// Cache manages fetching and caching of an OpenAPI specification.
40
type Cache struct {
41
        specURL   string
42
        cachePath string
43
        spec      *OpenAPISpec
44
        fetchedAt time.Time
45
}
46

47
// NewCache creates a new OpenAPI cache with default settings.
48
func NewCache() *Cache {
×
49
        return &Cache{
×
50
                specURL:   SpecURL,
×
51
                cachePath: filepath.Join(config.HomeConfigPath, CacheFileName),
×
52
        }
×
53
}
×
54

55
// NewCacheWithOptions creates a new OpenAPI cache with custom settings.
56
func NewCacheWithOptions(specURL, cachePath string) *Cache {
×
57
        return &Cache{
×
58
                specURL:   specURL,
×
59
                cachePath: cachePath,
×
60
        }
×
61
}
×
62

63
// Load loads the OpenAPI spec, using cache if valid or fetching if needed.
64
// If forceRefresh is true, the cache is ignored and a fresh spec is fetched.
65
func (c *Cache) Load(forceRefresh bool) error {
×
66
        if !forceRefresh {
×
67
                // Try to load from cache first
×
68
                if err := c.readCache(); err == nil && !c.isExpired() {
×
69
                        return nil
×
70
                }
×
71
        }
72

73
        // Fetch fresh spec
74
        if err := c.fetchSpec(); err != nil {
×
75
                // If fetch fails and we have a stale cache, use it
×
76
                if c.spec != nil {
×
77
                        return nil
×
78
                }
×
79
                return err
×
80
        }
81

82
        // Save to cache
83
        return c.saveCache()
×
84
}
85

86
// GetSpec returns the loaded OpenAPI spec.
87
func (c *Cache) GetSpec() *OpenAPISpec {
×
88
        return c.spec
×
89
}
×
90

91
// GetEndpoints extracts all endpoints from the loaded spec.
92
func (c *Cache) GetEndpoints() []Endpoint {
×
93
        if c.spec == nil {
×
94
                return nil
×
95
        }
×
96
        return ExtractEndpoints(c.spec)
×
97
}
98

99
// IsLoaded returns true if a spec has been loaded.
100
func (c *Cache) IsLoaded() bool {
×
101
        return c.spec != nil
×
102
}
×
103

104
// readCache attempts to read the cached spec from disk.
105
func (c *Cache) readCache() error {
×
106
        data, err := os.ReadFile(c.cachePath)
×
107
        if err != nil {
×
108
                return err
×
109
        }
×
110

111
        var cached CachedSpec
×
112
        if err := json.Unmarshal(data, &cached); err != nil {
×
113
                return err
×
114
        }
×
115

116
        c.spec = cached.Spec
×
117
        c.fetchedAt = cached.FetchedAt
×
118
        return nil
×
119
}
120

121
// saveCache saves the current spec to disk.
122
func (c *Cache) saveCache() error {
×
123
        cached := CachedSpec{
×
124
                Spec:      c.spec,
×
125
                FetchedAt: c.fetchedAt,
×
126
        }
×
127

×
128
        data, err := json.Marshal(cached)
×
129
        if err != nil {
×
130
                return err
×
131
        }
×
132

133
        // Ensure directory exists
134
        dir := filepath.Dir(c.cachePath)
×
NEW
135
        if err := os.MkdirAll(dir, dirPermissions); err != nil {
×
136
                return err
×
137
        }
×
138

NEW
139
        return os.WriteFile(c.cachePath, data, filePermissions)
×
140
}
141

142
// isExpired returns true if the cached spec has expired.
143
func (c *Cache) isExpired() bool {
×
144
        return time.Since(c.fetchedAt) > CacheTTL
×
145
}
×
146

147
// fetchSpec fetches the OpenAPI spec from the remote URL.
148
func (c *Cache) fetchSpec() error {
×
149
        ctx, cancel := context.WithTimeout(context.Background(), FetchTimeout)
×
150
        defer cancel()
×
151

×
NEW
152
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.specURL, http.NoBody)
×
153
        if err != nil {
×
154
                return fmt.Errorf("creating request: %w", err)
×
155
        }
×
156

157
        // Accept both JSON and YAML
158
        req.Header.Set("Accept", "application/json, application/yaml, text/yaml, */*")
×
159

×
160
        resp, err := http.DefaultClient.Do(req)
×
161
        if err != nil {
×
162
                return fmt.Errorf("fetching OpenAPI spec: %w", err)
×
163
        }
×
164
        defer resp.Body.Close()
×
165

×
166
        if resp.StatusCode != http.StatusOK {
×
167
                return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
×
168
        }
×
169

170
        body, err := io.ReadAll(resp.Body)
×
171
        if err != nil {
×
172
                return fmt.Errorf("reading response body: %w", err)
×
173
        }
×
174

175
        var spec OpenAPISpec
×
176
        contentType := resp.Header.Get("Content-Type")
×
177

×
178
        // Try to parse based on content type, fallback to trying both formats
×
NEW
179
        switch {
×
NEW
180
        case strings.Contains(contentType, "yaml") || strings.Contains(contentType, "yml"):
×
181
                if err := yaml.Unmarshal(body, &spec); err != nil {
×
182
                        return fmt.Errorf("parsing OpenAPI spec as YAML: %w", err)
×
183
                }
×
NEW
184
        case strings.Contains(contentType, "json"):
×
185
                if err := json.Unmarshal(body, &spec); err != nil {
×
186
                        return fmt.Errorf("parsing OpenAPI spec as JSON: %w", err)
×
187
                }
×
NEW
188
        default:
×
189
                // Content-Type not helpful, try YAML first (more common for OpenAPI), then JSON
×
190
                if err := yaml.Unmarshal(body, &spec); err != nil {
×
191
                        if err := json.Unmarshal(body, &spec); err != nil {
×
192
                                return fmt.Errorf("parsing OpenAPI spec (tried YAML and JSON): %w", err)
×
193
                        }
×
194
                }
195
        }
196

197
        c.spec = &spec
×
198
        c.fetchedAt = time.Now()
×
199
        return nil
×
200
}
201

202
// ClearCache removes the cached spec file.
203
func (c *Cache) ClearCache() error {
×
204
        return os.Remove(c.cachePath)
×
205
}
×
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