From a88ae8c103cf002e1bbe9b0da44652ff1b07f251 Mon Sep 17 00:00:00 2001 From: Malthe Poulsen Date: Wed, 23 Jul 2025 15:55:17 +0200 Subject: [PATCH 1/6] Add currency conversion support for MongoDB Atlas plugin - Create generic currency package for reuse across plugins - Implement exchange rate fetching from exchangerate-api.com - Add thread-safe caching with 24-hour TTL - Support all ISO 4217 currency codes - Update MongoDB Atlas plugin to support currency conversion - Add configuration for target currency and API key - Include comprehensive tests for currency conversion Signed-off-by: Malthe Poulsen --- pkg/common/currency/README.md | 64 +++++ pkg/common/currency/cache.go | 99 +++++++ pkg/common/currency/cache_test.go | 170 +++++++++++ pkg/common/currency/client.go | 93 ++++++ pkg/common/currency/converter.go | 135 +++++++++ pkg/common/currency/converter_test.go | 269 ++++++++++++++++++ pkg/common/currency/go.mod | 3 + pkg/common/currency/types.go | 63 ++++ pkg/plugins/mongodb-atlas/cmd/main/main.go | 62 +++- .../cmd/main/main_currency_test.go | 120 ++++++++ .../mongodb-atlas/config/atlasconfig.go | 14 +- pkg/plugins/mongodb-atlas/go.mod | 3 + 12 files changed, 1083 insertions(+), 12 deletions(-) create mode 100644 pkg/common/currency/README.md create mode 100644 pkg/common/currency/cache.go create mode 100644 pkg/common/currency/cache_test.go create mode 100644 pkg/common/currency/client.go create mode 100644 pkg/common/currency/converter.go create mode 100644 pkg/common/currency/converter_test.go create mode 100644 pkg/common/currency/go.mod create mode 100644 pkg/common/currency/types.go create mode 100644 pkg/plugins/mongodb-atlas/cmd/main/main_currency_test.go diff --git a/pkg/common/currency/README.md b/pkg/common/currency/README.md new file mode 100644 index 0000000..9e9dae8 --- /dev/null +++ b/pkg/common/currency/README.md @@ -0,0 +1,64 @@ +# Currency Package + +Convert costs between currencies in OpenCost plugins using live exchange rates. + +## Quick Start + +```go +import "github.com/opencost/opencost-plugins/pkg/common/currency" + +config := currency.Config{ + APIKey: "your-api-key", + CacheTTL: 24 * time.Hour, +} + +converter, err := currency.NewConverter(config) +if err != nil { + log.Fatal(err) +} + +// Convert 100 USD to EUR +amount, err := converter.Convert(100.0, "USD", "EUR") +``` + +## Setup + +Get a free API key from [exchangerate-api.com](https://www.exchangerate-api.com/) (1,500 requests/month). + +## How it Works + +The package fetches exchange rates and caches them for 24 hours. This keeps API usage low - most plugins use under 50 requests per month. + +Supports all ISO 4217 currencies (161 total). Thread-safe with automatic cache cleanup. + +## MongoDB Atlas Example + +```go +// Config +type AtlasConfig struct { + TargetCurrency string `json:"target_currency"` + ExchangeAPIKey string `json:"exchange_api_key"` +} + +// Usage +if atlasConfig.ExchangeAPIKey != "" { + converter, _ := currency.NewConverter(currency.Config{ + APIKey: atlasConfig.ExchangeAPIKey, + CacheTTL: 24 * time.Hour, + }) +} + +// Convert costs +if converter != nil { + cost, _ = converter.Convert(cost, "USD", targetCurrency) +} +``` + +## Testing + +```bash +cd pkg/common/currency +go test -v +``` + +Tests use mocks - no API calls needed. \ No newline at end of file diff --git a/pkg/common/currency/cache.go b/pkg/common/currency/cache.go new file mode 100644 index 0000000..d2e4c93 --- /dev/null +++ b/pkg/common/currency/cache.go @@ -0,0 +1,99 @@ +package currency + +import ( + "sync" + "time" +) + +type MemoryCache struct { + mu sync.RWMutex + data map[string]*CachedRates + ttl time.Duration + janitor *time.Ticker +} + +func NewMemoryCache(ttl time.Duration) *MemoryCache { + if ttl == 0 { + ttl = 24 * time.Hour + } + + cache := &MemoryCache{ + data: make(map[string]*CachedRates), + ttl: ttl, + janitor: time.NewTicker(ttl / 2), + } + + go cache.cleanup() + + return cache +} + +func (c *MemoryCache) Get(baseCurrency string) (*CachedRates, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + + rates, exists := c.data[baseCurrency] + if !exists { + return nil, false + } + + if time.Now().After(rates.ValidUntil) { + return nil, false + } + + return rates, true +} + +func (c *MemoryCache) Set(baseCurrency string, rates *CachedRates) { + c.mu.Lock() + defer c.mu.Unlock() + + rates.ValidUntil = rates.FetchedAt.Add(c.ttl) + c.data[baseCurrency] = rates +} + +func (c *MemoryCache) Clear() { + c.mu.Lock() + defer c.mu.Unlock() + + c.data = make(map[string]*CachedRates) +} + +func (c *MemoryCache) cleanup() { + for range c.janitor.C { + c.removeExpired() + } +} + +func (c *MemoryCache) removeExpired() { + c.mu.Lock() + defer c.mu.Unlock() + + now := time.Now() + for key, rates := range c.data { + if now.After(rates.ValidUntil) { + delete(c.data, key) + } + } +} + +func (c *MemoryCache) Stop() { + if c.janitor != nil { + c.janitor.Stop() + } +} + +func (c *MemoryCache) Stats() (entries int, oldestEntry time.Time) { + c.mu.RLock() + defer c.mu.RUnlock() + + entries = len(c.data) + + for _, rates := range c.data { + if oldestEntry.IsZero() || rates.FetchedAt.Before(oldestEntry) { + oldestEntry = rates.FetchedAt + } + } + + return entries, oldestEntry +} \ No newline at end of file diff --git a/pkg/common/currency/cache_test.go b/pkg/common/currency/cache_test.go new file mode 100644 index 0000000..2815cf2 --- /dev/null +++ b/pkg/common/currency/cache_test.go @@ -0,0 +1,170 @@ +package currency + +import ( + "testing" + "time" +) + +func TestMemoryCache_SetAndGet(t *testing.T) { + cache := NewMemoryCache(1 * time.Hour) + defer cache.Stop() + + // Test setting and getting rates + rates := &CachedRates{ + Rates: map[string]float64{ + "EUR": 0.85, + "GBP": 0.73, + }, + BaseCode: "USD", + FetchedAt: time.Now(), + } + + cache.Set("USD", rates) + + // Test successful get + retrieved, found := cache.Get("USD") + if !found { + t.Error("expected to find cached rates") + } + + if retrieved.BaseCode != "USD" { + t.Errorf("expected base code USD, got %s", retrieved.BaseCode) + } + + if len(retrieved.Rates) != 2 { + t.Errorf("expected 2 rates, got %d", len(retrieved.Rates)) + } + + // Test non-existent key + _, found = cache.Get("EUR") + if found { + t.Error("expected not to find rates for EUR") + } +} + +func TestMemoryCache_Expiration(t *testing.T) { + // Use short TTL for testing + cache := NewMemoryCache(100 * time.Millisecond) + defer cache.Stop() + + rates := &CachedRates{ + Rates: map[string]float64{ + "EUR": 0.85, + }, + BaseCode: "USD", + FetchedAt: time.Now(), + } + + cache.Set("USD", rates) + + // Should find it immediately + _, found := cache.Get("USD") + if !found { + t.Error("expected to find cached rates immediately") + } + + // Wait for expiration + time.Sleep(150 * time.Millisecond) + + // Should not find it after expiration + _, found = cache.Get("USD") + if found { + t.Error("expected rates to be expired") + } +} + +func TestMemoryCache_Clear(t *testing.T) { + cache := NewMemoryCache(1 * time.Hour) + defer cache.Stop() + + // Add multiple entries + for _, base := range []string{"USD", "EUR", "GBP"} { + rates := &CachedRates{ + Rates: map[string]float64{"TEST": 1.0}, + BaseCode: base, + FetchedAt: time.Now(), + } + cache.Set(base, rates) + } + + // Verify all entries exist + for _, base := range []string{"USD", "EUR", "GBP"} { + _, found := cache.Get(base) + if !found { + t.Errorf("expected to find rates for %s", base) + } + } + + // Clear cache + cache.Clear() + + // Verify all entries are gone + for _, base := range []string{"USD", "EUR", "GBP"} { + _, found := cache.Get(base) + if found { + t.Errorf("expected not to find rates for %s after clear", base) + } + } +} + +func TestMemoryCache_Stats(t *testing.T) { + cache := NewMemoryCache(1 * time.Hour) + defer cache.Stop() + + // Initially empty + entries, _ := cache.Stats() + if entries != 0 { + t.Errorf("expected 0 entries, got %d", entries) + } + + // Add entries + now := time.Now() + for i, base := range []string{"USD", "EUR", "GBP"} { + rates := &CachedRates{ + Rates: map[string]float64{"TEST": 1.0}, + BaseCode: base, + FetchedAt: now.Add(time.Duration(i) * time.Minute), + } + cache.Set(base, rates) + } + + entries, oldest := cache.Stats() + if entries != 3 { + t.Errorf("expected 3 entries, got %d", entries) + } + + // The oldest should be the first one we added (USD) + if !oldest.Equal(now) { + t.Errorf("expected oldest entry to be %v, got %v", now, oldest) + } +} + +func TestMemoryCache_Cleanup(t *testing.T) { + // Use very short TTL for testing + cache := NewMemoryCache(50 * time.Millisecond) + defer cache.Stop() + + // Add entry + rates := &CachedRates{ + Rates: map[string]float64{"EUR": 0.85}, + BaseCode: "USD", + FetchedAt: time.Now(), + } + cache.Set("USD", rates) + + // Verify it exists + entries, _ := cache.Stats() + if entries != 1 { + t.Errorf("expected 1 entry, got %d", entries) + } + + // Wait for cleanup cycle (janitor runs every TTL/2 = 25ms) + // Wait a bit longer to ensure cleanup has run + time.Sleep(100 * time.Millisecond) + + // Verify it's been cleaned up + entries, _ = cache.Stats() + if entries != 0 { + t.Errorf("expected 0 entries after cleanup, got %d", entries) + } +} \ No newline at end of file diff --git a/pkg/common/currency/client.go b/pkg/common/currency/client.go new file mode 100644 index 0000000..e76eae0 --- /dev/null +++ b/pkg/common/currency/client.go @@ -0,0 +1,93 @@ +package currency + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const ( + apiBaseURL = "https://v6.exchangerate-api.com/v6" + userAgent = "opencost-plugins/1.0" +) + +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +type ExchangeRateClient struct { + apiKey string + httpClient HTTPClient + timeout time.Duration +} + +func NewExchangeRateClient(apiKey string, timeout time.Duration) *ExchangeRateClient { + if timeout == 0 { + timeout = 10 * time.Second + } + + return &ExchangeRateClient{ + apiKey: apiKey, + httpClient: &http.Client{ + Timeout: timeout, + }, + timeout: timeout, + } +} + +func (c *ExchangeRateClient) FetchRates(baseCurrency string) (*ExchangeRateResponse, error) { + if c.apiKey == "" { + return nil, fmt.Errorf("API key is required") + } + + if baseCurrency == "" { + baseCurrency = "USD" + } + + url := fmt.Sprintf("%s/%s/latest/%s", apiBaseURL, c.apiKey, baseCurrency) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("User-Agent", userAgent) + req.Header.Set("Accept", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch exchange rates: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var response ExchangeRateResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + if response.Result != "success" { + return nil, fmt.Errorf("API returned error result: %s", response.Result) + } + + if len(response.ConversionRates) == 0 { + return nil, fmt.Errorf("no conversion rates returned") + } + + return &response, nil +} + +func (c *ExchangeRateClient) SetHTTPClient(client HTTPClient) { + c.httpClient = client +} \ No newline at end of file diff --git a/pkg/common/currency/converter.go b/pkg/common/currency/converter.go new file mode 100644 index 0000000..50ccb24 --- /dev/null +++ b/pkg/common/currency/converter.go @@ -0,0 +1,135 @@ +package currency + +import ( + "fmt" + "strings" + "sync" + "time" +) + +type CurrencyConverter struct { + client Client + cache Cache + config Config + mu sync.RWMutex +} + +func NewConverter(config Config) (*CurrencyConverter, error) { + if config.APIKey == "" { + return nil, fmt.Errorf("API key is required") + } + + if config.CacheTTL == 0 { + config.CacheTTL = 24 * time.Hour + } + + if config.APITimeout == 0 { + config.APITimeout = 10 * time.Second + } + + client := NewExchangeRateClient(config.APIKey, config.APITimeout) + cache := NewMemoryCache(config.CacheTTL) + + return &CurrencyConverter{ + client: client, + cache: cache, + config: config, + }, nil +} + +func (c *CurrencyConverter) Convert(amount float64, from, to string) (float64, error) { + from = strings.ToUpper(strings.TrimSpace(from)) + to = strings.ToUpper(strings.TrimSpace(to)) + + if from == to { + return amount, nil + } + + rate, err := c.GetRate(from, to) + if err != nil { + return 0, fmt.Errorf("failed to get exchange rate from %s to %s: %w", from, to, err) + } + + return amount * rate, nil +} + +func (c *CurrencyConverter) GetRate(from, to string) (float64, error) { + from = strings.ToUpper(strings.TrimSpace(from)) + to = strings.ToUpper(strings.TrimSpace(to)) + + if from == to { + return 1.0, nil + } + + cachedRates, found := c.cache.Get(from) + if found && cachedRates.Rates != nil { + if rate, exists := cachedRates.Rates[to]; exists { + return rate, nil + } + } + + rates, err := c.fetchAndCacheRates(from) + if err != nil { + return 0, err + } + + rate, exists := rates[to] + if !exists { + return 0, fmt.Errorf("currency %s not supported or not found in exchange rates", to) + } + + return rate, nil +} + +func (c *CurrencyConverter) GetSupportedCurrencies() ([]string, error) { + rates, err := c.fetchAndCacheRates("USD") + if err != nil { + return nil, fmt.Errorf("failed to fetch supported currencies: %w", err) + } + + currencies := make([]string, 0, len(rates)) + for currency := range rates { + currencies = append(currencies, currency) + } + + return currencies, nil +} + +func (c *CurrencyConverter) fetchAndCacheRates(baseCurrency string) (map[string]float64, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if cachedRates, found := c.cache.Get(baseCurrency); found { + return cachedRates.Rates, nil + } + + response, err := c.client.FetchRates(baseCurrency) + if err != nil { + return nil, fmt.Errorf("failed to fetch rates from API: %w", err) + } + + cachedRates := &CachedRates{ + Rates: response.ConversionRates, + BaseCode: response.BaseCode, + FetchedAt: time.Now(), + } + c.cache.Set(baseCurrency, cachedRates) + + return response.ConversionRates, nil +} + +func (c *CurrencyConverter) SetClient(client Client) { + c.mu.Lock() + defer c.mu.Unlock() + c.client = client +} + +func (c *CurrencyConverter) SetCache(cache Cache) { + c.mu.Lock() + defer c.mu.Unlock() + c.cache = cache +} + +func (c *CurrencyConverter) ClearCache() { + c.cache.Clear() +} \ No newline at end of file diff --git a/pkg/common/currency/converter_test.go b/pkg/common/currency/converter_test.go new file mode 100644 index 0000000..0afc996 --- /dev/null +++ b/pkg/common/currency/converter_test.go @@ -0,0 +1,269 @@ +package currency + +import ( + "fmt" + "testing" + "time" +) + +type MockClient struct { + rates map[string]map[string]float64 + err error +} + +func (m *MockClient) FetchRates(baseCurrency string) (*ExchangeRateResponse, error) { + if m.err != nil { + return nil, m.err + } + + rates, exists := m.rates[baseCurrency] + if !exists { + return nil, fmt.Errorf("no rates for base currency %s", baseCurrency) + } + + return &ExchangeRateResponse{ + Result: "success", + BaseCode: baseCurrency, + ConversionRates: rates, + }, nil +} + +type MockCache struct { + data map[string]*CachedRates +} + +func NewMockCache() *MockCache { + return &MockCache{ + data: make(map[string]*CachedRates), + } +} + +func (m *MockCache) Get(baseCurrency string) (*CachedRates, bool) { + rates, exists := m.data[baseCurrency] + if !exists || time.Now().After(rates.ValidUntil) { + return nil, false + } + return rates, true +} + +func (m *MockCache) Set(baseCurrency string, rates *CachedRates) { + m.data[baseCurrency] = rates +} + +func (m *MockCache) Clear() { + m.data = make(map[string]*CachedRates) +} + +func TestCurrencyConverter_Convert(t *testing.T) { + mockClient := &MockClient{ + rates: map[string]map[string]float64{ + "USD": { + "USD": 1.0, + "EUR": 0.85, + "GBP": 0.73, + "JPY": 110.0, + }, + "EUR": { + "EUR": 1.0, + "USD": 1.18, + "GBP": 0.86, + "JPY": 129.53, + }, + }, + } + + converter := &CurrencyConverter{ + client: mockClient, + cache: NewMockCache(), + config: Config{APIKey: "test"}, + } + + tests := []struct { + name string + amount float64 + from string + to string + expected float64 + expectError bool + }{ + { + name: "USD to EUR", + amount: 100, + from: "USD", + to: "EUR", + expected: 85, + }, + { + name: "USD to GBP", + amount: 100, + from: "USD", + to: "GBP", + expected: 73, + }, + { + name: "EUR to USD", + amount: 100, + from: "EUR", + to: "USD", + expected: 118, + }, + { + name: "Same currency", + amount: 100, + from: "USD", + to: "USD", + expected: 100, + }, + { + name: "Case insensitive", + amount: 100, + from: "usd", + to: "eur", + expected: 85, + }, + { + name: "Unsupported currency", + amount: 100, + from: "USD", + to: "XYZ", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := converter.Convert(tt.amount, tt.from, tt.to) + + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if result != tt.expected { + t.Errorf("expected %f, got %f", tt.expected, result) + } + }) + } +} + +func TestCurrencyConverter_GetRate(t *testing.T) { + mockClient := &MockClient{ + rates: map[string]map[string]float64{ + "USD": { + "USD": 1.0, + "EUR": 0.85, + "GBP": 0.73, + }, + }, + } + + converter := &CurrencyConverter{ + client: mockClient, + cache: NewMockCache(), + config: Config{APIKey: "test"}, + } + + // Test getting rate + rate, err := converter.GetRate("USD", "EUR") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if rate != 0.85 { + t.Errorf("expected rate 0.85, got %f", rate) + } + + // Test same currency + rate, err = converter.GetRate("USD", "USD") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if rate != 1.0 { + t.Errorf("expected rate 1.0, got %f", rate) + } + + // Test cache hit + rate, err = converter.GetRate("USD", "EUR") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if rate != 0.85 { + t.Errorf("expected cached rate 0.85, got %f", rate) + } +} + +func TestCurrencyConverter_GetSupportedCurrencies(t *testing.T) { + mockClient := &MockClient{ + rates: map[string]map[string]float64{ + "USD": { + "USD": 1.0, + "EUR": 0.85, + "GBP": 0.73, + "JPY": 110.0, + }, + }, + } + + converter := &CurrencyConverter{ + client: mockClient, + cache: NewMockCache(), + config: Config{APIKey: "test"}, + } + + currencies, err := converter.GetSupportedCurrencies() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(currencies) != 4 { + t.Errorf("expected 4 currencies, got %d", len(currencies)) + } + + // Check that all expected currencies are present + expected := map[string]bool{ + "USD": false, + "EUR": false, + "GBP": false, + "JPY": false, + } + + for _, curr := range currencies { + if _, exists := expected[curr]; exists { + expected[curr] = true + } + } + + for curr, found := range expected { + if !found { + t.Errorf("expected currency %s not found", curr) + } + } +} + +func TestNewConverter(t *testing.T) { + // Test with empty API key + _, err := NewConverter(Config{}) + if err == nil { + t.Error("expected error for empty API key") + } + + // Test with valid config + converter, err := NewConverter(Config{APIKey: "test-key"}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if converter.config.CacheTTL != 24*time.Hour { + t.Errorf("expected default cache TTL of 24h, got %v", converter.config.CacheTTL) + } + + if converter.config.APITimeout != 10*time.Second { + t.Errorf("expected default API timeout of 10s, got %v", converter.config.APITimeout) + } +} \ No newline at end of file diff --git a/pkg/common/currency/go.mod b/pkg/common/currency/go.mod new file mode 100644 index 0000000..6dc42ee --- /dev/null +++ b/pkg/common/currency/go.mod @@ -0,0 +1,3 @@ +module github.com/opencost/opencost-plugins/pkg/common/currency + +go 1.21 \ No newline at end of file diff --git a/pkg/common/currency/types.go b/pkg/common/currency/types.go new file mode 100644 index 0000000..a9de39d --- /dev/null +++ b/pkg/common/currency/types.go @@ -0,0 +1,63 @@ +package currency + +import ( + "time" +) + +// ExchangeRateResponse represents the API response from exchangerate-api.com +type ExchangeRateResponse struct { + Result string `json:"result"` + Documentation string `json:"documentation"` + TermsOfUse string `json:"terms_of_use"` + TimeLastUpdateUnix int64 `json:"time_last_update_unix"` + TimeLastUpdateUTC string `json:"time_last_update_utc"` + TimeNextUpdateUnix int64 `json:"time_next_update_unix"` + TimeNextUpdateUTC string `json:"time_next_update_utc"` + BaseCode string `json:"base_code"` + ConversionRates map[string]float64 `json:"conversion_rates"` +} + +// CachedRates stores exchange rates with metadata +type CachedRates struct { + Rates map[string]float64 + BaseCode string + FetchedAt time.Time + ValidUntil time.Time +} + +// Converter interface defines currency conversion operations +type Converter interface { + // Convert converts an amount from one currency to another + Convert(amount float64, from, to string) (float64, error) + + // GetRate returns the exchange rate between two currencies + GetRate(from, to string) (float64, error) + + // GetSupportedCurrencies returns a list of supported currency codes + GetSupportedCurrencies() ([]string, error) +} + +// Client interface for fetching exchange rates +type Client interface { + // FetchRates fetches current exchange rates for a base currency + FetchRates(baseCurrency string) (*ExchangeRateResponse, error) +} + +// Cache interface for storing exchange rates +type Cache interface { + // Get retrieves cached rates for a base currency + Get(baseCurrency string) (*CachedRates, bool) + + // Set stores rates for a base currency with TTL + Set(baseCurrency string, rates *CachedRates) + + // Clear removes all cached rates + Clear() +} + +// Config holds configuration for the currency converter +type Config struct { + APIKey string + CacheTTL time.Duration + APITimeout time.Duration +} \ No newline at end of file diff --git a/pkg/plugins/mongodb-atlas/cmd/main/main.go b/pkg/plugins/mongodb-atlas/cmd/main/main.go index 9738051..3974b40 100644 --- a/pkg/plugins/mongodb-atlas/cmd/main/main.go +++ b/pkg/plugins/mongodb-atlas/cmd/main/main.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/go-plugin" "github.com/icholy/digest" commonconfig "github.com/opencost/opencost-plugins/common/config" + "github.com/opencost/opencost-plugins/pkg/common/currency" atlasconfig "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/config" atlasplugin "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/plugin" "github.com/opencost/opencost/core/pkg/log" @@ -50,9 +51,26 @@ func main() { // as per https://www.mongodb.com/docs/atlas/api/atlas-admin-api-ref/, // atlas admin APIs have a limit of 100 requests per minute rateLimiter := rate.NewLimiter(1.1, 2) + + var currencyConverter currency.Converter + if atlasConfig.ExchangeAPIKey != "" && atlasConfig.TargetCurrency != "USD" { + converter, err := currency.NewConverter(currency.Config{ + APIKey: atlasConfig.ExchangeAPIKey, + CacheTTL: 24 * time.Hour, + }) + if err != nil { + log.Warnf("Failed to initialize currency converter: %v. Will use USD.", err) + } else { + currencyConverter = converter + log.Infof("Currency converter initialized for target currency: %s", atlasConfig.TargetCurrency) + } + } + atlasCostSrc := AtlasCostSource{ - rateLimiter: rateLimiter, - orgID: atlasConfig.OrgID, + rateLimiter: rateLimiter, + orgID: atlasConfig.OrgID, + targetCurrency: atlasConfig.TargetCurrency, + currencyConverter: currencyConverter, } atlasCostSrc.atlasClient = getAtlasClient(*atlasConfig) @@ -80,9 +98,11 @@ func getAtlasClient(atlasConfig atlasconfig.AtlasConfig) HTTPClient { // Implementation of CustomCostSource type AtlasCostSource struct { - orgID string - rateLimiter *rate.Limiter - atlasClient HTTPClient + orgID string + rateLimiter *rate.Limiter + atlasClient HTTPClient + targetCurrency string + currencyConverter currency.Converter } type HTTPClient interface { @@ -217,16 +237,42 @@ func filterLineItemsByWindow(win *opencost.Window, lineItems []atlasplugin.LineI func (a *AtlasCostSource) getAtlasCostsForWindow(win *opencost.Window, lineItems []atlasplugin.LineItem) *pb.CustomCostResponse { - //filter responses between the win start and win end dates - costsInWindow := filterLineItemsByWindow(win, lineItems) + respCurrency := "USD" + if a.currencyConverter != nil && a.targetCurrency != "USD" && a.targetCurrency != "" { + for _, cost := range costsInWindow { + if convertedBilled, err := a.currencyConverter.Convert(float64(cost.BilledCost), "USD", a.targetCurrency); err == nil { + cost.BilledCost = float32(convertedBilled) + } else { + log.Debugf("Failed to convert billed cost: %v", err) + } + + if convertedList, err := a.currencyConverter.Convert(float64(cost.ListCost), "USD", a.targetCurrency); err == nil { + cost.ListCost = float32(convertedList) + } else { + log.Debugf("Failed to convert list cost: %v", err) + } + + if convertedUnit, err := a.currencyConverter.Convert(float64(cost.ListUnitPrice), "USD", a.targetCurrency); err == nil { + cost.ListUnitPrice = float32(convertedUnit) + } else { + log.Debugf("Failed to convert unit price: %v", err) + } + } + respCurrency = a.targetCurrency + + if rate, err := a.currencyConverter.GetRate("USD", a.targetCurrency); err == nil { + log.Debugf("Using exchange rate USD to %s: %f", a.targetCurrency, rate) + } + } + resp := pb.CustomCostResponse{ Metadata: map[string]string{"api_client_version": "v1"}, CostSource: "data_storage", Domain: "mongodb-atlas", Version: "v1", - Currency: "USD", + Currency: respCurrency, Start: timestamppb.New(*win.Start()), End: timestamppb.New(*win.End()), Errors: []string{}, diff --git a/pkg/plugins/mongodb-atlas/cmd/main/main_currency_test.go b/pkg/plugins/mongodb-atlas/cmd/main/main_currency_test.go new file mode 100644 index 0000000..3baa87d --- /dev/null +++ b/pkg/plugins/mongodb-atlas/cmd/main/main_currency_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "testing" + "time" + + atlasplugin "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/plugin" + "github.com/opencost/opencost/core/pkg/opencost" + "github.com/stretchr/testify/assert" + "golang.org/x/time/rate" +) + +type MockConverter struct { + rate float64 +} + +func (m *MockConverter) Convert(amount float64, from, to string) (float64, error) { + if from == "USD" && to == "EUR" { + return amount * m.rate, nil + } + return amount, nil +} + +func (m *MockConverter) GetRate(from, to string) (float64, error) { + if from == "USD" && to == "EUR" { + return m.rate, nil + } + return 1.0, nil +} + +func (m *MockConverter) GetSupportedCurrencies() ([]string, error) { + return []string{"USD", "EUR", "GBP"}, nil +} + +func TestAtlasCostSource_CurrencyConversion(t *testing.T) { + mockConverter := &MockConverter{rate: 0.85} + + costSource := &AtlasCostSource{ + orgID: "test-org", + rateLimiter: rate.NewLimiter(1.0, 1), + targetCurrency: "EUR", + currencyConverter: mockConverter, + } + + start := time.Now().Add(-24 * time.Hour).UTC() + end := time.Now().UTC() + win := opencost.NewWindow(&start, &end) + + // Create test line items with USD prices + // Note: StartDate and EndDate need to be within the window to be included + itemStart := start.Add(1 * time.Hour) + itemEnd := end.Add(-1 * time.Hour) + lineItems := []atlasplugin.LineItem{ + { + ClusterName: "test-cluster", + GroupId: "test-group", + GroupName: "Test Group", + SKU: "TEST_SKU", + TotalPriceCents: 10000, // $100.00 + UnitPriceDollars: 1.0, + Quantity: 100, + Unit: "hours", + StartDate: itemStart.Format(time.RFC3339), + EndDate: itemEnd.Format(time.RFC3339), + }, + } + + resp := costSource.getAtlasCostsForWindow(&win, lineItems) + + assert.Equal(t, "EUR", resp.Currency) + assert.Len(t, resp.Costs, 1) + + cost := resp.Costs[0] + expectedBilledCost := float32(100.0 * 0.85) + expectedListCost := float32(100.0 * 0.85) + expectedUnitPrice := float32(1.0 * 0.85) + + assert.Equal(t, expectedBilledCost, cost.BilledCost) + assert.Equal(t, expectedListCost, cost.ListCost) + assert.Equal(t, expectedUnitPrice, cost.ListUnitPrice) +} + +func TestAtlasCostSource_NoConversion(t *testing.T) { + costSource := &AtlasCostSource{ + orgID: "test-org", + rateLimiter: rate.NewLimiter(1.0, 1), + targetCurrency: "USD", + } + + start := time.Now().Add(-24 * time.Hour).UTC() + end := time.Now().UTC() + win := opencost.NewWindow(&start, &end) + + itemStart := start.Add(1 * time.Hour) + itemEnd := end.Add(-1 * time.Hour) + lineItems := []atlasplugin.LineItem{ + { + ClusterName: "test-cluster", + GroupId: "test-group", + GroupName: "Test Group", + SKU: "TEST_SKU", + TotalPriceCents: 10000, // $100.00 + UnitPriceDollars: 1.0, + Quantity: 100, + Unit: "hours", + StartDate: itemStart.Format(time.RFC3339), + EndDate: itemEnd.Format(time.RFC3339), + }, + } + + resp := costSource.getAtlasCostsForWindow(&win, lineItems) + + assert.Equal(t, "USD", resp.Currency) + assert.Len(t, resp.Costs, 1) + + cost := resp.Costs[0] + assert.Equal(t, float32(100.0), cost.BilledCost) + assert.Equal(t, float32(100.0), cost.ListCost) + assert.Equal(t, float32(1.0), cost.ListUnitPrice) +} \ No newline at end of file diff --git a/pkg/plugins/mongodb-atlas/config/atlasconfig.go b/pkg/plugins/mongodb-atlas/config/atlasconfig.go index 98cad03..6a34e65 100644 --- a/pkg/plugins/mongodb-atlas/config/atlasconfig.go +++ b/pkg/plugins/mongodb-atlas/config/atlasconfig.go @@ -7,10 +7,12 @@ import ( ) type AtlasConfig struct { - PublicKey string `json:"atlas_public_key"` - PrivateKey string `json:"atlas_private_key"` - OrgID string `json:"atlas_org_id"` - LogLevel string `json:"atlas_plugin_log_level"` + PublicKey string `json:"atlas_public_key"` + PrivateKey string `json:"atlas_private_key"` + OrgID string `json:"atlas_org_id"` + LogLevel string `json:"atlas_plugin_log_level"` + TargetCurrency string `json:"target_currency"` + ExchangeAPIKey string `json:"exchange_api_key"` } func GetAtlasConfig(configFilePath string) (*AtlasConfig, error) { @@ -28,5 +30,9 @@ func GetAtlasConfig(configFilePath string) (*AtlasConfig, error) { result.LogLevel = "info" } + if result.TargetCurrency == "" { + result.TargetCurrency = "USD" + } + return &result, nil } diff --git a/pkg/plugins/mongodb-atlas/go.mod b/pkg/plugins/mongodb-atlas/go.mod index aed5aea..e871c72 100644 --- a/pkg/plugins/mongodb-atlas/go.mod +++ b/pkg/plugins/mongodb-atlas/go.mod @@ -4,10 +4,13 @@ go 1.22.5 replace github.com/opencost/opencost-plugins/common => ../../common +replace github.com/opencost/opencost-plugins/pkg/common/currency => ../../common/currency + require ( github.com/hashicorp/go-plugin v1.6.1 github.com/icholy/digest v0.1.23 github.com/opencost/opencost-plugins/common v0.0.0-00010101000000-000000000000 + github.com/opencost/opencost-plugins/pkg/common/currency v0.0.0-00010101000000-000000000000 github.com/opencost/opencost/core v0.0.0-20240829194822-b82370afd830 github.com/stretchr/testify v1.9.0 golang.org/x/time v0.6.0 From 37625c0fdf6e15af8ba86d6d25fd3ee4fb4acd5d Mon Sep 17 00:00:00 2001 From: Malthe Poulsen Date: Wed, 23 Jul 2025 16:33:38 +0200 Subject: [PATCH 2/6] don't initialize to default TTL value Signed-off-by: Malthe Poulsen --- pkg/plugins/mongodb-atlas/cmd/main/main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/plugins/mongodb-atlas/cmd/main/main.go b/pkg/plugins/mongodb-atlas/cmd/main/main.go index 3974b40..fe0eaf4 100644 --- a/pkg/plugins/mongodb-atlas/cmd/main/main.go +++ b/pkg/plugins/mongodb-atlas/cmd/main/main.go @@ -55,8 +55,7 @@ func main() { var currencyConverter currency.Converter if atlasConfig.ExchangeAPIKey != "" && atlasConfig.TargetCurrency != "USD" { converter, err := currency.NewConverter(currency.Config{ - APIKey: atlasConfig.ExchangeAPIKey, - CacheTTL: 24 * time.Hour, + APIKey: atlasConfig.ExchangeAPIKey, }) if err != nil { log.Warnf("Failed to initialize currency converter: %v. Will use USD.", err) From 04ac273ebbd3919137d353a3bfd10159a27b89c6 Mon Sep 17 00:00:00 2001 From: Malthe Poulsen Date: Wed, 23 Jul 2025 16:42:24 +0200 Subject: [PATCH 3/6] Update module references for `common/currency` package Signed-off-by: Malthe Poulsen --- pkg/plugins/mongodb-atlas/cmd/main/main.go | 2 +- pkg/plugins/mongodb-atlas/go.mod | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/plugins/mongodb-atlas/cmd/main/main.go b/pkg/plugins/mongodb-atlas/cmd/main/main.go index fe0eaf4..35f768c 100644 --- a/pkg/plugins/mongodb-atlas/cmd/main/main.go +++ b/pkg/plugins/mongodb-atlas/cmd/main/main.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/go-plugin" "github.com/icholy/digest" commonconfig "github.com/opencost/opencost-plugins/common/config" - "github.com/opencost/opencost-plugins/pkg/common/currency" + "github.com/opencost/opencost-plugins/common/currency" atlasconfig "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/config" atlasplugin "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/plugin" "github.com/opencost/opencost/core/pkg/log" diff --git a/pkg/plugins/mongodb-atlas/go.mod b/pkg/plugins/mongodb-atlas/go.mod index e871c72..4d2caee 100644 --- a/pkg/plugins/mongodb-atlas/go.mod +++ b/pkg/plugins/mongodb-atlas/go.mod @@ -4,13 +4,13 @@ go 1.22.5 replace github.com/opencost/opencost-plugins/common => ../../common -replace github.com/opencost/opencost-plugins/pkg/common/currency => ../../common/currency +replace github.com/opencost/opencost-plugins/common/currency => ../../common/currency require ( github.com/hashicorp/go-plugin v1.6.1 github.com/icholy/digest v0.1.23 github.com/opencost/opencost-plugins/common v0.0.0-00010101000000-000000000000 - github.com/opencost/opencost-plugins/pkg/common/currency v0.0.0-00010101000000-000000000000 + github.com/opencost/opencost-plugins/common/currency v0.0.0-00010101000000-000000000000 github.com/opencost/opencost/core v0.0.0-20240829194822-b82370afd830 github.com/stretchr/testify v1.9.0 golang.org/x/time v0.6.0 From e72f8078512841055d81e887c72f90e048af9b95 Mon Sep 17 00:00:00 2001 From: Malthe Poulsen Date: Wed, 23 Jul 2025 16:44:13 +0200 Subject: [PATCH 4/6] Refactor currency handling by introducing `defaultCurrency` constant and replacing hardcoded "USD" references. Signed-off-by: Malthe Poulsen --- pkg/plugins/mongodb-atlas/cmd/main/main.go | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pkg/plugins/mongodb-atlas/cmd/main/main.go b/pkg/plugins/mongodb-atlas/cmd/main/main.go index 35f768c..5761921 100644 --- a/pkg/plugins/mongodb-atlas/cmd/main/main.go +++ b/pkg/plugins/mongodb-atlas/cmd/main/main.go @@ -32,7 +32,10 @@ var handshakeConfig = plugin.HandshakeConfig{ MagicCookieValue: "mongodb-atlas", } -const costExplorerPendingInvoicesURL = "https://cloud.mongodb.com/api/atlas/v2/orgs/%s/invoices/pending" +const ( + costExplorerPendingInvoicesURL = "https://cloud.mongodb.com/api/atlas/v2/orgs/%s/invoices/pending" + defaultCurrency = "USD" +) func main() { log.Debug("Initializing Mongo plugin") @@ -53,12 +56,12 @@ func main() { rateLimiter := rate.NewLimiter(1.1, 2) var currencyConverter currency.Converter - if atlasConfig.ExchangeAPIKey != "" && atlasConfig.TargetCurrency != "USD" { + if atlasConfig.ExchangeAPIKey != "" && atlasConfig.TargetCurrency != defaultCurrency { converter, err := currency.NewConverter(currency.Config{ APIKey: atlasConfig.ExchangeAPIKey, }) if err != nil { - log.Warnf("Failed to initialize currency converter: %v. Will use USD.", err) + log.Warnf("Failed to initialize currency converter: %v. Will use %s.", err, defaultCurrency) } else { currencyConverter = converter log.Infof("Currency converter initialized for target currency: %s", atlasConfig.TargetCurrency) @@ -238,31 +241,29 @@ func (a *AtlasCostSource) getAtlasCostsForWindow(win *opencost.Window, lineItems costsInWindow := filterLineItemsByWindow(win, lineItems) - respCurrency := "USD" - if a.currencyConverter != nil && a.targetCurrency != "USD" && a.targetCurrency != "" { + if a.currencyConverter != nil && a.targetCurrency != defaultCurrency && a.targetCurrency != "" { for _, cost := range costsInWindow { - if convertedBilled, err := a.currencyConverter.Convert(float64(cost.BilledCost), "USD", a.targetCurrency); err == nil { + if convertedBilled, err := a.currencyConverter.Convert(float64(cost.BilledCost), defaultCurrency, a.targetCurrency); err == nil { cost.BilledCost = float32(convertedBilled) } else { log.Debugf("Failed to convert billed cost: %v", err) } - if convertedList, err := a.currencyConverter.Convert(float64(cost.ListCost), "USD", a.targetCurrency); err == nil { + if convertedList, err := a.currencyConverter.Convert(float64(cost.ListCost), defaultCurrency, a.targetCurrency); err == nil { cost.ListCost = float32(convertedList) } else { log.Debugf("Failed to convert list cost: %v", err) } - if convertedUnit, err := a.currencyConverter.Convert(float64(cost.ListUnitPrice), "USD", a.targetCurrency); err == nil { + if convertedUnit, err := a.currencyConverter.Convert(float64(cost.ListUnitPrice), defaultCurrency, a.targetCurrency); err == nil { cost.ListUnitPrice = float32(convertedUnit) } else { log.Debugf("Failed to convert unit price: %v", err) } } - respCurrency = a.targetCurrency - if rate, err := a.currencyConverter.GetRate("USD", a.targetCurrency); err == nil { - log.Debugf("Using exchange rate USD to %s: %f", a.targetCurrency, rate) + if exchangeRate, err := a.currencyConverter.GetRate(defaultCurrency, a.targetCurrency); err == nil { + log.Debugf("Using exchange rate %s to %s: %f", defaultCurrency, a.targetCurrency, exchangeRate) } } @@ -271,7 +272,7 @@ func (a *AtlasCostSource) getAtlasCostsForWindow(win *opencost.Window, lineItems CostSource: "data_storage", Domain: "mongodb-atlas", Version: "v1", - Currency: respCurrency, + Currency: a.targetCurrency, Start: timestamppb.New(*win.Start()), End: timestamppb.New(*win.End()), Errors: []string{}, From 0cf7f095bf39bdf1f2ca7ddebee767786e61518b Mon Sep 17 00:00:00 2001 From: Malthe Poulsen Date: Wed, 23 Jul 2025 17:24:30 +0200 Subject: [PATCH 5/6] Standardize naming conventions across currency package components. Use unexported types/fields and camelCase for uniformity. Update associated tests and methods accordingly. Signed-off-by: Malthe Poulsen --- pkg/common/currency/cache.go | 62 +++++------ pkg/common/currency/cache_test.go | 148 +++++++++++++------------- pkg/common/currency/client.go | 42 ++++---- pkg/common/currency/converter.go | 104 +++++++----------- pkg/common/currency/converter_test.go | 133 ++++++++--------------- pkg/common/currency/types.go | 83 +++++++-------- 6 files changed, 247 insertions(+), 325 deletions(-) diff --git a/pkg/common/currency/cache.go b/pkg/common/currency/cache.go index d2e4c93..ce868ef 100644 --- a/pkg/common/currency/cache.go +++ b/pkg/common/currency/cache.go @@ -5,95 +5,95 @@ import ( "time" ) -type MemoryCache struct { +type memoryCache struct { mu sync.RWMutex - data map[string]*CachedRates + data map[string]*cachedRates ttl time.Duration janitor *time.Ticker } -func NewMemoryCache(ttl time.Duration) *MemoryCache { +func newMemoryCache(ttl time.Duration) *memoryCache { if ttl == 0 { ttl = 24 * time.Hour } - - cache := &MemoryCache{ - data: make(map[string]*CachedRates), + + cache := &memoryCache{ + data: make(map[string]*cachedRates), ttl: ttl, janitor: time.NewTicker(ttl / 2), } - + go cache.cleanup() - + return cache } -func (c *MemoryCache) Get(baseCurrency string) (*CachedRates, bool) { +func (c *memoryCache) get(baseCurrency string) (*cachedRates, bool) { c.mu.RLock() defer c.mu.RUnlock() - + rates, exists := c.data[baseCurrency] if !exists { return nil, false } - - if time.Now().After(rates.ValidUntil) { + + if time.Now().After(rates.validUntil) { return nil, false } - + return rates, true } -func (c *MemoryCache) Set(baseCurrency string, rates *CachedRates) { +func (c *memoryCache) set(baseCurrency string, rates *cachedRates) { c.mu.Lock() defer c.mu.Unlock() - - rates.ValidUntil = rates.FetchedAt.Add(c.ttl) + + rates.validUntil = rates.fetchedAt.Add(c.ttl) c.data[baseCurrency] = rates } -func (c *MemoryCache) Clear() { +func (c *memoryCache) clear() { c.mu.Lock() defer c.mu.Unlock() - - c.data = make(map[string]*CachedRates) + + c.data = make(map[string]*cachedRates) } -func (c *MemoryCache) cleanup() { +func (c *memoryCache) cleanup() { for range c.janitor.C { c.removeExpired() } } -func (c *MemoryCache) removeExpired() { +func (c *memoryCache) removeExpired() { c.mu.Lock() defer c.mu.Unlock() - + now := time.Now() for key, rates := range c.data { - if now.After(rates.ValidUntil) { + if now.After(rates.validUntil) { delete(c.data, key) } } } -func (c *MemoryCache) Stop() { +func (c *memoryCache) stop() { if c.janitor != nil { c.janitor.Stop() } } -func (c *MemoryCache) Stats() (entries int, oldestEntry time.Time) { +func (c *memoryCache) stats() (entries int, oldestEntry time.Time) { c.mu.RLock() defer c.mu.RUnlock() - + entries = len(c.data) - + for _, rates := range c.data { - if oldestEntry.IsZero() || rates.FetchedAt.Before(oldestEntry) { - oldestEntry = rates.FetchedAt + if oldestEntry.IsZero() || rates.fetchedAt.Before(oldestEntry) { + oldestEntry = rates.fetchedAt } } - + return entries, oldestEntry -} \ No newline at end of file +} diff --git a/pkg/common/currency/cache_test.go b/pkg/common/currency/cache_test.go index 2815cf2..c05472f 100644 --- a/pkg/common/currency/cache_test.go +++ b/pkg/common/currency/cache_test.go @@ -6,37 +6,37 @@ import ( ) func TestMemoryCache_SetAndGet(t *testing.T) { - cache := NewMemoryCache(1 * time.Hour) - defer cache.Stop() - + cache := newMemoryCache(1 * time.Hour) + defer cache.stop() + // Test setting and getting rates - rates := &CachedRates{ - Rates: map[string]float64{ + rates := &cachedRates{ + rates: map[string]float64{ "EUR": 0.85, "GBP": 0.73, }, - BaseCode: "USD", - FetchedAt: time.Now(), + baseCode: "USD", + fetchedAt: time.Now(), } - - cache.Set("USD", rates) - + + cache.set("USD", rates) + // Test successful get - retrieved, found := cache.Get("USD") + retrieved, found := cache.get("USD") if !found { t.Error("expected to find cached rates") } - - if retrieved.BaseCode != "USD" { - t.Errorf("expected base code USD, got %s", retrieved.BaseCode) + + if retrieved.baseCode != "USD" { + t.Errorf("expected base code USD, got %s", retrieved.baseCode) } - - if len(retrieved.Rates) != 2 { - t.Errorf("expected 2 rates, got %d", len(retrieved.Rates)) + + if len(retrieved.rates) != 2 { + t.Errorf("expected 2 rates, got %d", len(retrieved.rates)) } - + // Test non-existent key - _, found = cache.Get("EUR") + _, found = cache.get("EUR") if found { t.Error("expected not to find rates for EUR") } @@ -44,63 +44,63 @@ func TestMemoryCache_SetAndGet(t *testing.T) { func TestMemoryCache_Expiration(t *testing.T) { // Use short TTL for testing - cache := NewMemoryCache(100 * time.Millisecond) - defer cache.Stop() - - rates := &CachedRates{ - Rates: map[string]float64{ + cache := newMemoryCache(100 * time.Millisecond) + defer cache.stop() + + rates := &cachedRates{ + rates: map[string]float64{ "EUR": 0.85, }, - BaseCode: "USD", - FetchedAt: time.Now(), + baseCode: "USD", + fetchedAt: time.Now(), } - - cache.Set("USD", rates) - + + cache.set("USD", rates) + // Should find it immediately - _, found := cache.Get("USD") + _, found := cache.get("USD") if !found { t.Error("expected to find cached rates immediately") } - + // Wait for expiration time.Sleep(150 * time.Millisecond) - + // Should not find it after expiration - _, found = cache.Get("USD") + _, found = cache.get("USD") if found { t.Error("expected rates to be expired") } } func TestMemoryCache_Clear(t *testing.T) { - cache := NewMemoryCache(1 * time.Hour) - defer cache.Stop() - + cache := newMemoryCache(1 * time.Hour) + defer cache.stop() + // Add multiple entries for _, base := range []string{"USD", "EUR", "GBP"} { - rates := &CachedRates{ - Rates: map[string]float64{"TEST": 1.0}, - BaseCode: base, - FetchedAt: time.Now(), + rates := &cachedRates{ + rates: map[string]float64{"TEST": 1.0}, + baseCode: base, + fetchedAt: time.Now(), } - cache.Set(base, rates) + cache.set(base, rates) } - + // Verify all entries exist for _, base := range []string{"USD", "EUR", "GBP"} { - _, found := cache.Get(base) + _, found := cache.get(base) if !found { t.Errorf("expected to find rates for %s", base) } } - + // Clear cache - cache.Clear() - + cache.clear() + // Verify all entries are gone for _, base := range []string{"USD", "EUR", "GBP"} { - _, found := cache.Get(base) + _, found := cache.get(base) if found { t.Errorf("expected not to find rates for %s after clear", base) } @@ -108,31 +108,31 @@ func TestMemoryCache_Clear(t *testing.T) { } func TestMemoryCache_Stats(t *testing.T) { - cache := NewMemoryCache(1 * time.Hour) - defer cache.Stop() - + cache := newMemoryCache(1 * time.Hour) + defer cache.stop() + // Initially empty - entries, _ := cache.Stats() + entries, _ := cache.stats() if entries != 0 { t.Errorf("expected 0 entries, got %d", entries) } - + // Add entries now := time.Now() for i, base := range []string{"USD", "EUR", "GBP"} { - rates := &CachedRates{ - Rates: map[string]float64{"TEST": 1.0}, - BaseCode: base, - FetchedAt: now.Add(time.Duration(i) * time.Minute), + rates := &cachedRates{ + rates: map[string]float64{"TEST": 1.0}, + baseCode: base, + fetchedAt: now.Add(time.Duration(i) * time.Minute), } - cache.Set(base, rates) + cache.set(base, rates) } - - entries, oldest := cache.Stats() + + entries, oldest := cache.stats() if entries != 3 { t.Errorf("expected 3 entries, got %d", entries) } - + // The oldest should be the first one we added (USD) if !oldest.Equal(now) { t.Errorf("expected oldest entry to be %v, got %v", now, oldest) @@ -141,30 +141,30 @@ func TestMemoryCache_Stats(t *testing.T) { func TestMemoryCache_Cleanup(t *testing.T) { // Use very short TTL for testing - cache := NewMemoryCache(50 * time.Millisecond) - defer cache.Stop() - + cache := newMemoryCache(50 * time.Millisecond) + defer cache.stop() + // Add entry - rates := &CachedRates{ - Rates: map[string]float64{"EUR": 0.85}, - BaseCode: "USD", - FetchedAt: time.Now(), + rates := &cachedRates{ + rates: map[string]float64{"EUR": 0.85}, + baseCode: "USD", + fetchedAt: time.Now(), } - cache.Set("USD", rates) - + cache.set("USD", rates) + // Verify it exists - entries, _ := cache.Stats() + entries, _ := cache.stats() if entries != 1 { t.Errorf("expected 1 entry, got %d", entries) } - + // Wait for cleanup cycle (janitor runs every TTL/2 = 25ms) // Wait a bit longer to ensure cleanup has run time.Sleep(100 * time.Millisecond) - + // Verify it's been cleaned up - entries, _ = cache.Stats() + entries, _ = cache.stats() if entries != 0 { t.Errorf("expected 0 entries after cleanup, got %d", entries) } -} \ No newline at end of file +} diff --git a/pkg/common/currency/client.go b/pkg/common/currency/client.go index e76eae0..06c7557 100644 --- a/pkg/common/currency/client.go +++ b/pkg/common/currency/client.go @@ -13,22 +13,22 @@ const ( userAgent = "opencost-plugins/1.0" ) -type HTTPClient interface { +type httpClient interface { Do(req *http.Request) (*http.Response, error) } -type ExchangeRateClient struct { +type exchangeRateClient struct { apiKey string - httpClient HTTPClient + httpClient httpClient timeout time.Duration } -func NewExchangeRateClient(apiKey string, timeout time.Duration) *ExchangeRateClient { +func newExchangeRateClient(apiKey string, timeout time.Duration) *exchangeRateClient { if timeout == 0 { timeout = 10 * time.Second } - - return &ExchangeRateClient{ + + return &exchangeRateClient{ apiKey: apiKey, httpClient: &http.Client{ Timeout: timeout, @@ -37,57 +37,53 @@ func NewExchangeRateClient(apiKey string, timeout time.Duration) *ExchangeRateCl } } -func (c *ExchangeRateClient) FetchRates(baseCurrency string) (*ExchangeRateResponse, error) { +func (c *exchangeRateClient) fetchRates(baseCurrency string) (*exchangeRateResponse, error) { if c.apiKey == "" { return nil, fmt.Errorf("API key is required") } - + if baseCurrency == "" { baseCurrency = "USD" } - + url := fmt.Sprintf("%s/%s/latest/%s", apiBaseURL, c.apiKey, baseCurrency) - + req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } - + req.Header.Set("User-Agent", userAgent) req.Header.Set("Accept", "application/json") - + resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch exchange rates: %w", err) } defer resp.Body.Close() - + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) } - + body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } - - var response ExchangeRateResponse + + var response exchangeRateResponse if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } - + if response.Result != "success" { return nil, fmt.Errorf("API returned error result: %s", response.Result) } - + if len(response.ConversionRates) == 0 { return nil, fmt.Errorf("no conversion rates returned") } - + return &response, nil } - -func (c *ExchangeRateClient) SetHTTPClient(client HTTPClient) { - c.httpClient = client -} \ No newline at end of file diff --git a/pkg/common/currency/converter.go b/pkg/common/currency/converter.go index 50ccb24..c9acf8e 100644 --- a/pkg/common/currency/converter.go +++ b/pkg/common/currency/converter.go @@ -7,129 +7,99 @@ import ( "time" ) -type CurrencyConverter struct { - client Client - cache Cache +type currencyConverter struct { + client client + cache cache config Config mu sync.RWMutex } -func NewConverter(config Config) (*CurrencyConverter, error) { +func NewConverter(config Config) (Converter, error) { if config.APIKey == "" { return nil, fmt.Errorf("API key is required") } - + if config.CacheTTL == 0 { config.CacheTTL = 24 * time.Hour } - + if config.APITimeout == 0 { config.APITimeout = 10 * time.Second } - - client := NewExchangeRateClient(config.APIKey, config.APITimeout) - cache := NewMemoryCache(config.CacheTTL) - - return &CurrencyConverter{ + + client := newExchangeRateClient(config.APIKey, config.APITimeout) + cache := newMemoryCache(config.CacheTTL) + + return ¤cyConverter{ client: client, cache: cache, config: config, }, nil } -func (c *CurrencyConverter) Convert(amount float64, from, to string) (float64, error) { +func (c *currencyConverter) Convert(amount float64, from, to string) (float64, error) { from = strings.ToUpper(strings.TrimSpace(from)) to = strings.ToUpper(strings.TrimSpace(to)) - + if from == to { return amount, nil } - + rate, err := c.GetRate(from, to) if err != nil { return 0, fmt.Errorf("failed to get exchange rate from %s to %s: %w", from, to, err) } - + return amount * rate, nil } -func (c *CurrencyConverter) GetRate(from, to string) (float64, error) { +func (c *currencyConverter) GetRate(from, to string) (float64, error) { from = strings.ToUpper(strings.TrimSpace(from)) to = strings.ToUpper(strings.TrimSpace(to)) - + if from == to { return 1.0, nil } - - cachedRates, found := c.cache.Get(from) - if found && cachedRates.Rates != nil { - if rate, exists := cachedRates.Rates[to]; exists { + + cachedRates, found := c.cache.get(from) + if found && cachedRates.rates != nil { + if rate, exists := cachedRates.rates[to]; exists { return rate, nil } } - + rates, err := c.fetchAndCacheRates(from) if err != nil { return 0, err } - + rate, exists := rates[to] if !exists { return 0, fmt.Errorf("currency %s not supported or not found in exchange rates", to) } - - return rate, nil -} -func (c *CurrencyConverter) GetSupportedCurrencies() ([]string, error) { - rates, err := c.fetchAndCacheRates("USD") - if err != nil { - return nil, fmt.Errorf("failed to fetch supported currencies: %w", err) - } - - currencies := make([]string, 0, len(rates)) - for currency := range rates { - currencies = append(currencies, currency) - } - - return currencies, nil + return rate, nil } -func (c *CurrencyConverter) fetchAndCacheRates(baseCurrency string) (map[string]float64, error) { +func (c *currencyConverter) fetchAndCacheRates(baseCurrency string) (map[string]float64, error) { c.mu.Lock() defer c.mu.Unlock() - - if cachedRates, found := c.cache.Get(baseCurrency); found { - return cachedRates.Rates, nil + + if cachedRates, found := c.cache.get(baseCurrency); found { + return cachedRates.rates, nil } - - response, err := c.client.FetchRates(baseCurrency) + + response, err := c.client.fetchRates(baseCurrency) if err != nil { return nil, fmt.Errorf("failed to fetch rates from API: %w", err) } - - cachedRates := &CachedRates{ - Rates: response.ConversionRates, - BaseCode: response.BaseCode, - FetchedAt: time.Now(), - } - c.cache.Set(baseCurrency, cachedRates) - - return response.ConversionRates, nil -} -func (c *CurrencyConverter) SetClient(client Client) { - c.mu.Lock() - defer c.mu.Unlock() - c.client = client -} + cachedRates := &cachedRates{ + rates: response.ConversionRates, + baseCode: response.BaseCode, + fetchedAt: time.Now(), + } + c.cache.set(baseCurrency, cachedRates) -func (c *CurrencyConverter) SetCache(cache Cache) { - c.mu.Lock() - defer c.mu.Unlock() - c.cache = cache + return response.ConversionRates, nil } - -func (c *CurrencyConverter) ClearCache() { - c.cache.Clear() -} \ No newline at end of file diff --git a/pkg/common/currency/converter_test.go b/pkg/common/currency/converter_test.go index 0afc996..095c044 100644 --- a/pkg/common/currency/converter_test.go +++ b/pkg/common/currency/converter_test.go @@ -6,56 +6,56 @@ import ( "time" ) -type MockClient struct { +type mockClient struct { rates map[string]map[string]float64 err error } -func (m *MockClient) FetchRates(baseCurrency string) (*ExchangeRateResponse, error) { +func (m *mockClient) fetchRates(baseCurrency string) (*exchangeRateResponse, error) { if m.err != nil { return nil, m.err } - + rates, exists := m.rates[baseCurrency] if !exists { return nil, fmt.Errorf("no rates for base currency %s", baseCurrency) } - - return &ExchangeRateResponse{ + + return &exchangeRateResponse{ Result: "success", BaseCode: baseCurrency, ConversionRates: rates, }, nil } -type MockCache struct { - data map[string]*CachedRates +type mockCache struct { + data map[string]*cachedRates } -func NewMockCache() *MockCache { - return &MockCache{ - data: make(map[string]*CachedRates), +func newMockCache() *mockCache { + return &mockCache{ + data: make(map[string]*cachedRates), } } -func (m *MockCache) Get(baseCurrency string) (*CachedRates, bool) { +func (m *mockCache) get(baseCurrency string) (*cachedRates, bool) { rates, exists := m.data[baseCurrency] - if !exists || time.Now().After(rates.ValidUntil) { + if !exists || time.Now().After(rates.validUntil) { return nil, false } return rates, true } -func (m *MockCache) Set(baseCurrency string, rates *CachedRates) { +func (m *mockCache) set(baseCurrency string, rates *cachedRates) { m.data[baseCurrency] = rates } -func (m *MockCache) Clear() { - m.data = make(map[string]*CachedRates) +func (m *mockCache) clear() { + m.data = make(map[string]*cachedRates) } func TestCurrencyConverter_Convert(t *testing.T) { - mockClient := &MockClient{ + mockClient := &mockClient{ rates: map[string]map[string]float64{ "USD": { "USD": 1.0, @@ -71,13 +71,13 @@ func TestCurrencyConverter_Convert(t *testing.T) { }, }, } - - converter := &CurrencyConverter{ + + converter := ¤cyConverter{ client: mockClient, - cache: NewMockCache(), + cache: newMockCache(), config: Config{APIKey: "test"}, } - + tests := []struct { name string amount float64 @@ -129,23 +129,23 @@ func TestCurrencyConverter_Convert(t *testing.T) { expectError: true, }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := converter.Convert(tt.amount, tt.from, tt.to) - + if tt.expectError { if err == nil { t.Errorf("expected error but got none") } return } - + if err != nil { t.Errorf("unexpected error: %v", err) return } - + if result != tt.expected { t.Errorf("expected %f, got %f", tt.expected, result) } @@ -154,7 +154,7 @@ func TestCurrencyConverter_Convert(t *testing.T) { } func TestCurrencyConverter_GetRate(t *testing.T) { - mockClient := &MockClient{ + mockClient := &mockClient{ rates: map[string]map[string]float64{ "USD": { "USD": 1.0, @@ -163,13 +163,13 @@ func TestCurrencyConverter_GetRate(t *testing.T) { }, }, } - - converter := &CurrencyConverter{ + + converter := ¤cyConverter{ client: mockClient, - cache: NewMockCache(), + cache: newMockCache(), config: Config{APIKey: "test"}, } - + // Test getting rate rate, err := converter.GetRate("USD", "EUR") if err != nil { @@ -178,7 +178,7 @@ func TestCurrencyConverter_GetRate(t *testing.T) { if rate != 0.85 { t.Errorf("expected rate 0.85, got %f", rate) } - + // Test same currency rate, err = converter.GetRate("USD", "USD") if err != nil { @@ -187,7 +187,7 @@ func TestCurrencyConverter_GetRate(t *testing.T) { if rate != 1.0 { t.Errorf("expected rate 1.0, got %f", rate) } - + // Test cache hit rate, err = converter.GetRate("USD", "EUR") if err != nil { @@ -198,72 +198,31 @@ func TestCurrencyConverter_GetRate(t *testing.T) { } } -func TestCurrencyConverter_GetSupportedCurrencies(t *testing.T) { - mockClient := &MockClient{ - rates: map[string]map[string]float64{ - "USD": { - "USD": 1.0, - "EUR": 0.85, - "GBP": 0.73, - "JPY": 110.0, - }, - }, - } - - converter := &CurrencyConverter{ - client: mockClient, - cache: NewMockCache(), - config: Config{APIKey: "test"}, - } - - currencies, err := converter.GetSupportedCurrencies() - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - if len(currencies) != 4 { - t.Errorf("expected 4 currencies, got %d", len(currencies)) - } - - // Check that all expected currencies are present - expected := map[string]bool{ - "USD": false, - "EUR": false, - "GBP": false, - "JPY": false, - } - - for _, curr := range currencies { - if _, exists := expected[curr]; exists { - expected[curr] = true - } - } - - for curr, found := range expected { - if !found { - t.Errorf("expected currency %s not found", curr) - } - } -} - func TestNewConverter(t *testing.T) { // Test with empty API key _, err := NewConverter(Config{}) if err == nil { t.Error("expected error for empty API key") } - + // Test with valid config converter, err := NewConverter(Config{APIKey: "test-key"}) if err != nil { t.Errorf("unexpected error: %v", err) } - - if converter.config.CacheTTL != 24*time.Hour { - t.Errorf("expected default cache TTL of 24h, got %v", converter.config.CacheTTL) + + // Convert to concrete type to access internal fields + cc, ok := converter.(*currencyConverter) + if !ok { + t.Error("expected converter to be of type *currencyConverter") + return } - - if converter.config.APITimeout != 10*time.Second { - t.Errorf("expected default API timeout of 10s, got %v", converter.config.APITimeout) + + if cc.config.CacheTTL != 24*time.Hour { + t.Errorf("expected default cache TTL of 24h, got %v", cc.config.CacheTTL) + } + + if cc.config.APITimeout != 10*time.Second { + t.Errorf("expected default API timeout of 10s, got %v", cc.config.APITimeout) } -} \ No newline at end of file +} diff --git a/pkg/common/currency/types.go b/pkg/common/currency/types.go index a9de39d..c79965b 100644 --- a/pkg/common/currency/types.go +++ b/pkg/common/currency/types.go @@ -4,60 +4,57 @@ import ( "time" ) -// ExchangeRateResponse represents the API response from exchangerate-api.com -type ExchangeRateResponse struct { - Result string `json:"result"` - Documentation string `json:"documentation"` - TermsOfUse string `json:"terms_of_use"` - TimeLastUpdateUnix int64 `json:"time_last_update_unix"` - TimeLastUpdateUTC string `json:"time_last_update_utc"` - TimeNextUpdateUnix int64 `json:"time_next_update_unix"` - TimeNextUpdateUTC string `json:"time_next_update_utc"` - BaseCode string `json:"base_code"` - ConversionRates map[string]float64 `json:"conversion_rates"` -} - -// CachedRates stores exchange rates with metadata -type CachedRates struct { - Rates map[string]float64 - BaseCode string - FetchedAt time.Time - ValidUntil time.Time +// Config holds configuration for the currency converter +type Config struct { + APIKey string + CacheTTL time.Duration + APITimeout time.Duration } // Converter interface defines currency conversion operations type Converter interface { // Convert converts an amount from one currency to another Convert(amount float64, from, to string) (float64, error) - + // GetRate returns the exchange rate between two currencies GetRate(from, to string) (float64, error) - - // GetSupportedCurrencies returns a list of supported currency codes - GetSupportedCurrencies() ([]string, error) } -// Client interface for fetching exchange rates -type Client interface { - // FetchRates fetches current exchange rates for a base currency - FetchRates(baseCurrency string) (*ExchangeRateResponse, error) +// exchangeRateResponse represents the API response from exchangerate-api.com +type exchangeRateResponse struct { + Result string `json:"result"` + Documentation string `json:"documentation"` + TermsOfUse string `json:"terms_of_use"` + TimeLastUpdateUnix int64 `json:"time_last_update_unix"` + TimeLastUpdateUTC string `json:"time_last_update_utc"` + TimeNextUpdateUnix int64 `json:"time_next_update_unix"` + TimeNextUpdateUTC string `json:"time_next_update_utc"` + BaseCode string `json:"base_code"` + ConversionRates map[string]float64 `json:"conversion_rates"` } -// Cache interface for storing exchange rates -type Cache interface { - // Get retrieves cached rates for a base currency - Get(baseCurrency string) (*CachedRates, bool) - - // Set stores rates for a base currency with TTL - Set(baseCurrency string, rates *CachedRates) - - // Clear removes all cached rates - Clear() +// cachedRates stores exchange rates with metadata +type cachedRates struct { + rates map[string]float64 + baseCode string + fetchedAt time.Time + validUntil time.Time } -// Config holds configuration for the currency converter -type Config struct { - APIKey string - CacheTTL time.Duration - APITimeout time.Duration -} \ No newline at end of file +// client interface for fetching exchange rates +type client interface { + // fetchRates fetches current exchange rates for a base currency + fetchRates(baseCurrency string) (*exchangeRateResponse, error) +} + +// cache interface for storing exchange rates +type cache interface { + // get retrieves cached rates for a base currency + get(baseCurrency string) (*cachedRates, bool) + + // set stores rates for a base currency with TTL + set(baseCurrency string, rates *cachedRates) + + // clear removes all cached rates + clear() +} From 556dc03fe340d4b68b1bb6c5989644eabf09de89 Mon Sep 17 00:00:00 2001 From: malpou Date: Sat, 16 Aug 2025 09:54:25 +0200 Subject: [PATCH 6/6] refactor: Update MongoDB Atlas plugin to use currency package from opencost - Remove local currency package from opencost-plugins/common/currency - Update imports to use github.com/opencost/opencost/pkg/currency - Fix duplicate imports and incorrect replace directives in go.mod - Remove unused standard library plugin import Signed-off-by: malpou --- pkg/common/currency/README.md | 64 ------ pkg/common/currency/cache.go | 99 --------- pkg/common/currency/cache_test.go | 170 --------------- pkg/common/currency/client.go | 89 -------- pkg/common/currency/converter.go | 105 ---------- pkg/common/currency/converter_test.go | 228 --------------------- pkg/common/currency/go.mod | 3 - pkg/common/currency/types.go | 60 ------ pkg/plugins/mongodb-atlas/cmd/main/main.go | 16 +- pkg/plugins/mongodb-atlas/go.mod | 62 +++--- pkg/plugins/mongodb-atlas/go.sum | 163 +++++++++------ 11 files changed, 139 insertions(+), 920 deletions(-) delete mode 100644 pkg/common/currency/README.md delete mode 100644 pkg/common/currency/cache.go delete mode 100644 pkg/common/currency/cache_test.go delete mode 100644 pkg/common/currency/client.go delete mode 100644 pkg/common/currency/converter.go delete mode 100644 pkg/common/currency/converter_test.go delete mode 100644 pkg/common/currency/go.mod delete mode 100644 pkg/common/currency/types.go diff --git a/pkg/common/currency/README.md b/pkg/common/currency/README.md deleted file mode 100644 index 9e9dae8..0000000 --- a/pkg/common/currency/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Currency Package - -Convert costs between currencies in OpenCost plugins using live exchange rates. - -## Quick Start - -```go -import "github.com/opencost/opencost-plugins/pkg/common/currency" - -config := currency.Config{ - APIKey: "your-api-key", - CacheTTL: 24 * time.Hour, -} - -converter, err := currency.NewConverter(config) -if err != nil { - log.Fatal(err) -} - -// Convert 100 USD to EUR -amount, err := converter.Convert(100.0, "USD", "EUR") -``` - -## Setup - -Get a free API key from [exchangerate-api.com](https://www.exchangerate-api.com/) (1,500 requests/month). - -## How it Works - -The package fetches exchange rates and caches them for 24 hours. This keeps API usage low - most plugins use under 50 requests per month. - -Supports all ISO 4217 currencies (161 total). Thread-safe with automatic cache cleanup. - -## MongoDB Atlas Example - -```go -// Config -type AtlasConfig struct { - TargetCurrency string `json:"target_currency"` - ExchangeAPIKey string `json:"exchange_api_key"` -} - -// Usage -if atlasConfig.ExchangeAPIKey != "" { - converter, _ := currency.NewConverter(currency.Config{ - APIKey: atlasConfig.ExchangeAPIKey, - CacheTTL: 24 * time.Hour, - }) -} - -// Convert costs -if converter != nil { - cost, _ = converter.Convert(cost, "USD", targetCurrency) -} -``` - -## Testing - -```bash -cd pkg/common/currency -go test -v -``` - -Tests use mocks - no API calls needed. \ No newline at end of file diff --git a/pkg/common/currency/cache.go b/pkg/common/currency/cache.go deleted file mode 100644 index ce868ef..0000000 --- a/pkg/common/currency/cache.go +++ /dev/null @@ -1,99 +0,0 @@ -package currency - -import ( - "sync" - "time" -) - -type memoryCache struct { - mu sync.RWMutex - data map[string]*cachedRates - ttl time.Duration - janitor *time.Ticker -} - -func newMemoryCache(ttl time.Duration) *memoryCache { - if ttl == 0 { - ttl = 24 * time.Hour - } - - cache := &memoryCache{ - data: make(map[string]*cachedRates), - ttl: ttl, - janitor: time.NewTicker(ttl / 2), - } - - go cache.cleanup() - - return cache -} - -func (c *memoryCache) get(baseCurrency string) (*cachedRates, bool) { - c.mu.RLock() - defer c.mu.RUnlock() - - rates, exists := c.data[baseCurrency] - if !exists { - return nil, false - } - - if time.Now().After(rates.validUntil) { - return nil, false - } - - return rates, true -} - -func (c *memoryCache) set(baseCurrency string, rates *cachedRates) { - c.mu.Lock() - defer c.mu.Unlock() - - rates.validUntil = rates.fetchedAt.Add(c.ttl) - c.data[baseCurrency] = rates -} - -func (c *memoryCache) clear() { - c.mu.Lock() - defer c.mu.Unlock() - - c.data = make(map[string]*cachedRates) -} - -func (c *memoryCache) cleanup() { - for range c.janitor.C { - c.removeExpired() - } -} - -func (c *memoryCache) removeExpired() { - c.mu.Lock() - defer c.mu.Unlock() - - now := time.Now() - for key, rates := range c.data { - if now.After(rates.validUntil) { - delete(c.data, key) - } - } -} - -func (c *memoryCache) stop() { - if c.janitor != nil { - c.janitor.Stop() - } -} - -func (c *memoryCache) stats() (entries int, oldestEntry time.Time) { - c.mu.RLock() - defer c.mu.RUnlock() - - entries = len(c.data) - - for _, rates := range c.data { - if oldestEntry.IsZero() || rates.fetchedAt.Before(oldestEntry) { - oldestEntry = rates.fetchedAt - } - } - - return entries, oldestEntry -} diff --git a/pkg/common/currency/cache_test.go b/pkg/common/currency/cache_test.go deleted file mode 100644 index c05472f..0000000 --- a/pkg/common/currency/cache_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package currency - -import ( - "testing" - "time" -) - -func TestMemoryCache_SetAndGet(t *testing.T) { - cache := newMemoryCache(1 * time.Hour) - defer cache.stop() - - // Test setting and getting rates - rates := &cachedRates{ - rates: map[string]float64{ - "EUR": 0.85, - "GBP": 0.73, - }, - baseCode: "USD", - fetchedAt: time.Now(), - } - - cache.set("USD", rates) - - // Test successful get - retrieved, found := cache.get("USD") - if !found { - t.Error("expected to find cached rates") - } - - if retrieved.baseCode != "USD" { - t.Errorf("expected base code USD, got %s", retrieved.baseCode) - } - - if len(retrieved.rates) != 2 { - t.Errorf("expected 2 rates, got %d", len(retrieved.rates)) - } - - // Test non-existent key - _, found = cache.get("EUR") - if found { - t.Error("expected not to find rates for EUR") - } -} - -func TestMemoryCache_Expiration(t *testing.T) { - // Use short TTL for testing - cache := newMemoryCache(100 * time.Millisecond) - defer cache.stop() - - rates := &cachedRates{ - rates: map[string]float64{ - "EUR": 0.85, - }, - baseCode: "USD", - fetchedAt: time.Now(), - } - - cache.set("USD", rates) - - // Should find it immediately - _, found := cache.get("USD") - if !found { - t.Error("expected to find cached rates immediately") - } - - // Wait for expiration - time.Sleep(150 * time.Millisecond) - - // Should not find it after expiration - _, found = cache.get("USD") - if found { - t.Error("expected rates to be expired") - } -} - -func TestMemoryCache_Clear(t *testing.T) { - cache := newMemoryCache(1 * time.Hour) - defer cache.stop() - - // Add multiple entries - for _, base := range []string{"USD", "EUR", "GBP"} { - rates := &cachedRates{ - rates: map[string]float64{"TEST": 1.0}, - baseCode: base, - fetchedAt: time.Now(), - } - cache.set(base, rates) - } - - // Verify all entries exist - for _, base := range []string{"USD", "EUR", "GBP"} { - _, found := cache.get(base) - if !found { - t.Errorf("expected to find rates for %s", base) - } - } - - // Clear cache - cache.clear() - - // Verify all entries are gone - for _, base := range []string{"USD", "EUR", "GBP"} { - _, found := cache.get(base) - if found { - t.Errorf("expected not to find rates for %s after clear", base) - } - } -} - -func TestMemoryCache_Stats(t *testing.T) { - cache := newMemoryCache(1 * time.Hour) - defer cache.stop() - - // Initially empty - entries, _ := cache.stats() - if entries != 0 { - t.Errorf("expected 0 entries, got %d", entries) - } - - // Add entries - now := time.Now() - for i, base := range []string{"USD", "EUR", "GBP"} { - rates := &cachedRates{ - rates: map[string]float64{"TEST": 1.0}, - baseCode: base, - fetchedAt: now.Add(time.Duration(i) * time.Minute), - } - cache.set(base, rates) - } - - entries, oldest := cache.stats() - if entries != 3 { - t.Errorf("expected 3 entries, got %d", entries) - } - - // The oldest should be the first one we added (USD) - if !oldest.Equal(now) { - t.Errorf("expected oldest entry to be %v, got %v", now, oldest) - } -} - -func TestMemoryCache_Cleanup(t *testing.T) { - // Use very short TTL for testing - cache := newMemoryCache(50 * time.Millisecond) - defer cache.stop() - - // Add entry - rates := &cachedRates{ - rates: map[string]float64{"EUR": 0.85}, - baseCode: "USD", - fetchedAt: time.Now(), - } - cache.set("USD", rates) - - // Verify it exists - entries, _ := cache.stats() - if entries != 1 { - t.Errorf("expected 1 entry, got %d", entries) - } - - // Wait for cleanup cycle (janitor runs every TTL/2 = 25ms) - // Wait a bit longer to ensure cleanup has run - time.Sleep(100 * time.Millisecond) - - // Verify it's been cleaned up - entries, _ = cache.stats() - if entries != 0 { - t.Errorf("expected 0 entries after cleanup, got %d", entries) - } -} diff --git a/pkg/common/currency/client.go b/pkg/common/currency/client.go deleted file mode 100644 index 06c7557..0000000 --- a/pkg/common/currency/client.go +++ /dev/null @@ -1,89 +0,0 @@ -package currency - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -const ( - apiBaseURL = "https://v6.exchangerate-api.com/v6" - userAgent = "opencost-plugins/1.0" -) - -type httpClient interface { - Do(req *http.Request) (*http.Response, error) -} - -type exchangeRateClient struct { - apiKey string - httpClient httpClient - timeout time.Duration -} - -func newExchangeRateClient(apiKey string, timeout time.Duration) *exchangeRateClient { - if timeout == 0 { - timeout = 10 * time.Second - } - - return &exchangeRateClient{ - apiKey: apiKey, - httpClient: &http.Client{ - Timeout: timeout, - }, - timeout: timeout, - } -} - -func (c *exchangeRateClient) fetchRates(baseCurrency string) (*exchangeRateResponse, error) { - if c.apiKey == "" { - return nil, fmt.Errorf("API key is required") - } - - if baseCurrency == "" { - baseCurrency = "USD" - } - - url := fmt.Sprintf("%s/%s/latest/%s", apiBaseURL, c.apiKey, baseCurrency) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("User-Agent", userAgent) - req.Header.Set("Accept", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to fetch exchange rates: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - var response exchangeRateResponse - if err := json.Unmarshal(body, &response); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) - } - - if response.Result != "success" { - return nil, fmt.Errorf("API returned error result: %s", response.Result) - } - - if len(response.ConversionRates) == 0 { - return nil, fmt.Errorf("no conversion rates returned") - } - - return &response, nil -} diff --git a/pkg/common/currency/converter.go b/pkg/common/currency/converter.go deleted file mode 100644 index c9acf8e..0000000 --- a/pkg/common/currency/converter.go +++ /dev/null @@ -1,105 +0,0 @@ -package currency - -import ( - "fmt" - "strings" - "sync" - "time" -) - -type currencyConverter struct { - client client - cache cache - config Config - mu sync.RWMutex -} - -func NewConverter(config Config) (Converter, error) { - if config.APIKey == "" { - return nil, fmt.Errorf("API key is required") - } - - if config.CacheTTL == 0 { - config.CacheTTL = 24 * time.Hour - } - - if config.APITimeout == 0 { - config.APITimeout = 10 * time.Second - } - - client := newExchangeRateClient(config.APIKey, config.APITimeout) - cache := newMemoryCache(config.CacheTTL) - - return ¤cyConverter{ - client: client, - cache: cache, - config: config, - }, nil -} - -func (c *currencyConverter) Convert(amount float64, from, to string) (float64, error) { - from = strings.ToUpper(strings.TrimSpace(from)) - to = strings.ToUpper(strings.TrimSpace(to)) - - if from == to { - return amount, nil - } - - rate, err := c.GetRate(from, to) - if err != nil { - return 0, fmt.Errorf("failed to get exchange rate from %s to %s: %w", from, to, err) - } - - return amount * rate, nil -} - -func (c *currencyConverter) GetRate(from, to string) (float64, error) { - from = strings.ToUpper(strings.TrimSpace(from)) - to = strings.ToUpper(strings.TrimSpace(to)) - - if from == to { - return 1.0, nil - } - - cachedRates, found := c.cache.get(from) - if found && cachedRates.rates != nil { - if rate, exists := cachedRates.rates[to]; exists { - return rate, nil - } - } - - rates, err := c.fetchAndCacheRates(from) - if err != nil { - return 0, err - } - - rate, exists := rates[to] - if !exists { - return 0, fmt.Errorf("currency %s not supported or not found in exchange rates", to) - } - - return rate, nil -} - -func (c *currencyConverter) fetchAndCacheRates(baseCurrency string) (map[string]float64, error) { - c.mu.Lock() - defer c.mu.Unlock() - - if cachedRates, found := c.cache.get(baseCurrency); found { - return cachedRates.rates, nil - } - - response, err := c.client.fetchRates(baseCurrency) - if err != nil { - return nil, fmt.Errorf("failed to fetch rates from API: %w", err) - } - - cachedRates := &cachedRates{ - rates: response.ConversionRates, - baseCode: response.BaseCode, - fetchedAt: time.Now(), - } - c.cache.set(baseCurrency, cachedRates) - - return response.ConversionRates, nil -} diff --git a/pkg/common/currency/converter_test.go b/pkg/common/currency/converter_test.go deleted file mode 100644 index 095c044..0000000 --- a/pkg/common/currency/converter_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package currency - -import ( - "fmt" - "testing" - "time" -) - -type mockClient struct { - rates map[string]map[string]float64 - err error -} - -func (m *mockClient) fetchRates(baseCurrency string) (*exchangeRateResponse, error) { - if m.err != nil { - return nil, m.err - } - - rates, exists := m.rates[baseCurrency] - if !exists { - return nil, fmt.Errorf("no rates for base currency %s", baseCurrency) - } - - return &exchangeRateResponse{ - Result: "success", - BaseCode: baseCurrency, - ConversionRates: rates, - }, nil -} - -type mockCache struct { - data map[string]*cachedRates -} - -func newMockCache() *mockCache { - return &mockCache{ - data: make(map[string]*cachedRates), - } -} - -func (m *mockCache) get(baseCurrency string) (*cachedRates, bool) { - rates, exists := m.data[baseCurrency] - if !exists || time.Now().After(rates.validUntil) { - return nil, false - } - return rates, true -} - -func (m *mockCache) set(baseCurrency string, rates *cachedRates) { - m.data[baseCurrency] = rates -} - -func (m *mockCache) clear() { - m.data = make(map[string]*cachedRates) -} - -func TestCurrencyConverter_Convert(t *testing.T) { - mockClient := &mockClient{ - rates: map[string]map[string]float64{ - "USD": { - "USD": 1.0, - "EUR": 0.85, - "GBP": 0.73, - "JPY": 110.0, - }, - "EUR": { - "EUR": 1.0, - "USD": 1.18, - "GBP": 0.86, - "JPY": 129.53, - }, - }, - } - - converter := ¤cyConverter{ - client: mockClient, - cache: newMockCache(), - config: Config{APIKey: "test"}, - } - - tests := []struct { - name string - amount float64 - from string - to string - expected float64 - expectError bool - }{ - { - name: "USD to EUR", - amount: 100, - from: "USD", - to: "EUR", - expected: 85, - }, - { - name: "USD to GBP", - amount: 100, - from: "USD", - to: "GBP", - expected: 73, - }, - { - name: "EUR to USD", - amount: 100, - from: "EUR", - to: "USD", - expected: 118, - }, - { - name: "Same currency", - amount: 100, - from: "USD", - to: "USD", - expected: 100, - }, - { - name: "Case insensitive", - amount: 100, - from: "usd", - to: "eur", - expected: 85, - }, - { - name: "Unsupported currency", - amount: 100, - from: "USD", - to: "XYZ", - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := converter.Convert(tt.amount, tt.from, tt.to) - - if tt.expectError { - if err == nil { - t.Errorf("expected error but got none") - } - return - } - - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - if result != tt.expected { - t.Errorf("expected %f, got %f", tt.expected, result) - } - }) - } -} - -func TestCurrencyConverter_GetRate(t *testing.T) { - mockClient := &mockClient{ - rates: map[string]map[string]float64{ - "USD": { - "USD": 1.0, - "EUR": 0.85, - "GBP": 0.73, - }, - }, - } - - converter := ¤cyConverter{ - client: mockClient, - cache: newMockCache(), - config: Config{APIKey: "test"}, - } - - // Test getting rate - rate, err := converter.GetRate("USD", "EUR") - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if rate != 0.85 { - t.Errorf("expected rate 0.85, got %f", rate) - } - - // Test same currency - rate, err = converter.GetRate("USD", "USD") - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if rate != 1.0 { - t.Errorf("expected rate 1.0, got %f", rate) - } - - // Test cache hit - rate, err = converter.GetRate("USD", "EUR") - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if rate != 0.85 { - t.Errorf("expected cached rate 0.85, got %f", rate) - } -} - -func TestNewConverter(t *testing.T) { - // Test with empty API key - _, err := NewConverter(Config{}) - if err == nil { - t.Error("expected error for empty API key") - } - - // Test with valid config - converter, err := NewConverter(Config{APIKey: "test-key"}) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - // Convert to concrete type to access internal fields - cc, ok := converter.(*currencyConverter) - if !ok { - t.Error("expected converter to be of type *currencyConverter") - return - } - - if cc.config.CacheTTL != 24*time.Hour { - t.Errorf("expected default cache TTL of 24h, got %v", cc.config.CacheTTL) - } - - if cc.config.APITimeout != 10*time.Second { - t.Errorf("expected default API timeout of 10s, got %v", cc.config.APITimeout) - } -} diff --git a/pkg/common/currency/go.mod b/pkg/common/currency/go.mod deleted file mode 100644 index 6dc42ee..0000000 --- a/pkg/common/currency/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/opencost/opencost-plugins/pkg/common/currency - -go 1.21 \ No newline at end of file diff --git a/pkg/common/currency/types.go b/pkg/common/currency/types.go deleted file mode 100644 index c79965b..0000000 --- a/pkg/common/currency/types.go +++ /dev/null @@ -1,60 +0,0 @@ -package currency - -import ( - "time" -) - -// Config holds configuration for the currency converter -type Config struct { - APIKey string - CacheTTL time.Duration - APITimeout time.Duration -} - -// Converter interface defines currency conversion operations -type Converter interface { - // Convert converts an amount from one currency to another - Convert(amount float64, from, to string) (float64, error) - - // GetRate returns the exchange rate between two currencies - GetRate(from, to string) (float64, error) -} - -// exchangeRateResponse represents the API response from exchangerate-api.com -type exchangeRateResponse struct { - Result string `json:"result"` - Documentation string `json:"documentation"` - TermsOfUse string `json:"terms_of_use"` - TimeLastUpdateUnix int64 `json:"time_last_update_unix"` - TimeLastUpdateUTC string `json:"time_last_update_utc"` - TimeNextUpdateUnix int64 `json:"time_next_update_unix"` - TimeNextUpdateUTC string `json:"time_next_update_utc"` - BaseCode string `json:"base_code"` - ConversionRates map[string]float64 `json:"conversion_rates"` -} - -// cachedRates stores exchange rates with metadata -type cachedRates struct { - rates map[string]float64 - baseCode string - fetchedAt time.Time - validUntil time.Time -} - -// client interface for fetching exchange rates -type client interface { - // fetchRates fetches current exchange rates for a base currency - fetchRates(baseCurrency string) (*exchangeRateResponse, error) -} - -// cache interface for storing exchange rates -type cache interface { - // get retrieves cached rates for a base currency - get(baseCurrency string) (*cachedRates, bool) - - // set stores rates for a base currency with TTL - set(baseCurrency string, rates *cachedRates) - - // clear removes all cached rates - clear() -} diff --git a/pkg/plugins/mongodb-atlas/cmd/main/main.go b/pkg/plugins/mongodb-atlas/cmd/main/main.go index 5761921..0bbb9b9 100644 --- a/pkg/plugins/mongodb-atlas/cmd/main/main.go +++ b/pkg/plugins/mongodb-atlas/cmd/main/main.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "github.com/opencost/opencost/pkg/currency" "io" "net/http" "time" @@ -10,7 +11,6 @@ import ( "github.com/hashicorp/go-plugin" "github.com/icholy/digest" commonconfig "github.com/opencost/opencost-plugins/common/config" - "github.com/opencost/opencost-plugins/common/currency" atlasconfig "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/config" atlasplugin "github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas/plugin" "github.com/opencost/opencost/core/pkg/log" @@ -117,7 +117,7 @@ func validateRequest(req *pb.CustomCostRequest) []string { // 1. Check if resolution is less than a day if req.Resolution.AsDuration() < 24*time.Hour { var resolutionMessage = "Resolution should be at least one day." - log.Warnf(resolutionMessage) + log.Warnf("%s", resolutionMessage) errors = append(errors, resolutionMessage) } // Get the start of the current month @@ -126,14 +126,14 @@ func validateRequest(req *pb.CustomCostRequest) []string { // 2. Check if start time is before the start of the current month if req.Start.AsTime().Before(currentMonthStart) { var startDateMessage = "Start date cannot be before the current month. Historical costs not currently supported" - log.Warnf(startDateMessage) + log.Warnf("%s", startDateMessage) errors = append(errors, startDateMessage) } // 3. Check if end time is before the start of the current month if req.End.AsTime().Before(currentMonthStart) { var endDateMessage = "End date cannot be before the current month. Historical costs not currently supported" - log.Warnf(endDateMessage) + log.Warnf("%s", endDateMessage) errors = append(errors, endDateMessage) } @@ -290,8 +290,8 @@ func GetPendingInvoices(org string, client HTTPClient) ([]atlasplugin.LineItem, response, error := client.Do(request) if error != nil { msg := fmt.Sprintf("getPending Invoices: error from server: %v", error) - log.Errorf(msg) - return nil, fmt.Errorf(msg) + log.Errorf("%s", msg) + return nil, fmt.Errorf("%s", msg) } @@ -302,8 +302,8 @@ func GetPendingInvoices(org string, client HTTPClient) ([]atlasplugin.LineItem, respUnmarshalError := json.Unmarshal([]byte(body), &pendingInvoicesResponse) if respUnmarshalError != nil { msg := fmt.Sprintf("pendingInvoices: error unmarshalling response: %v", respUnmarshalError) - log.Errorf(msg) - return nil, fmt.Errorf(msg) + log.Errorf("%s", msg) + return nil, fmt.Errorf("%s", msg) } return pendingInvoicesResponse.LineItems, nil diff --git a/pkg/plugins/mongodb-atlas/go.mod b/pkg/plugins/mongodb-atlas/go.mod index 4d2caee..a1ac1c2 100644 --- a/pkg/plugins/mongodb-atlas/go.mod +++ b/pkg/plugins/mongodb-atlas/go.mod @@ -1,70 +1,74 @@ module github.com/opencost/opencost-plugins/pkg/plugins/mongodb-atlas -go 1.22.5 +go 1.25.1 replace github.com/opencost/opencost-plugins/common => ../../common -replace github.com/opencost/opencost-plugins/common/currency => ../../common/currency +replace github.com/opencost/opencost/core => ../../../../opencost/core + +replace github.com/opencost/opencost => ../../../../opencost require ( - github.com/hashicorp/go-plugin v1.6.1 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-plugin v1.7.0 github.com/icholy/digest v0.1.23 + github.com/opencost/opencost v0.0.0-00010101000000-000000000000 github.com/opencost/opencost-plugins/common v0.0.0-00010101000000-000000000000 - github.com/opencost/opencost-plugins/common/currency v0.0.0-00010101000000-000000000000 - github.com/opencost/opencost/core v0.0.0-20240829194822-b82370afd830 - github.com/stretchr/testify v1.9.0 - golang.org/x/time v0.6.0 - google.golang.org/protobuf v1.34.2 - k8s.io/apimachinery v0.25.3 + github.com/opencost/opencost/core v0.0.0-20250521155634-81d2b597d1bc + github.com/stretchr/testify v1.10.0 + golang.org/x/time v0.12.0 + google.golang.org/protobuf v1.36.6 + k8s.io/apimachinery v0.33.1 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fatih/color v1.17.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect - github.com/goccy/go-json v0.9.11 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.3 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rs/zerolog v1.26.1 // indirect - github.com/spf13/afero v1.6.0 // indirect + github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.8.1 // indirect github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect - google.golang.org/grpc v1.66.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect + google.golang.org/grpc v1.74.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.25.3 // indirect - k8s.io/klog/v2 v2.80.0 // indirect - k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + k8s.io/api v0.33.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/pkg/plugins/mongodb-atlas/go.sum b/pkg/plugins/mongodb-atlas/go.sum index 77b2838..d7ec7f8 100644 --- a/pkg/plugins/mongodb-atlas/go.sum +++ b/pkg/plugins/mongodb-atlas/go.sum @@ -3,6 +3,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -15,6 +16,7 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -36,6 +38,7 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -45,8 +48,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= -github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -58,8 +61,9 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -74,16 +78,18 @@ github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLg github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -131,11 +137,10 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -148,6 +153,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -156,6 +162,7 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= @@ -170,8 +177,8 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= -github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -186,14 +193,14 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/icholy/digest v0.1.23 h1:4hX2pIloP0aDx7RJW0JewhPPy3R8kU+vWKdxPsCCGtY= github.com/icholy/digest v0.1.23/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= -github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= -github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -204,10 +211,11 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -225,8 +233,6 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -241,12 +247,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/opencost/opencost/core v0.0.0-20240829194822-b82370afd830 h1:PDYQw0cygJ8ehn/AObpRVru4Cg718aGrDJQis4XfHWg= -github.com/opencost/opencost/core v0.0.0-20240829194822-b82370afd830/go.mod h1:c1he7ogYA3J/m2BNbWD5FDLRTpUwuG8CGIyOkDV+i+s= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= @@ -255,12 +257,16 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= @@ -268,9 +274,9 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= @@ -287,10 +293,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -307,6 +315,18 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= @@ -317,7 +337,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -328,8 +350,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -387,13 +409,15 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -453,13 +477,16 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -467,8 +494,8 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -478,13 +505,14 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -534,6 +562,7 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= @@ -605,14 +634,16 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 h1:qJW29YvkiJmXOYMu5Tf8lyrTp3dOS+K4z6IixtLaCf8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -633,8 +664,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -647,12 +678,12 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -677,21 +708,23 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.25.3 h1:Q1v5UFfYe87vi5H7NU0p4RXC26PPMT8KOpr1TLQbCMQ= -k8s.io/api v0.25.3/go.mod h1:o42gKscFrEVjHdQnyRenACrMtbuJsVdP+WVjqejfzmI= -k8s.io/apimachinery v0.25.3 h1:7o9ium4uyUOM76t6aunP0nZuex7gDf8VGwkR5RcJnQc= -k8s.io/apimachinery v0.25.3/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.80.0 h1:lyJt0TWMPaGoODa8B8bUuxgHS3W/m/bNr2cca3brA/g= -k8s.io/klog/v2 v2.80.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=