diff --git a/alerter/rules/store.go b/alerter/rules/store.go index 3b169ab3e..af1bfc0a8 100644 --- a/alerter/rules/store.go +++ b/alerter/rules/store.go @@ -19,8 +19,9 @@ import ( ) type StoreOpts struct { - Region string - CtrlCli client.Client + Region string + Namespace string + CtrlCli client.Client } type Store struct { @@ -108,7 +109,11 @@ func (s *Store) reloadRules() ([]*Rule, error) { defer cancel() ruleList := &alertrulev1.AlertRuleList{} - if err := s.ctrlCli.List(ctx, ruleList); err != nil { + var listOpts []client.ListOption + if s.opts.Namespace != "" { + listOpts = append(listOpts, client.InNamespace(s.opts.Namespace)) + } + if err := s.ctrlCli.List(ctx, ruleList, listOpts...); err != nil { return nil, fmt.Errorf("failed to list alert rules: %w", err) } diff --git a/alerter/rules/store_test.go b/alerter/rules/store_test.go new file mode 100644 index 000000000..ace10a032 --- /dev/null +++ b/alerter/rules/store_test.go @@ -0,0 +1,113 @@ +package rules + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + alertrulev1 "github.com/Azure/adx-mon/api/v1" +) + +func newAlertRule(name, namespace, database string) *alertrulev1.AlertRule { + return &alertrulev1.AlertRule{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "adx-mon.azure.com/v1", + Kind: "AlertRule", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: alertrulev1.AlertRuleSpec{ + Database: database, + Interval: metav1.Duration{Duration: time.Minute}, + Query: "TestTable | count", + Destination: "test-dest", + }, + } +} + +func TestStore_ReloadRules_AllNamespaces(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, alertrulev1.AddToScheme(scheme)) + + rule1 := newAlertRule("rule-a", "ns-one", "DB1") + rule2 := newAlertRule("rule-b", "ns-two", "DB2") + + cli := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(rule1, rule2). + Build() + + store := NewStore(StoreOpts{ + Region: "eastus", + CtrlCli: cli, + }) + + ctx := context.Background() + require.NoError(t, store.Open(ctx)) + defer store.Close() + + rules := store.Rules() + require.Len(t, rules, 2) +} + +func TestStore_ReloadRules_FilteredNamespace(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, alertrulev1.AddToScheme(scheme)) + + rule1 := newAlertRule("rule-a", "ns-one", "DB1") + rule2 := newAlertRule("rule-b", "ns-two", "DB2") + rule3 := newAlertRule("rule-c", "ns-one", "DB3") + + cli := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(rule1, rule2, rule3). + Build() + + store := NewStore(StoreOpts{ + Region: "eastus", + Namespace: "ns-one", + CtrlCli: cli, + }) + + ctx := context.Background() + require.NoError(t, store.Open(ctx)) + defer store.Close() + + rules := store.Rules() + require.Len(t, rules, 2) + for _, r := range rules { + require.Equal(t, "ns-one", r.Namespace) + } +} + +func TestStore_ReloadRules_FilteredNamespace_NoMatches(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, alertrulev1.AddToScheme(scheme)) + + rule1 := newAlertRule("rule-a", "ns-one", "DB1") + + cli := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(rule1). + Build() + + store := NewStore(StoreOpts{ + Region: "eastus", + Namespace: "ns-nonexistent", + CtrlCli: cli, + }) + + ctx := context.Background() + require.NoError(t, store.Open(ctx)) + defer store.Close() + + rules := store.Rules() + require.Empty(t, rules) +} diff --git a/alerter/service.go b/alerter/service.go index 333461058..d00d5d657 100644 --- a/alerter/service.go +++ b/alerter/service.go @@ -34,6 +34,10 @@ type AlerterOpts struct { // MaxNotifications is the maximum number of notifications to send per rule. MaxNotifications int + // Namespace is the Kubernetes namespace to watch for AlertRules. + // If empty, all namespaces are watched. + Namespace string + // Managed Identity options MSIID string MSIResource string @@ -72,8 +76,9 @@ type KustoClient interface { func NewService(opts *AlerterOpts) (*Alerter, error) { ruleStore := rules.NewStore(rules.StoreOpts{ - Region: opts.Region, - CtrlCli: opts.CtrlCli, + Region: opts.Region, + Namespace: opts.Namespace, + CtrlCli: opts.CtrlCli, }) l2m := &Alerter{ diff --git a/cmd/alerter/main.go b/cmd/alerter/main.go index f91b40d42..91ba92637 100644 --- a/cmd/alerter/main.go +++ b/cmd/alerter/main.go @@ -36,6 +36,7 @@ func main() { &cli.StringFlag{Name: "alerter-address", Usage: "Address of the alert notification service"}, &cli.IntFlag{Name: "concurrency", Value: 10, Usage: "Number of concurrent queries to run"}, &cli.IntFlag{Name: "max-notifications", Value: 25, Usage: "Maximum number of notifications to send per rule"}, + &cli.StringFlag{Name: "namespace", Usage: "Kubernetes namespace to watch for AlertRules. If not set, all namespaces are watched"}, &cli.StringSliceFlag{Name: "tag", Usage: "Tag in the format of = that applies to execution context"}, }, Action: realMain, @@ -102,6 +103,7 @@ func realMain(ctx *cli.Context) error { AlertAddr: ctx.String("alerter-address"), Concurrency: ctx.Int("concurrency"), MaxNotifications: ctx.Int("max-notifications"), + Namespace: ctx.String("namespace"), MSIID: ctx.String("auth-msi-id"), KustoToken: ctx.String("auth-token"), Tags: tags,