@@ -2,7 +2,9 @@ package config
22
33import (
44 "cmp"
5+ "context"
56 "encoding/json"
7+ "errors"
68 "fmt"
79 "log/slog"
810 "os"
@@ -17,7 +19,7 @@ import (
1719)
1820
1921type ProviderClient interface {
20− GetProviders() ([]catwalk.Provider, error)
22+ GetProviders(context.Context, string) ([]catwalk.Provider, error)
2123}
2224
2325var (
@@ -53,7 +55,7 @@ func saveProvidersInCache(path string, providers []catwalk.Provider) error {
5355 return fmt.Errorf("failed to create directory for provider cache: %w", err)
5456 }
5557
56− data, err := json.MarshalIndent(providers, "", " ")
58+ data, err := json.Marshal(providers)
5759 if err != nil {
5860 return fmt.Errorf("failed to marshal provider data: %w", err)
5961 }
@@ -64,34 +66,35 @@ func saveProvidersInCache(path string, providers []catwalk.Provider) error {
6466 return nil
6567}
6668
67−func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
69+func loadProvidersFromCache(path string) ([]catwalk.Provider, string, error) {
6870 data, err := os.ReadFile(path)
6971 if err != nil {
70− return nil, fmt.Errorf("failed to read provider cache file: %w", err)
72+ return nil, "", fmt.Errorf("failed to read provider cache file: %w", err)
7173 }
7274
7375 var providers []catwalk.Provider
7476 if err := json.Unmarshal(data, &providers); err != nil {
75− return nil, fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
77+ return nil, "", fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
7678 }
77− return providers, nil
79+
80+ return providers, catwalk.Etag(data), nil
7881}
7982
80−func UpdateProviders(pathOrUrl string) error {
83+func UpdateProviders(pathOrURL string) error {
8184 var providers []catwalk.Provider
82− pathOrUrl = cmp.Or(pathOrUrl, os.Getenv("CATWALK_URL"), defaultCatwalkURL)
85+ pathOrURL = cmp.Or(pathOrURL, os.Getenv("CATWALK_URL"), defaultCatwalkURL)
8386
8487 switch {
85− case pathOrUrl == "embedded":
88+ case pathOrURL == "embedded":
8689 providers = embedded.GetAll()
87− case strings.HasPrefix(pathOrUrl, "http://") || strings.HasPrefix(pathOrUrl, "https://"):
90+ case strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://"):
8891 var err error
89− providers, err = catwalk.NewWithURL(pathOrUrl).GetProviders()
92+ providers, err = catwalk.NewWithURL(pathOrURL).GetProviders(context.Background(), "")
9093 if err != nil {
9194 return fmt.Errorf("failed to fetch providers from Catwalk: %w", err)
9295 }
9396 default:
94− content, err := os.ReadFile(pathOrUrl)
97+ content, err := os.ReadFile(pathOrURL)
9598 if err != nil {
9699 return fmt.Errorf("failed to read file: %w", err)
97100 }
@@ -108,61 +111,61 @@ func UpdateProviders(pathOrUrl string) error {
108111 return fmt.Errorf("failed to save providers to cache: %w", err)
109112 }
110113
111− slog.Info("Providers updated successfully", "count", len(providers), "from", pathOrUrl, "to", cachePath)
114+ slog.Info("Providers updated successfully", "count", len(providers), "from", pathOrURL, "to", cachePath)
112115 return nil
113116}
114117
118+// Providers returns the list of providers, taking into account cached results
119+// and whether or not auto update is enabled.
120+//
121+// It will:
122+// 1. if auto update is disabled, it'll return the embedded providers at the
123+// time of release.
124+// 2. load the cached providers
125+// 3. try to get the fresh list of providers, and return either this new list,
126+// the cached list, or the embedded list if all others fail.
115127func Providers(cfg *Config) ([]catwalk.Provider, error) {
116128 providerOnce.Do(func() {
117129 catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
118130 client := catwalk.NewWithURL(catwalkURL)
119131 path := providerCacheFileData()
120132
121− autoUpdateDisabled := cfg.Options.DisableProviderAutoUpdate
122− providerList, providerErr = loadProviders(autoUpdateDisabled, client, path)
123− })
124− return providerList, providerErr
125−}
126−
127−func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) ([]catwalk.Provider, error) {
128− catwalkGetAndSave := func() ([]catwalk.Provider, error) {
129− providers, err := client.GetProviders()
130− if err != nil {
131− return nil, fmt.Errorf("failed to fetch providers from catwalk: %w", err)
133+ if cfg.Options.DisableProviderAutoUpdate {
134+ slog.Info("Using embedded Catwalk providers")
135+ providerList, providerErr = embedded.GetAll(), nil
136+ return
132137 }
133− if len(providers) == 0 {
134− return nil, fmt.Errorf("empty providers list from catwalk")
135− }
136− if err := saveProvidersInCache(path, providers); err != nil {
137− return nil, err
138− }
139− return providers, nil
140− }
141−
142− switch {
143− case autoUpdateDisabled:
144− slog.Warn("Providers auto-update is disabled")
145138
146− if _, err := os.Stat(path); err == nil {
147− slog.Warn("Using locally cached providers")
148− return loadProvidersFromCache(path)
139+ cached, etag, cachedErr := loadProvidersFromCache(path)
140+ if len(cached) == 0 || cachedErr != nil {
141+ // if cached file is empty, default to embedded providers
142+ cached = embedded.GetAll()
149143 }
150144
151− slog.Warn("Saving embedded providers to cache")
152− providers := embedded.GetAll()
153− if err := saveProvidersInCache(path, providers); err != nil {
154− return nil, err
145+ providerList, providerErr = loadProviders(client, etag, path)
146+ if errors.Is(providerErr, catwalk.ErrNotModified) {
147+ slog.Info("Catwalk providers not modified")
148+ providerList, providerErr = cached, nil
155149 }
156− return providers, nil
157−
158− default:
159− slog.Info("Fetching providers from Catwalk.", "path", path)
150+ })
151+ if providerErr != nil {
152+ catwalkURL := fmt.Sprintf("%s/v2/providers", cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL))
153+ return nil, fmt.Errorf("Crush was unable to fetch an updated list of providers from %s. Consider setting CRUSH_DISABLE_PROVIDER_AUTO_UPDATE=1 to use the embedded providers bundled at the time of this Crush release. You can also update providers manually. For more info see crush update-providers --help.\n\nCause: %w", catwalkURL, providerErr) //nolint:staticcheck
154+ }
155+ return providerList, nil
156+}
160157
161− providers, err := catwalkGetAndSave()
162− if err != nil {
163− catwalkUrl := fmt.Sprintf("%s/v2/providers", cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL))
164− return nil, fmt.Errorf("Crush was unable to fetch an updated list of providers from %s. Consider setting CRUSH_DISABLE_PROVIDER_AUTO_UPDATE=1 to use the embedded providers bundled at the time of this Crush release. You can also update providers manually. For more info see crush update-providers --help. %w", catwalkUrl, err) //nolint:staticcheck
165− }
166− return providers, nil
158+func loadProviders(client ProviderClient, etag, path string) ([]catwalk.Provider, error) {
159+ slog.Info("Fetching providers from Catwalk.", "path", path)
160+ providers, err := client.GetProviders(context.Background(), etag)
161+ if err != nil {
162+ return nil, fmt.Errorf("failed to fetch providers from catwalk: %w", err)
163+ }
164+ if len(providers) == 0 {
165+ return nil, errors.New("empty providers list from catwalk")
167166 }
167+ if err := saveProvidersInCache(path, providers); err != nil {
168+ return nil, err
169+ }
170+ return providers, nil
168171}