Skip to content

refactor(config): split validation into per-notifier Validate methods - #5395

Open
ayushi-work wants to merge 3 commits into
prometheus:mainfrom
ayushi-work:split-notifier-validation
Open

refactor(config): split validation into per-notifier Validate methods#5395
ayushi-work wants to merge 3 commits into
prometheus:mainfrom
ayushi-work:split-notifier-validation

Conversation

@ayushi-work

@ayushi-work ayushi-work commented Jul 17, 2026

Copy link
Copy Markdown

Extract validation logic from UnmarshalYAML methods into standalone Validate() error methods for every notifier config type. Adds a Validator interface in config/common.

This is a prerequisite for #4989 — once validation lives in separate methods, config loading can call Validate() on each notifier independently and collect all errors with errors.Join instead of failing on the first error.

  • 11 files, +123/−20 lines
  • All 22 notifier config types now have Validate() error
  • Data mutation (defaults, normalization) stays in UnmarshalYAML
  • Pure validation checks move to Validate()

Closes #4992

Pull Request Checklist

  • I have signed-off my commits

@ayushi-work
ayushi-work requested a review from a team as a code owner July 17, 2026 12:55
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Notifier configuration validation is split into dedicated Validate() methods. YAML unmarshalling retains defaulting and normalization, then invokes validation across shared and package-specific notifier configurations.

Changes

Notifier validation centralization

Layer / File(s) Summary
Validation contract
config/common/notifierconfig.go
Exports a Validator interface with a Validate() error method.
Central notifier validation
config/notifiers.go
Moves Webex, Email, Slack, Wechat, VictorOps, Pushover, SNS, and Rocketchat checks into dedicated validation methods while preserving unmarshalling defaults and normalization.
Package notifier validation
notify/*/config.go
Adds or invokes validation for Discord, Incidentio, Jira, Mattermost, Microsoft Teams, OpsGenie, PagerDuty, Telegram, and Webhook configurations during YAML decoding.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moving notifier validation into per-notifier Validate methods.
Description check ✅ Passed The description follows the template well and includes summary, issue linkage, and checklist items, with only the release-notes block left blank.
Linked Issues check ✅ Passed The PR satisfies #4992 by extracting notifier validation into standalone per-notifier Validate methods as requested.
Out of Scope Changes check ✅ Passed The changes stay focused on notifier validation refactoring and the supporting Validator interface, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
notify/mattermost/config.go (1)

141-147: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Consider adding a requirement check for the Mattermost webhook URL.

Unlike other webhook-based notifiers (such as Discord and MSTeams), this configuration does not verify that at least one of webhook_url or webhook_url_file is provided. If there is no global fallback for Mattermost endpoints, omitting both could lead to errors during message delivery rather than being caught at startup.

🛡️ Proposed fix
 func (c *MattermostConfig) Validate() error {
+	if c.WebhookURL == nil && len(c.WebhookURLFile) == 0 {
+		return errors.New("one of webhook_url or webhook_url_file must be configured")
+	}
+
 	if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {
 		return errors.New("at most one of webhook_url & webhook_url_file must be configured")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notify/mattermost/config.go` around lines 141 - 147, Update
MattermostConfig.Validate to require at least one of WebhookURL or
WebhookURLFile, while preserving the existing mutual-exclusion validation when
both are configured. Return a clear configuration error when neither endpoint is
provided.
config/notifiers.go (2)

491-493: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Prevent potential panic if APIURL is nil.

If UpdateMessage is true and APIURL is nil, calling .String() could cause a nil-pointer dereference panic (depending on SecretURL's implementation). Since the validation requires the URL to be a specific Slack API endpoint, explicitly checking for nil guarantees safety.

🛡️ Proposed fix
-	if c.UpdateMessage && c.APIURL.String() != "https://slack.com/api/chat.postMessage" {
+	if c.UpdateMessage && (c.APIURL == nil || c.APIURL.String() != "https://slack.com/api/chat.postMessage") {
 		return errors.New("update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/notifiers.go` around lines 491 - 493, Update the validation condition
in the notifier configuration path around UpdateMessage so it checks APIURL for
nil before calling APIURL.String(). Treat a nil URL as invalid and return the
existing validation error, while preserving acceptance only for the required
Slack chat.postMessage endpoint.

688-693: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the logic to reject configs where all three fields are provided.

The boolean logic (A != B) != C (which corresponds to A XOR B XOR C) correctly rejects configs with 0 or 2 provided fields. However, it incorrectly evaluates to false (no error) when all three fields are provided, bypassing the mutually exclusive requirement.

Consider using an explicit count to ensure exactly one field is configured.

🐛 Proposed fix
 func (c *SNSConfig) Validate() error {
-	if (c.TargetARN == "") != (c.TopicARN == "") != (c.PhoneNumber == "") {
+	provided := 0
+	if c.TargetARN != "" {
+		provided++
+	}
+	if c.TopicARN != "" {
+		provided++
+	}
+	if c.PhoneNumber != "" {
+		provided++
+	}
+	if provided != 1 {
 		return errors.New("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config")
 	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/notifiers.go` around lines 688 - 693, Update SNSConfig.Validate to
count the non-empty values among TargetARN, TopicARN, and PhoneNumber, and
return the existing validation error unless exactly one field is configured.
Remove the chained XOR condition so configurations with all three fields, none,
or multiple fields are rejected.
notify/incidentio/config.go (1)

83-85: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix logic error when HTTPConfig is nil.

The condition c.HTTPConfig != nil prevents this validation from triggering when HTTPConfig is completely omitted (i.e., nil). This incorrectly allows a configuration with no authentication at all to pass validation. Change != to == so the requirement is properly enforced when HTTPConfig is missing.

🐛 Proposed fix
-	if (c.HTTPConfig != nil && c.HTTPConfig.Authorization == nil) && c.AlertSourceToken == "" && c.AlertSourceTokenFile == "" {
+	if (c.HTTPConfig == nil || c.HTTPConfig.Authorization == nil) && c.AlertSourceToken == "" && c.AlertSourceTokenFile == "" {
 		return errors.New("at least one of alert_source_token, alert_source_token_file or http_config.authorization must be configured")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notify/incidentio/config.go` around lines 83 - 85, Update the authentication
validation condition in the configuration validation logic to treat a nil
HTTPConfig as missing authorization, while still checking AlertSourceToken and
AlertSourceTokenFile. Change the HTTPConfig nil comparison so configurations
without any authentication are rejected with the existing error.
🧹 Nitpick comments (1)
config/notifiers.go (1)

348-369: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the "missing name or url" validation to Validate().

To fully align with the PR's objective of separating data mutation from pure validation, consider moving the check for missing name or url into the new Validate() method.

♻️ Proposed refactor
 	if c.URL != "" {
 		// Clear all message action fields.
 		c.Name = ""
 		c.Value = ""
 		c.ConfirmField = nil
 	} else if c.Name != "" {
 		c.URL = ""
-	} else {
-		return errors.New("missing name or url in Slack action configuration")
 	}
 	return c.Validate()
 }
 
 func (c *SlackAction) Validate() error {
+	if c.URL == "" && c.Name == "" {
+		return errors.New("missing name or url in Slack action configuration")
+	}
 	if c.Type == "" {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/notifiers.go` around lines 348 - 369, Move the missing name-or-URL
check from the mutation logic into SlackAction.Validate, keeping the existing
error message. Ensure the mutating method only clears conflicting fields and
then delegates validation, while Validate rejects configurations where both Name
and URL are empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@config/notifiers.go`:
- Around line 491-493: Update the validation condition in the notifier
configuration path around UpdateMessage so it checks APIURL for nil before
calling APIURL.String(). Treat a nil URL as invalid and return the existing
validation error, while preserving acceptance only for the required Slack
chat.postMessage endpoint.
- Around line 688-693: Update SNSConfig.Validate to count the non-empty values
among TargetARN, TopicARN, and PhoneNumber, and return the existing validation
error unless exactly one field is configured. Remove the chained XOR condition
so configurations with all three fields, none, or multiple fields are rejected.

In `@notify/incidentio/config.go`:
- Around line 83-85: Update the authentication validation condition in the
configuration validation logic to treat a nil HTTPConfig as missing
authorization, while still checking AlertSourceToken and AlertSourceTokenFile.
Change the HTTPConfig nil comparison so configurations without any
authentication are rejected with the existing error.

In `@notify/mattermost/config.go`:
- Around line 141-147: Update MattermostConfig.Validate to require at least one
of WebhookURL or WebhookURLFile, while preserving the existing mutual-exclusion
validation when both are configured. Return a clear configuration error when
neither endpoint is provided.

---

Nitpick comments:
In `@config/notifiers.go`:
- Around line 348-369: Move the missing name-or-URL check from the mutation
logic into SlackAction.Validate, keeping the existing error message. Ensure the
mutating method only clears conflicting fields and then delegates validation,
while Validate rejects configurations where both Name and URL are empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0da194d9-9bae-4371-8216-3f9714fbe06b

📥 Commits

Reviewing files that changed from the base of the PR and between 9e50fad and 57d3342.

📒 Files selected for processing (11)
  • config/common/notifierconfig.go
  • config/notifiers.go
  • notify/discord/config.go
  • notify/incidentio/config.go
  • notify/jira/config.go
  • notify/mattermost/config.go
  • notify/msteams/config.go
  • notify/msteamsv2/config.go
  • notify/opsgenie/config.go
  • notify/telegram/config.go
  • notify/webhook/config.go

@TheMeier

Copy link
Copy Markdown
Contributor

@ayushi-work that PR is maybe a bit pre mature, the proposal I made in the issue was just an idea. I am not sure it is a good one. And if this is the way to go the interface would probably be less sepcific eg type NotifierConfg interface. And further the validation implementations would then go into notify//config.go or so

@ayushi-work

Copy link
Copy Markdown
Author

@TheMeier Hi! I kept the interface small because Validate() is really the only behavior I need for #4989, and it felt cleaner to keep validation separate from other concerns. That said, I'm not attached to the interface itself, if we'd rather see a different interface, I'm happy to change it. For the file layout, the external notifier Validate() methods are already in notify//config.go. The ones in config/notifiers.go remained there because those notifier types have always lived in the config package, and moving them felt like a separate cleanup from what I was trying to do here. This is mostly just the mechanical extraction to make the follow-up work in #4989 easier.

@TheMeier

Copy link
Copy Markdown
Contributor

oh sorry I overlooked that this was already done in notify/$NAME/config.go where available. i am currently moving ther configs out of notifiy/config.go one by one. so actually it looks quite ok to me

@ayushi-work

Copy link
Copy Markdown
Author

oh sorry I overlooked that this was already done in notify/$NAME/config.go where available. i am currently moving ther configs out of notifiy/config.go one by one. so actually it looks quite ok to me

alright great! let me know if anything needs to be changed!

@ayushi-work

Copy link
Copy Markdown
Author

Hi! just bumping this up!

@Spaceman1701 Spaceman1701 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quite like this change - decoupling unmarshaling from validation is a good idea.

Comment thread config/notifiers.go
for h, v := range c.Headers {
normalized := textproto.CanonicalMIMEHeaderKey(h)
if _, ok := normalizedHeaders[normalized]; ok {
return fmt.Errorf("duplicate header %q in email config", normalized)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's obvious right now, but would you mind adding a comment that explains why this validation is not in Validate? For a future someone who isn't familiar with the history of this code, it might be rather surprising.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, added a comment explaining why the duplicate detection stays in UnmarshalYAML.

…halYAML

Signed-off-by: ayushi-work <ayushi.work007@gmail.com>
Signed-off-by: ayushi-work <ayushi.work007@gmail.com>
@ayushi-work
ayushi-work force-pushed the split-notifier-validation branch from ab02362 to 2710fb9 Compare July 29, 2026 05:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
config/notifiers.go (2)

254-279: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Move the name-or-URL requirement into Validate.

Line 267 leaves a pure validation rule in UnmarshalYAML; SlackAction{Type: "...", Text: "..."}.Validate() currently succeeds without either name or url. Keep field clearing in unmarshalling, but enforce this invariant in Validate.

Proposed fix
-	} else {
-		return errors.New("missing name or url in Slack action configuration")
 	}
 	return c.Validate()
 }
 
 func (c *SlackAction) Validate() error {
+	if c.Name == "" && c.URL == "" {
+		return errors.New("missing name or url in Slack action configuration")
+	}
 	if c.Type == "" {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/notifiers.go` around lines 254 - 279, Move the missing name-or-URL
validation from SlackAction.UnmarshalYAML into SlackAction.Validate so direct
Validate calls enforce the same invariant. Keep UnmarshalYAML responsible for
unmarshalling and clearing conflicting fields, while Validate rejects
configurations where both URL and Name are empty alongside its existing Type and
Text checks.

391-404: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject nil api_url before validating update_message.

SlackConfig.Validate() calls c.APIURL.String() after UpdateMessage, but APIURL is a zero *commoncfg.SecretURL by default. That value has no String() method, so the call panics instead of returning a config error. Check c.APIURL != nil first; the subsequent URL comparison can then return the nil/unknown URL as the invalid value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/notifiers.go` around lines 391 - 404, Update SlackConfig.Validate so
the update_message check validates c.APIURL is non-nil before calling
c.APIURL.String(). When UpdateMessage is enabled with a nil APIURL, return the
configuration error; only perform the URL comparison after the nil check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/notifiers.go`:
- Around line 599-602: Update SNSConfig.Validate to explicitly count the
non-empty values among TargetARN, TopicARN, and PhoneNumber, and return the
existing validation error unless exactly one target identifier is configured.
Remove the chained != expression while preserving the current error message.

---

Outside diff comments:
In `@config/notifiers.go`:
- Around line 254-279: Move the missing name-or-URL validation from
SlackAction.UnmarshalYAML into SlackAction.Validate so direct Validate calls
enforce the same invariant. Keep UnmarshalYAML responsible for unmarshalling and
clearing conflicting fields, while Validate rejects configurations where both
URL and Name are empty alongside its existing Type and Text checks.
- Around line 391-404: Update SlackConfig.Validate so the update_message check
validates c.APIURL is non-nil before calling c.APIURL.String(). When
UpdateMessage is enabled with a nil APIURL, return the configuration error; only
perform the URL comparison after the nil check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cc6a83ed-92d9-4454-9907-cce564212d2c

📥 Commits

Reviewing files that changed from the base of the PR and between ab02362 and 2710fb9.

📒 Files selected for processing (12)
  • config/common/notifierconfig.go
  • config/notifiers.go
  • notify/discord/config.go
  • notify/incidentio/config.go
  • notify/jira/config.go
  • notify/mattermost/config.go
  • notify/msteams/config.go
  • notify/msteamsv2/config.go
  • notify/opsgenie/config.go
  • notify/pagerduty/config.go
  • notify/telegram/config.go
  • notify/webhook/config.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • config/common/notifierconfig.go
  • notify/opsgenie/config.go
  • notify/mattermost/config.go
  • notify/msteams/config.go
  • notify/incidentio/config.go
  • notify/webhook/config.go
  • notify/telegram/config.go

Comment thread config/notifiers.go
Comment on lines +599 to 602
func (c *SNSConfig) Validate() error {
if (c.TargetARN == "") != (c.TopicARN == "") != (c.PhoneNumber == "") {
return errors.New("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Count configured SNS targets explicitly.

Chained != evaluates left-to-right, so all three target fields configured passes validation. Count non-empty target identifiers and require the count to equal one.

Proposed fix
 func (c *SNSConfig) Validate() error {
-	if (c.TargetARN == "") != (c.TopicARN == "") != (c.PhoneNumber == "") {
+	targets := 0
+	for _, target := range []string{c.TargetARN, c.TopicARN, c.PhoneNumber} {
+		if target != "" {
+			targets++
+		}
+	}
+	if targets != 1 {
 		return errors.New("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config")
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (c *SNSConfig) Validate() error {
if (c.TargetARN == "") != (c.TopicARN == "") != (c.PhoneNumber == "") {
return errors.New("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config")
}
func (c *SNSConfig) Validate() error {
targets := 0
for _, target := range []string{c.TargetARN, c.TopicARN, c.PhoneNumber} {
if target != "" {
targets++
}
}
if targets != 1 {
return errors.New("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/notifiers.go` around lines 599 - 602, Update SNSConfig.Validate to
explicitly count the non-empty values among TargetARN, TopicARN, and
PhoneNumber, and return the existing validation error unless exactly one target
identifier is configured. Remove the chained != expression while preserving the
current error message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Split validation logic into a separate function for each notifier

3 participants