Skip to content

Commit efc8639

Browse files
committed
cache azure hosted fetch under the azure key
1 parent 07af582 commit efc8639

4 files changed

Lines changed: 119 additions & 46 deletions

File tree

internal/azure/logic.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,11 @@ func printListedValues(prefixes []Prefix, dim string) error {
119119
// fetchRawData mirrors utils.GetRawData's source dispatch but adds a
120120
// browser-UA scrape step when the resolved endpoint is the Microsoft
121121
// details.aspx page (Microsoft serves an anti-bot stub to non-browser UAs).
122+
// Hosted runs go through utils.GetCached so the resolved JSON gets cached
123+
// under the "azure" key — without that wrapper the scrape+download URL
124+
// path would refetch on every invocation (raw URLs aren't cached).
122125
func fetchRawData(ctx context.Context, source string) (string, error) {
123-
var endpointURL string
126+
var endpointURL, cacheKey string
124127
switch {
125128
case strings.HasPrefix(source, "https://") || strings.HasPrefix(source, "http://"):
126129
endpointURL = source
@@ -134,21 +137,23 @@ func fetchRawData(ctx context.Context, source string) (string, error) {
134137
if endpointURL == "" {
135138
endpointURL = utils.DefaultEndpoints[source]
136139
}
140+
cacheKey = source
137141
}
138142

139143
if endpointURL == "" {
140144
return utils.GetRawData(ctx, source)
141145
}
142146

143-
if strings.HasSuffix(strings.ToLower(endpointURL), ".json") {
144-
return utils.GetRawData(ctx, endpointURL)
145-
}
146-
147-
jsonURL, err := scrapeJSONURL(ctx, endpointURL)
148-
if err != nil {
149-
return "", err
150-
}
151-
return utils.GetRawData(ctx, jsonURL)
147+
return utils.GetCached(ctx, cacheKey, func(ctx context.Context) (string, error) {
148+
if strings.HasSuffix(strings.ToLower(endpointURL), ".json") {
149+
return utils.GetRawData(ctx, endpointURL)
150+
}
151+
jsonURL, err := scrapeJSONURL(ctx, endpointURL)
152+
if err != nil {
153+
return "", err
154+
}
155+
return utils.GetRawData(ctx, jsonURL)
156+
})
152157
}
153158

154159
func scrapeJSONURL(ctx context.Context, pageURL string) (string, error) {

internal/azure/logic_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import (
44
"bytes"
55
"context"
66
"io"
7+
"net/http"
8+
"net/http/httptest"
79
"os"
810
"path/filepath"
911
"testing"
1012

13+
"github.com/spf13/viper"
1114
"github.com/stretchr/testify/assert"
1215
"github.com/stretchr/testify/require"
1316
)
@@ -300,3 +303,57 @@ func TestGetIPRanges_List(t *testing.T) {
300303
})
301304
assert.Equal(t, "ActionGroup\nAzureStorage\n", out)
302305
}
306+
307+
// Regression: azure's two-stage hosted fetch (HTML scrape -> JSON download)
308+
// previously bypassed the cache because both steps reduced to raw URL calls
309+
// that GetRawData doesn't cache. Verify the cache wrap now keys the resolved
310+
// JSON under "azure" so a second call within TTL returns from disk.
311+
func TestFetchRawData_HostedCachesUnderAzureKey(t *testing.T) {
312+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
313+
t.Cleanup(func() { viper.Reset() })
314+
315+
jsonBody := loadFixture(t)
316+
317+
var hits int
318+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
319+
hits++
320+
_, _ = w.Write([]byte(jsonBody))
321+
}))
322+
defer srv.Close()
323+
324+
// Point at a .json endpoint to skip the page-scrape branch (HTTP-redirecting
325+
// download.microsoft.com URLs into a test server is more trouble than it's
326+
// worth here). What we're proving is that fetchRawData wraps its inner
327+
// work in GetCached at all — the scrape branch shares the same wrapper.
328+
viper.Set("azure_endpoint", srv.URL+"/ServiceTags_Public_20260101.json")
329+
330+
for i := 0; i < 2; i++ {
331+
got, err := fetchRawData(context.Background(), "azure")
332+
require.NoError(t, err)
333+
assert.Equal(t, jsonBody, got)
334+
}
335+
assert.Equal(t, 1, hits, "second call should hit cache, not refetch")
336+
}
337+
338+
func TestFetchRawData_NoCacheBypass(t *testing.T) {
339+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
340+
t.Cleanup(func() { viper.Reset() })
341+
342+
jsonBody := loadFixture(t)
343+
344+
var hits int
345+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
346+
hits++
347+
_, _ = w.Write([]byte(jsonBody))
348+
}))
349+
defer srv.Close()
350+
351+
viper.Set("azure_endpoint", srv.URL+"/x.json")
352+
viper.Set("no_cache", true)
353+
354+
for i := 0; i < 2; i++ {
355+
_, err := fetchRawData(context.Background(), "azure")
356+
require.NoError(t, err)
357+
}
358+
assert.Equal(t, 2, hits, "--no-cache should refetch every call")
359+
}

internal/utils/cache.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,55 @@
11
package utils
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"os"
78
"path/filepath"
89
"time"
10+
11+
"github.com/spf13/viper"
912
)
1013

14+
// GetCached wraps fetch with the standard cache-aside policy: read on hit,
15+
// run fetch on miss, write the result. If key is "" or --no-cache is set,
16+
// fetch runs unconditionally and the cache is left untouched. Cache write
17+
// failures are logged but never fatal. Suitable for providers (e.g. azure)
18+
// whose fetch path doesn't reduce to a single utils.GetRawData call.
19+
func GetCached(ctx context.Context, key string, fetch func(context.Context) (string, error)) (string, error) {
20+
if key == "" || viper.GetBool("no_cache") {
21+
return fetch(ctx)
22+
}
23+
ttl := resolveCacheTTL(key)
24+
if ttl > 0 {
25+
if data, ok, _ := readCache(key, ttl); ok {
26+
fmt.Fprintf(os.Stderr, "Using cached IP ranges for %s (age %s)\n", key, cacheAge(key))
27+
return string(data), nil
28+
}
29+
}
30+
body, err := fetch(ctx)
31+
if err != nil {
32+
return "", err
33+
}
34+
if werr := writeCache(key, []byte(body)); werr != nil {
35+
fmt.Fprintln(os.Stderr, "Warning: cache write failed:", werr)
36+
}
37+
return body, nil
38+
}
39+
40+
func resolveCacheTTL(key string) time.Duration {
41+
raw := viper.GetString(key + "_cache_ttl")
42+
if raw == "" {
43+
return defaultCacheTTL
44+
}
45+
ttl, err := time.ParseDuration(raw)
46+
if err != nil {
47+
fmt.Fprintf(os.Stderr, "Warning: invalid %s_cache_ttl=%q, using %s\n", key, raw, defaultCacheTTL)
48+
return defaultCacheTTL
49+
}
50+
return ttl
51+
}
52+
1153
func cacheDir() (string, error) {
1254
if dir := os.Getenv("XDG_CACHE_HOME"); dir != "" {
1355
return filepath.Join(dir, "cipr"), nil

internal/utils/raw_data_handler.go

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func GetRawData(ctx context.Context, source string) (string, error) {
3737
if endpointURL == "" && localFile == "" {
3838
endpointURL = DefaultEndpoints[source]
3939
}
40-
if localFile == "" && !viper.GetBool("no_cache") {
40+
if localFile == "" {
4141
cacheKey = source
4242
}
4343
}
@@ -51,41 +51,10 @@ func GetRawData(ctx context.Context, source string) (string, error) {
5151
return "", fmt.Errorf("no endpoint URL or local file specified for source %q", source)
5252
}
5353

54-
if cacheKey != "" {
55-
ttl := resolveCacheTTL(cacheKey)
56-
if ttl > 0 {
57-
if data, ok, _ := readCache(cacheKey, ttl); ok {
58-
fmt.Fprintf(os.Stderr, "Using cached IP ranges for %s (age %s)\n", cacheKey, cacheAge(cacheKey))
59-
return string(data), nil
60-
}
61-
}
62-
}
63-
64-
fmt.Fprintln(os.Stderr, "Fetching IP ranges from endpoint:", endpointURL)
65-
body, err := loadFromEndpoint(ctx, endpointURL)
66-
if err != nil {
67-
return "", err
68-
}
69-
70-
if cacheKey != "" {
71-
if werr := writeCache(cacheKey, []byte(body)); werr != nil {
72-
fmt.Fprintln(os.Stderr, "Warning: cache write failed:", werr)
73-
}
74-
}
75-
return body, nil
76-
}
77-
78-
func resolveCacheTTL(key string) time.Duration {
79-
raw := viper.GetString(key + "_cache_ttl")
80-
if raw == "" {
81-
return defaultCacheTTL
82-
}
83-
ttl, err := time.ParseDuration(raw)
84-
if err != nil {
85-
fmt.Fprintf(os.Stderr, "Warning: invalid %s_cache_ttl=%q, using %s\n", key, raw, defaultCacheTTL)
86-
return defaultCacheTTL
87-
}
88-
return ttl
54+
return GetCached(ctx, cacheKey, func(ctx context.Context) (string, error) {
55+
fmt.Fprintln(os.Stderr, "Fetching IP ranges from endpoint:", endpointURL)
56+
return loadFromEndpoint(ctx, endpointURL)
57+
})
8958
}
9059

9160
func loadFromFile(filePath string) (string, error) {

0 commit comments

Comments
 (0)