From ec24cf959db7517ec45cd1d8849aa8c12b2a9ead Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Fri, 13 Mar 2026 16:26:37 +0000 Subject: [PATCH 01/11] add protection log in logger --- pkg/queryfrontend/queryrange_logger.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/pkg/queryfrontend/queryrange_logger.go b/pkg/queryfrontend/queryrange_logger.go index 665a857e127..6ed3d323997 100644 --- a/pkg/queryfrontend/queryrange_logger.go +++ b/pkg/queryfrontend/queryrange_logger.go @@ -58,6 +58,10 @@ type MetricsRangeQueryLogging struct { Shard string `json:"shard"` // Pantheon shard name // Store-matcher details StoreMatchers []StoreMatcherSet `json:"storeMatchers"` + // Protection fields + ProtectionTriggered bool `json:"protectionTriggered"` // Whether a protection rule was triggered + ProtectionRuleName string `json:"protectionRuleName"` // Name of the rule that triggered + ProtectionAction string `json:"protectionAction"` // Action taken: "log" or "block" } // RangeQueryLogConfig holds configuration for range query logging. @@ -130,13 +134,13 @@ func (m *rangeQueryLoggingMiddleware) Do(ctx context.Context, r queryrange.Reque // Calculate latency. latencyMs := time.Since(startTime).Milliseconds() - // Log the range query. - m.logRangeQuery(rangeReq, resp, err, latencyMs) + // Log the range query, passing ctx so protection results can be read. + m.logRangeQuery(ctx, rangeReq, resp, err, latencyMs) return resp, err } -func (m *rangeQueryLoggingMiddleware) logRangeQuery(req *ThanosQueryRangeRequest, resp queryrange.Response, err error, latencyMs int64) { +func (m *rangeQueryLoggingMiddleware) logRangeQuery(ctx context.Context, req *ThanosQueryRangeRequest, resp queryrange.Response, err error, latencyMs int64) { success := err == nil userInfo := ExtractUserInfoFromHeaders(req.Headers) @@ -148,6 +152,15 @@ func (m *rangeQueryLoggingMiddleware) logRangeQuery(req *ThanosQueryRangeRequest metricNames = ExtractMetricNames(resp) } + // Read protection result from context (written by protection middleware). + var protectionTriggered bool + var protectionRuleName, protectionAction string + if result := GetProtectionResult(ctx); result != nil { + protectionTriggered = result.Triggered + protectionRuleName = result.RuleName + protectionAction = result.Action + } + // Create the range query log entry. rangeQueryLog := MetricsRangeQueryLogging{ TimestampMs: time.Now().UnixMilli(), @@ -188,6 +201,10 @@ func (m *rangeQueryLoggingMiddleware) logRangeQuery(req *ThanosQueryRangeRequest Shard: os.Getenv("PANTHEON_SHARDNAME"), // Store-matcher details StoreMatchers: ConvertStoreMatchers(req.StoreMatchers), + // Protection fields + ProtectionTriggered: protectionTriggered, + ProtectionRuleName: protectionRuleName, + ProtectionAction: protectionAction, } // Log to file if available. From 20cd0b155bacfd9b20d2349f233ddd02fc5a66d6 Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Fri, 13 Mar 2026 16:51:53 +0000 Subject: [PATCH 02/11] basic datastructures --- pkg/queryfrontend/protection.go | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 pkg/queryfrontend/protection.go diff --git a/pkg/queryfrontend/protection.go b/pkg/queryfrontend/protection.go new file mode 100644 index 00000000000..72c0cb0cd76 --- /dev/null +++ b/pkg/queryfrontend/protection.go @@ -0,0 +1,40 @@ +// Copyright (c) The Thanos Authors. +// Licensed under the Apache License 2.0. + +package queryfrontend + +import ( + "context" +) + +// RuleAction represents the action to take when a protection rule is triggered. +type RuleAction int + +const ( + RuleActionLog RuleAction = iota + RuleActionBlock RuleAction = iota +) + +// ProtectionResult holds the result of evaluating protection rules against a query. +// It is stored in the context by the protection middleware and read by the logging middleware. +type ProtectionResult struct { + Triggered bool + RuleName string + Action string // "log" or "block" +} + +type protectionContextKey int + +const protectionResultKey protectionContextKey = iota + +// WithProtectionResult stores a ProtectionResult in the context. +func WithProtectionResult(ctx context.Context, result *ProtectionResult) context.Context { + return context.WithValue(ctx, protectionResultKey, result) +} + +// GetProtectionResult retrieves a ProtectionResult from the context. +// Returns nil if no result is stored. +func GetProtectionResult(ctx context.Context) *ProtectionResult { + result, _ := ctx.Value(protectionResultKey).(*ProtectionResult) + return result +} From 89fa137369fd9d667bf53646003d0f3d3eaf2dcb Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Fri, 13 Mar 2026 19:20:06 +0000 Subject: [PATCH 03/11] change ProtectionResult Action from string to RuleAction --- pkg/queryfrontend/protection.go | 14 +++++++++++++- pkg/queryfrontend/queryrange_logger.go | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/queryfrontend/protection.go b/pkg/queryfrontend/protection.go index 72c0cb0cd76..e76b9dac699 100644 --- a/pkg/queryfrontend/protection.go +++ b/pkg/queryfrontend/protection.go @@ -20,7 +20,7 @@ const ( type ProtectionResult struct { Triggered bool RuleName string - Action string // "log" or "block" + Action RuleAction } type protectionContextKey int @@ -38,3 +38,15 @@ func GetProtectionResult(ctx context.Context) *ProtectionResult { result, _ := ctx.Value(protectionResultKey).(*ProtectionResult) return result } + +// RuleActionToString converts a RuleAction to a string. +func RuleActionToString(action RuleAction) string { + switch action { + case RuleActionLog: + return "RuleActionLog" + case RuleActionBlock: + return "RuleActionBlock" + default: + return "Unknown RuleAction" + } +} diff --git a/pkg/queryfrontend/queryrange_logger.go b/pkg/queryfrontend/queryrange_logger.go index 6ed3d323997..68d484760d3 100644 --- a/pkg/queryfrontend/queryrange_logger.go +++ b/pkg/queryfrontend/queryrange_logger.go @@ -61,7 +61,7 @@ type MetricsRangeQueryLogging struct { // Protection fields ProtectionTriggered bool `json:"protectionTriggered"` // Whether a protection rule was triggered ProtectionRuleName string `json:"protectionRuleName"` // Name of the rule that triggered - ProtectionAction string `json:"protectionAction"` // Action taken: "log" or "block" + ProtectionAction string `json:"protectionAction"` // "RuleActionLog" or "RuleActionBlock" } // RangeQueryLogConfig holds configuration for range query logging. @@ -158,7 +158,7 @@ func (m *rangeQueryLoggingMiddleware) logRangeQuery(ctx context.Context, req *Th if result := GetProtectionResult(ctx); result != nil { protectionTriggered = result.Triggered protectionRuleName = result.RuleName - protectionAction = result.Action + protectionAction = RuleActionToString(result.Action) } // Create the range query log entry. From 89a5c0adfec938d66c649962dfe7bfe68dacf99b Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Mon, 16 Mar 2026 17:17:26 +0000 Subject: [PATCH 04/11] simplified ProtectionAction string --- pkg/queryfrontend/protection.go | 6 +++--- pkg/queryfrontend/queryrange_logger.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/queryfrontend/protection.go b/pkg/queryfrontend/protection.go index e76b9dac699..b95f67fa6aa 100644 --- a/pkg/queryfrontend/protection.go +++ b/pkg/queryfrontend/protection.go @@ -43,10 +43,10 @@ func GetProtectionResult(ctx context.Context) *ProtectionResult { func RuleActionToString(action RuleAction) string { switch action { case RuleActionLog: - return "RuleActionLog" + return "Log" case RuleActionBlock: - return "RuleActionBlock" + return "Block" default: - return "Unknown RuleAction" + return "Unknown" } } diff --git a/pkg/queryfrontend/queryrange_logger.go b/pkg/queryfrontend/queryrange_logger.go index 68d484760d3..5ff20395a2e 100644 --- a/pkg/queryfrontend/queryrange_logger.go +++ b/pkg/queryfrontend/queryrange_logger.go @@ -61,7 +61,7 @@ type MetricsRangeQueryLogging struct { // Protection fields ProtectionTriggered bool `json:"protectionTriggered"` // Whether a protection rule was triggered ProtectionRuleName string `json:"protectionRuleName"` // Name of the rule that triggered - ProtectionAction string `json:"protectionAction"` // "RuleActionLog" or "RuleActionBlock" + ProtectionAction string `json:"protectionAction"` // "Log" or "Block" } // RangeQueryLogConfig holds configuration for range query logging. From f91a3ff3c26f84f912964eeec5253e4830d39413 Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Mon, 16 Mar 2026 22:52:48 +0000 Subject: [PATCH 05/11] add protection engine and sample protection impls --- pkg/queryfrontend/protection.go | 41 +++++++++++++++++ pkg/queryfrontend/protection_engine.go | 61 ++++++++++++++++++++++++++ pkg/queryfrontend/protection_impls.go | 39 ++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 pkg/queryfrontend/protection_engine.go create mode 100644 pkg/queryfrontend/protection_impls.go diff --git a/pkg/queryfrontend/protection.go b/pkg/queryfrontend/protection.go index b95f67fa6aa..2577ef80a46 100644 --- a/pkg/queryfrontend/protection.go +++ b/pkg/queryfrontend/protection.go @@ -5,6 +5,10 @@ package queryfrontend import ( "context" + "regexp" + + "github.com/prometheus/prometheus/promql/parser" + "github.com/thanos-io/thanos/internal/cortex/querier/queryrange" ) // RuleAction represents the action to take when a protection rule is triggered. @@ -50,3 +54,40 @@ func RuleActionToString(action RuleAction) string { return "Unknown" } } + +// thanosQueryReq wraps a query request with parsed PromQL and actor information. +type thanosQueryReq struct { + inner queryrange.Request + parsed parser.Expr + actor string +} + +// Protection is the interface that all protection implementations must satisfy. +// It only determines whether a query matches — the action (log/block) is configured in Rule. +type Protection interface { + Name() string + Run(ctx context.Context, req thanosQueryReq) (bool, error) +} + +// ProtectionFactory constructs a Protection from a map of args parsed from config. +type ProtectionFactory func(args map[string]string) (Protection, error) + +// Rule combines a Protection with its configured action, actor filter, and enabled state. +type Rule struct { + name string + protection Protection + action RuleAction + actorRegex *regexp.Regexp + enabled bool +} + +// NewRule creates a Rule. actorRegex may be nil to match all actors. +func NewRule(name string, protection Protection, action RuleAction, actorRegex *regexp.Regexp, enabled bool) *Rule { + return &Rule{ + name: name, + protection: protection, + action: action, + actorRegex: actorRegex, + enabled: enabled, + } +} diff --git a/pkg/queryfrontend/protection_engine.go b/pkg/queryfrontend/protection_engine.go new file mode 100644 index 00000000000..15f33041f6d --- /dev/null +++ b/pkg/queryfrontend/protection_engine.go @@ -0,0 +1,61 @@ +// Copyright (c) The Thanos Authors. +// Licensed under the Apache License 2.0. + +package queryfrontend + +import ( + "context" + "sync" +) + +// ProtectionEngine evaluates a list of rules against a query request. +type ProtectionEngine struct { + mu sync.RWMutex + rules []*Rule +} + +// NewProtectionEngine creates a new ProtectionEngine with the given rules. +func NewProtectionEngine(rules []*Rule) *ProtectionEngine { + return &ProtectionEngine{rules: rules} +} + +// UpdateRules replaces the current rule set. Safe for concurrent use. +func (e *ProtectionEngine) UpdateRules(rules []*Rule) { + e.mu.Lock() + defer e.mu.Unlock() + e.rules = rules +} + +// Evaluate runs all applicable rules against the request. +// Rules are evaluated in order; the first matching rule wins. +// Returns the updated context (with ProtectionResult if a rule triggered), +// the action to take, and any error. +func (e *ProtectionEngine) Evaluate(ctx context.Context, req thanosQueryReq) (*ProtectionResult, error) { + e.mu.RLock() + rules := e.rules + e.mu.RUnlock() + + for _, rule := range rules { + // Skip disabled rules. + if !rule.enabled { + continue + } + + // Check actor filter. + if rule.actorRegex != nil && !rule.actorRegex.MatchString(req.actor) { + continue + } + + matched, err := rule.protection.Run(ctx, req) + if !matched { + continue + } + + return &ProtectionResult{ + Triggered: true, + RuleName: rule.name, + Action: rule.action, + }, err + } + return nil, nil +} diff --git a/pkg/queryfrontend/protection_impls.go b/pkg/queryfrontend/protection_impls.go new file mode 100644 index 00000000000..ab3ebe3bc62 --- /dev/null +++ b/pkg/queryfrontend/protection_impls.go @@ -0,0 +1,39 @@ +// Copyright (c) The Thanos Authors. +// Licensed under the Apache License 2.0. + +package queryfrontend + +import ( + "context" + + "github.com/pkg/errors" +) + +// This file contains all protection rule implementations. +// Each protection implements the Protection interface defined in protection.go. + +// NoopProtection is a protection that always matches but never blocks any query. +// Used in M2 to verify the protection engine framework without affecting queries. +type NoopProtection struct{} + +func (n *NoopProtection) Name() string { return "noop" } + +func (n *NoopProtection) Run(_ context.Context, _ thanosQueryReq) (bool, error) { + return true, nil +} + +// protectionRegistry maps protection names (as used in config) to their factory functions. +var protectionRegistry = map[string]ProtectionFactory{ + "noop": func(_ map[string]string) (Protection, error) { + return &NoopProtection{}, nil + }, +} + +// LookupProtection returns the factory for the given protection name. +func LookupProtection(name string) (ProtectionFactory, error) { + factory, ok := protectionRegistry[name] + if !ok { + return nil, errors.Errorf("unknown protection %q", name) + } + return factory, nil +} From e6ddf82a99a6e297d5eae134e053d566d393eae8 Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Mon, 16 Mar 2026 22:53:34 +0000 Subject: [PATCH 06/11] add protection engine and impls unit tests --- pkg/queryfrontend/protection_engine_test.go | 114 ++++++++++++++++++++ pkg/queryfrontend/protection_impls_test.go | 34 ++++++ 2 files changed, 148 insertions(+) create mode 100644 pkg/queryfrontend/protection_engine_test.go create mode 100644 pkg/queryfrontend/protection_impls_test.go diff --git a/pkg/queryfrontend/protection_engine_test.go b/pkg/queryfrontend/protection_engine_test.go new file mode 100644 index 00000000000..8f2465c51d4 --- /dev/null +++ b/pkg/queryfrontend/protection_engine_test.go @@ -0,0 +1,114 @@ +// Copyright (c) The Thanos Authors. +// Licensed under the Apache License 2.0. + +// Tests for ProtectionEngine.Evaluate: verifies that rules are correctly +// evaluated, filtered, and prioritized against query requests. +package queryfrontend + +import ( + "context" + "regexp" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +// alwaysMatchProtection always matches (returns true). +type alwaysMatchProtection struct{} + +func (p *alwaysMatchProtection) Name() string { return "always" } +func (p *alwaysMatchProtection) Run(_ context.Context, _ thanosQueryReq) (bool, error) { + return true, nil +} + +// neverMatchProtection never matches (returns false). +type neverMatchProtection struct{} + +func (p *neverMatchProtection) Name() string { return "never" } +func (p *neverMatchProtection) Run(_ context.Context, _ thanosQueryReq) (bool, error) { + return false, nil +} + +// errorProtection always matches and returns an error. +type errorProtection struct{} + +func (p *errorProtection) Name() string { return "error" } +func (p *errorProtection) Run(_ context.Context, _ thanosQueryReq) (bool, error) { + return true, errors.New("protection error") +} + +func TestProtectionEngine_NoRules(t *testing.T) { + engine := NewProtectionEngine(nil) + protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{}) + require.NoError(t, err) + require.Nil(t, protectionResult) +} + +func TestProtectionEngine_DisabledRuleSkipped(t *testing.T) { + engine := NewProtectionEngine([]*Rule{ + NewRule("disabled", &alwaysMatchProtection{}, RuleActionBlock, nil, false), + }) + protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{}) + require.NoError(t, err) + require.Nil(t, protectionResult) +} + +func TestProtectionEngine_ActorRegexNoMatch(t *testing.T) { + engine := NewProtectionEngine([]*Rule{ + NewRule("filtered", &alwaysMatchProtection{}, RuleActionBlock, regexp.MustCompile("^admin$"), true), + }) + protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{actor: "user"}) + require.NoError(t, err) + require.Nil(t, protectionResult) +} + +func TestProtectionEngine_ActorRegexMatch(t *testing.T) { + engine := NewProtectionEngine([]*Rule{ + NewRule("filtered", &alwaysMatchProtection{}, RuleActionBlock, regexp.MustCompile("^admin$"), true), + }) + protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{actor: "admin"}) + require.NoError(t, err) + require.Equal(t, RuleActionBlock, protectionResult.Action) + require.True(t, protectionResult.Triggered) + require.Equal(t, "filtered", protectionResult.RuleName) +} + +func TestProtectionEngine_FirstMatchingRuleWins(t *testing.T) { + engine := NewProtectionEngine([]*Rule{ + NewRule("first", &neverMatchProtection{}, RuleActionBlock, nil, true), + NewRule("second", &alwaysMatchProtection{}, RuleActionLog, nil, true), + NewRule("third", &alwaysMatchProtection{}, RuleActionBlock, nil, true), + }) + protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{}) + require.NoError(t, err) + require.Equal(t, RuleActionLog, protectionResult.Action) + require.Equal(t, true, protectionResult.Triggered) + require.Equal(t, "second", protectionResult.RuleName) +} + +func TestProtectionEngine_RunError(t *testing.T) { + engine := NewProtectionEngine([]*Rule{ + NewRule("error-rule", &errorProtection{}, RuleActionLog, nil, true), + }) + _, err := engine.Evaluate(context.Background(), thanosQueryReq{}) + require.Error(t, err) + require.Contains(t, err.Error(), "protection error") +} + +func TestProtectionEngine_UpdateRules(t *testing.T) { + engine := NewProtectionEngine([]*Rule{ + NewRule("block-all", &alwaysMatchProtection{}, RuleActionBlock, nil, true), + }) + + protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{}) + require.NoError(t, err) + require.Equal(t, RuleActionBlock, protectionResult.Action) + require.Equal(t, true, protectionResult.Triggered) + + engine.UpdateRules(nil) + + protectionResult, err = engine.Evaluate(context.Background(), thanosQueryReq{}) + require.NoError(t, err) + require.Nil(t, protectionResult) +} diff --git a/pkg/queryfrontend/protection_impls_test.go b/pkg/queryfrontend/protection_impls_test.go new file mode 100644 index 00000000000..fc5f0d3effe --- /dev/null +++ b/pkg/queryfrontend/protection_impls_test.go @@ -0,0 +1,34 @@ +// Copyright (c) The Thanos Authors. +// Licensed under the Apache License 2.0. + +// Tests for protection implementations in protection_impls.go. +package queryfrontend + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNoopProtection_AlwaysMatches(t *testing.T) { + p := &NoopProtection{} + matched, err := p.Run(context.Background(), thanosQueryReq{}) + require.NoError(t, err) + require.True(t, matched) +} + +func TestLookupProtection_Noop(t *testing.T) { + factory, err := LookupProtection("noop") + require.NoError(t, err) + require.NotNil(t, factory) + + p, err := factory(nil) + require.NoError(t, err) + require.Equal(t, "noop", p.Name()) +} + +func TestLookupProtection_Unknown(t *testing.T) { + _, err := LookupProtection("unknown") + require.Error(t, err) +} From 3f389ac90e535e9eb9d0df278cfe52b47a19a6fd Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Mon, 16 Mar 2026 23:31:10 +0000 Subject: [PATCH 07/11] lint: remove current unused fields --- pkg/queryfrontend/protection.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/queryfrontend/protection.go b/pkg/queryfrontend/protection.go index 2577ef80a46..3ca3b3067db 100644 --- a/pkg/queryfrontend/protection.go +++ b/pkg/queryfrontend/protection.go @@ -6,9 +6,6 @@ package queryfrontend import ( "context" "regexp" - - "github.com/prometheus/prometheus/promql/parser" - "github.com/thanos-io/thanos/internal/cortex/querier/queryrange" ) // RuleAction represents the action to take when a protection rule is triggered. @@ -55,11 +52,9 @@ func RuleActionToString(action RuleAction) string { } } -// thanosQueryReq wraps a query request with parsed PromQL and actor information. +// thanosQueryReq wraps a query request with actor information. type thanosQueryReq struct { - inner queryrange.Request - parsed parser.Expr - actor string + actor string } // Protection is the interface that all protection implementations must satisfy. From bfd7d5e37991e4d7d20b65b880d4d330905deea8 Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Thu, 19 Mar 2026 16:35:00 +0000 Subject: [PATCH 08/11] use sync.Mutex instead of sync.RWMutex --- pkg/queryfrontend/protection_engine.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/queryfrontend/protection_engine.go b/pkg/queryfrontend/protection_engine.go index 15f33041f6d..a6b3b012061 100644 --- a/pkg/queryfrontend/protection_engine.go +++ b/pkg/queryfrontend/protection_engine.go @@ -10,7 +10,7 @@ import ( // ProtectionEngine evaluates a list of rules against a query request. type ProtectionEngine struct { - mu sync.RWMutex + mu sync.Mutex rules []*Rule } @@ -31,9 +31,9 @@ func (e *ProtectionEngine) UpdateRules(rules []*Rule) { // Returns the updated context (with ProtectionResult if a rule triggered), // the action to take, and any error. func (e *ProtectionEngine) Evaluate(ctx context.Context, req thanosQueryReq) (*ProtectionResult, error) { - e.mu.RLock() + e.mu.Lock() rules := e.rules - e.mu.RUnlock() + e.mu.Unlock() for _, rule := range rules { // Skip disabled rules. From 73d76f507c10164fe83e0741cb90632e3b553d50 Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Fri, 20 Mar 2026 17:41:29 +0000 Subject: [PATCH 09/11] rename NoopProtection to AlwaysMatchProtection for clarity --- pkg/queryfrontend/protection_impls.go | 11 +++++------ pkg/queryfrontend/protection_impls_test.go | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pkg/queryfrontend/protection_impls.go b/pkg/queryfrontend/protection_impls.go index ab3ebe3bc62..c2ae5fcb46a 100644 --- a/pkg/queryfrontend/protection_impls.go +++ b/pkg/queryfrontend/protection_impls.go @@ -12,20 +12,19 @@ import ( // This file contains all protection rule implementations. // Each protection implements the Protection interface defined in protection.go. -// NoopProtection is a protection that always matches but never blocks any query. -// Used in M2 to verify the protection engine framework without affecting queries. -type NoopProtection struct{} +// AlwaysMatchProtection is a protection that always matches every query. +type AlwaysMatchProtection struct{} -func (n *NoopProtection) Name() string { return "noop" } +func (n *AlwaysMatchProtection) Name() string { return "noop" } -func (n *NoopProtection) Run(_ context.Context, _ thanosQueryReq) (bool, error) { +func (n *AlwaysMatchProtection) Run(_ context.Context, _ thanosQueryReq) (bool, error) { return true, nil } // protectionRegistry maps protection names (as used in config) to their factory functions. var protectionRegistry = map[string]ProtectionFactory{ "noop": func(_ map[string]string) (Protection, error) { - return &NoopProtection{}, nil + return &AlwaysMatchProtection{}, nil }, } diff --git a/pkg/queryfrontend/protection_impls_test.go b/pkg/queryfrontend/protection_impls_test.go index fc5f0d3effe..411ac1d69cf 100644 --- a/pkg/queryfrontend/protection_impls_test.go +++ b/pkg/queryfrontend/protection_impls_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/require" ) -func TestNoopProtection_AlwaysMatches(t *testing.T) { - p := &NoopProtection{} +func TestAlwaysMatchProtection_AlwaysMatches(t *testing.T) { + p := &AlwaysMatchProtection{} matched, err := p.Run(context.Background(), thanosQueryReq{}) require.NoError(t, err) require.True(t, matched) From 9ff1c51955e793b346cb51b07377ad054d7dc73b Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Fri, 20 Mar 2026 18:46:24 +0000 Subject: [PATCH 10/11] rename noop protection to always-match --- pkg/queryfrontend/protection_impls.go | 4 ++-- pkg/queryfrontend/protection_impls_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/queryfrontend/protection_impls.go b/pkg/queryfrontend/protection_impls.go index c2ae5fcb46a..a012238ee31 100644 --- a/pkg/queryfrontend/protection_impls.go +++ b/pkg/queryfrontend/protection_impls.go @@ -15,7 +15,7 @@ import ( // AlwaysMatchProtection is a protection that always matches every query. type AlwaysMatchProtection struct{} -func (n *AlwaysMatchProtection) Name() string { return "noop" } +func (n *AlwaysMatchProtection) Name() string { return "always-match" } func (n *AlwaysMatchProtection) Run(_ context.Context, _ thanosQueryReq) (bool, error) { return true, nil @@ -23,7 +23,7 @@ func (n *AlwaysMatchProtection) Run(_ context.Context, _ thanosQueryReq) (bool, // protectionRegistry maps protection names (as used in config) to their factory functions. var protectionRegistry = map[string]ProtectionFactory{ - "noop": func(_ map[string]string) (Protection, error) { + "always-match": func(_ map[string]string) (Protection, error) { return &AlwaysMatchProtection{}, nil }, } diff --git a/pkg/queryfrontend/protection_impls_test.go b/pkg/queryfrontend/protection_impls_test.go index 411ac1d69cf..32d87e4def0 100644 --- a/pkg/queryfrontend/protection_impls_test.go +++ b/pkg/queryfrontend/protection_impls_test.go @@ -18,14 +18,14 @@ func TestAlwaysMatchProtection_AlwaysMatches(t *testing.T) { require.True(t, matched) } -func TestLookupProtection_Noop(t *testing.T) { - factory, err := LookupProtection("noop") +func TestLookupProtection_AlwaysMatch(t *testing.T) { + factory, err := LookupProtection("always-match") require.NoError(t, err) require.NotNil(t, factory) p, err := factory(nil) require.NoError(t, err) - require.Equal(t, "noop", p.Name()) + require.Equal(t, "always-match", p.Name()) } func TestLookupProtection_Unknown(t *testing.T) { From dd71bad76a5059388ea66f4cdd0a179b71b82368 Mon Sep 17 00:00:00 2001 From: Yi Shi Date: Mon, 23 Mar 2026 16:23:38 +0000 Subject: [PATCH 11/11] deduplicate alwaysMatchProtection in engine test --- pkg/queryfrontend/protection_engine_test.go | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/pkg/queryfrontend/protection_engine_test.go b/pkg/queryfrontend/protection_engine_test.go index 8f2465c51d4..5383ddd07bb 100644 --- a/pkg/queryfrontend/protection_engine_test.go +++ b/pkg/queryfrontend/protection_engine_test.go @@ -14,14 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -// alwaysMatchProtection always matches (returns true). -type alwaysMatchProtection struct{} - -func (p *alwaysMatchProtection) Name() string { return "always" } -func (p *alwaysMatchProtection) Run(_ context.Context, _ thanosQueryReq) (bool, error) { - return true, nil -} - // neverMatchProtection never matches (returns false). type neverMatchProtection struct{} @@ -47,7 +39,7 @@ func TestProtectionEngine_NoRules(t *testing.T) { func TestProtectionEngine_DisabledRuleSkipped(t *testing.T) { engine := NewProtectionEngine([]*Rule{ - NewRule("disabled", &alwaysMatchProtection{}, RuleActionBlock, nil, false), + NewRule("disabled", &AlwaysMatchProtection{}, RuleActionBlock, nil, false), }) protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{}) require.NoError(t, err) @@ -56,7 +48,7 @@ func TestProtectionEngine_DisabledRuleSkipped(t *testing.T) { func TestProtectionEngine_ActorRegexNoMatch(t *testing.T) { engine := NewProtectionEngine([]*Rule{ - NewRule("filtered", &alwaysMatchProtection{}, RuleActionBlock, regexp.MustCompile("^admin$"), true), + NewRule("filtered", &AlwaysMatchProtection{}, RuleActionBlock, regexp.MustCompile("^admin$"), true), }) protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{actor: "user"}) require.NoError(t, err) @@ -65,7 +57,7 @@ func TestProtectionEngine_ActorRegexNoMatch(t *testing.T) { func TestProtectionEngine_ActorRegexMatch(t *testing.T) { engine := NewProtectionEngine([]*Rule{ - NewRule("filtered", &alwaysMatchProtection{}, RuleActionBlock, regexp.MustCompile("^admin$"), true), + NewRule("filtered", &AlwaysMatchProtection{}, RuleActionBlock, regexp.MustCompile("^admin$"), true), }) protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{actor: "admin"}) require.NoError(t, err) @@ -77,8 +69,8 @@ func TestProtectionEngine_ActorRegexMatch(t *testing.T) { func TestProtectionEngine_FirstMatchingRuleWins(t *testing.T) { engine := NewProtectionEngine([]*Rule{ NewRule("first", &neverMatchProtection{}, RuleActionBlock, nil, true), - NewRule("second", &alwaysMatchProtection{}, RuleActionLog, nil, true), - NewRule("third", &alwaysMatchProtection{}, RuleActionBlock, nil, true), + NewRule("second", &AlwaysMatchProtection{}, RuleActionLog, nil, true), + NewRule("third", &AlwaysMatchProtection{}, RuleActionBlock, nil, true), }) protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{}) require.NoError(t, err) @@ -98,7 +90,7 @@ func TestProtectionEngine_RunError(t *testing.T) { func TestProtectionEngine_UpdateRules(t *testing.T) { engine := NewProtectionEngine([]*Rule{ - NewRule("block-all", &alwaysMatchProtection{}, RuleActionBlock, nil, true), + NewRule("block-all", &AlwaysMatchProtection{}, RuleActionBlock, nil, true), }) protectionResult, err := engine.Evaluate(context.Background(), thanosQueryReq{})