diff --git a/internal/config/recommendation_overrides.go b/internal/config/recommendation_overrides.go index d8f4be829..0712b4c64 100644 --- a/internal/config/recommendation_overrides.go +++ b/internal/config/recommendation_overrides.go @@ -2,6 +2,7 @@ package config import ( "context" + "errors" "fmt" ) @@ -48,6 +49,10 @@ func (c *globalConfigCache) lookup(ctx context.Context, store AccountConfigReade } fetched, err := store.GetServiceConfig(ctx, provider, service) if err != nil { + if errors.Is(err, ErrNotFound) { + c.missing[k] = true + return nil, nil + } return nil, fmt.Errorf("get service config %s/%s: %w", provider, service, err) } if fetched == nil { diff --git a/internal/config/recommendation_overrides_test.go b/internal/config/recommendation_overrides_test.go index 3ae5f1e6d..c75b88ae4 100644 --- a/internal/config/recommendation_overrides_test.go +++ b/internal/config/recommendation_overrides_test.go @@ -3,6 +3,7 @@ package config import ( "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -254,3 +255,20 @@ func TestResolveAccountConfigsForRecs_OverrideErrorPropagates(t *testing.T) { assert.Error(t, err) assert.Empty(t, got) } + +// TestResolveAccountConfigsForRecs_ErrNotFoundAbsorbed verifies that when +// GetServiceConfig returns a wrapped ErrNotFound (the no-rows case), the +// lookup treats it as absent rather than propagating it as an error. This +// is the root scenario of issue #263: the INFO log firing on every refresh +// for services that legitimately have no global config row. +func TestResolveAccountConfigsForRecs_ErrNotFoundAbsorbed(t *testing.T) { + reader := &fakeAccountConfigReader{ + globalErr: fmt.Errorf("service config not found for aws:rds: %w", ErrNotFound), + } + recs := []RecommendationRecord{acctRec("acct-A", "aws", "rds")} + + got, err := ResolveAccountConfigsForRecs(context.Background(), reader, recs) + assert.NoError(t, err, "ErrNotFound from GetServiceConfig must not propagate as an error") + assert.Empty(t, got, "triple skipped when global is absent and no override") + assert.Equal(t, 1, reader.globalCalls, "global fetched once") +} diff --git a/internal/config/store_postgres.go b/internal/config/store_postgres.go index b53628283..3771ccdc7 100644 --- a/internal/config/store_postgres.go +++ b/internal/config/store_postgres.go @@ -352,7 +352,7 @@ func (s *PostgresStore) GetServiceConfig(ctx context.Context, provider, service if err != nil { if errors.Is(err, pgx.ErrNoRows) { - return nil, fmt.Errorf("service config not found for %s:%s", provider, service) + return nil, fmt.Errorf("service config not found for %s:%s: %w", provider, service, ErrNotFound) } return nil, fmt.Errorf("failed to get service config: %w", err) } @@ -1833,7 +1833,7 @@ func (s *PostgresStore) GetActivePurchaseHistory(ctx context.Context, asOf time. // number, unknown provider) matches account_id with no provider gate. Providers // are sorted for deterministic SQL. The OR is wrapped in parentheses so it // composes with the surrounding AND chain. -func appendAccountPredicate(conds []string, args []any, accountIDs []string, externalIDsByProvider map[string][]string) ([]string, []any) { //nolint:gocritic // unnamedResult: return names would conflict with body locals +func appendAccountPredicate(conds []string, args []any, accountIDs []string, externalIDsByProvider map[string][]string) (outConds []string, outArgs []any) { if len(accountIDs) == 0 && len(externalIDsByProvider) == 0 { return conds, args } diff --git a/internal/config/store_postgres_mock_test.go b/internal/config/store_postgres_mock_test.go index 105966513..f9bc56f68 100644 --- a/internal/config/store_postgres_mock_test.go +++ b/internal/config/store_postgres_mock_test.go @@ -131,7 +131,7 @@ func (s *testablePostgresStore) GetServiceConfig(ctx context.Context, provider, if err != nil { if errors.Is(err, pgx.ErrNoRows) { - return nil, errors.New("service config not found") + return nil, fmt.Errorf("service config not found for %s:%s: %w", provider, service, ErrNotFound) } return nil, err }