Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/config/recommendation_overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"context"
"errors"
"fmt"
)

Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions internal/config/recommendation_overrides_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"context"
"errors"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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")
}
4 changes: 2 additions & 2 deletions internal/config/store_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion internal/config/store_postgres_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading