Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse notifier configs without creation of notifiers #62

Merged
merged 15 commits into from
Mar 3, 2023

Conversation

yuri-tceretian
Copy link
Contributor

@yuri-tceretian yuri-tceretian commented Feb 14, 2023

In Grafana Alertmanager configuration we have very complex API models that are usually a combination of upstream configurations and our custom fields. Those models are propagated everywhere in the code deep down to the notification-building logic where it is parsed and converted to Notifier. In order to validate the configuration in API handlers we have to access the notifiers factory and stub many fields because that factory requires many services.

For example,
https://github.com/grafana/grafana/blob/f066e8cdcd178b783d0c62959bea593d3c0c30b0/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go#L105-L128

This PR creates an intermediate layer that parses, decrypts secrets and validates settings of all notifiers in a single receiver, and returns either a valid configuration or error. A new method ValidateAPIReceiver accepts only parameters that are required for performing this task.

A new struct GrafanaReceiverTyped resembles the receiver configuration from the upstream
https://github.com/prometheus/alertmanager/blob/fe1b045c00204dc47a0f63728b6162e9d5cf9b5e/config/config.go#L883-L899 However, due to additional information that Grafana notifier hold (uid, type, name) I had to add a generic wrapper NotifierConfig

This will help make validation code in API much clear and reduce the scope of raw settings.

Based on #61

The new config struct looks like the upstream Alertmanager's

// GrafanaReceiverTyped represents a parsed and validated APIReceiver
type GrafanaReceiverTyped struct {
	Name                string
	AlertmanagerConfigs []*NotifierConfig[alertmanager.Config]
	DingdingConfigs     []*NotifierConfig[dinding.Config]
	DiscordConfigs      []*NotifierConfig[discord.Config]
	EmailConfigs        []*NotifierConfig[email.Config]
	GooglechatConfigs   []*NotifierConfig[googlechat.Config]
	KafkaConfigs        []*NotifierConfig[kafka.Config]
	LineConfigs         []*NotifierConfig[line.Config]
	OpsgenieConfigs     []*NotifierConfig[opsgenie.Config]
	PagerdutyConfigs    []*NotifierConfig[pagerduty.Config]
	PushoverConfigs     []*NotifierConfig[pushover.Config]
	SensugoConfigs      []*NotifierConfig[sensugo.Config]
	SlackConfigs        []*NotifierConfig[slack.Config]
	TeamsConfigs        []*NotifierConfig[teams.Config]
	TelegramConfigs     []*NotifierConfig[telegram.Config]
	ThreemaConfigs      []*NotifierConfig[threema.Config]
	VictoropsConfigs    []*NotifierConfig[victorops.Config]
	WebhookConfigs      []*NotifierConfig[webhook.Config]
	WecomConfigs        []*NotifierConfig[wecom.Config]
	WebexConfigs        []*NotifierConfig[webex.Config]
}

@yuri-tceretian yuri-tceretian marked this pull request as ready for review February 23, 2023 15:09
DisableResolveMessage: fc.Config.DisableResolveMessage,
Settings: fc.Config.Settings,
SecureSettings: nil,
},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

temporary, this will go away in #63

Name string
Type string
DisableResolveMessage bool
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This struct will replace NotificationChannelConfig in #63

return nil
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is a utility func to propagate errors in the case of validation failure.

@@ -292,3 +316,189 @@ func ProcessNotifierError(config *GrafanaReceiver, err error) error {

return err
}

// GrafanaReceiverTyped represents a parsed and validated APIReceiver
type GrafanaReceiverTyped struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it should be called GrafanaReceiver.. but that name is already taken. Probably it makes sense to rename that to GrafanaNotifierSettings and this to GrafanaReceiverConfiguration

Copy link
Collaborator

Choose a reason for hiding this comment

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

Remind me again how is this going to be used that will benefit from generics?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is a way to have Metadata for all notifier configs

Name string `json:"name"`
Type string `json:"type"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Settings json.RawMessage `json:"settings"`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this will actually help move forward with refactoring in Grafana because we have to do conversions back and forth for that field.

Copy link
Collaborator

@gotjosh gotjosh left a comment

Choose a reason for hiding this comment

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

Overall LGTM and I find this is a net positive, but please see my comments.

receivers/factory.go Outdated Show resolved Hide resolved
receivers/factory.go Outdated Show resolved Hide resolved
Comment on lines 355 to 374
parseConfig := func(receiver *GrafanaReceiver) error {
// secure settings are already encrypted at this point
secureSettings := make(map[string][]byte, len(receiver.SecureSettings))

if receiver.SecureSettings != nil {
for k, v := range receiver.SecureSettings {
d, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return InvalidReceiverError{
Receiver: receiver,
Err: errors.New("failed to decode secure setting"),
}
}
secureSettings[k] = d
}
}

var decryptFn receivers.DecryptFunc = func(key string, fallback string) string {
return decrypt(ctx, secureSettings, key, fallback)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit:

I'd like to move away from these nested functions, they make this part of the code really hard to read.

Why not just make this a separate function, even better why not embrace the pointer to structs here all of this a function that's part of APIReceiver. I don't really understand the relationship between these struct at all.

The code as written appears to take a functional approach - which even though is well intended I personally find it highly un-idiomatic.

For example, in this function the main actor is:

	for _, receiver := range api.Receivers {
		err := parseConfig(receiver)
		if err != nil {
			return GrafanaReceiverTyped{}, &ReceiverValidationError{
				Cfg: receiver,
				Err: err,
			}
		}
	}

Yet, I'm presented with parseConfig first which forces to me read this wall before I even begin to understand what I'm doing as part of this function.

A good example of the use of inline functions is when you care about concurrency and sending that execution to a goroutine - this is not any of that.

notify/receivers.go Outdated Show resolved Hide resolved
notify/receivers.go Outdated Show resolved Hide resolved
Comment on lines 352 to 369
result := GrafanaReceiverTyped{
Name: api.Name,
}
for _, receiver := range api.Receivers {
secureSettings, err := decodeSecretsFromBase64(receiver.SecureSettings)
if err == nil {
err = parseReceiver(&result, receiver, func(key string, fallback string) string {
return decrypt(ctx, secureSettings, key, fallback)
})
}
if err != nil {
return GrafanaReceiverTyped{}, &ReceiverValidationError{
Cfg: receiver,
Err: fmt.Errorf("failed to parse notifier %s (UID: %s): %w", receiver.Name, receiver.UID, err),
}
}
}
return result, nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I'd re-structure this function.

Generally, functions tend to do four main things: Collecting input, handling failure, doing work and delivering results.

I'd stick to that order when possible.

In addition to that, the name of ValidateAPIReceiver is highly misleading - we're way past the name of validation. I suggest BuildReceiverIntegrations.

Copy link
Collaborator

Choose a reason for hiding this comment

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

With the above, my suggestion is:

var ErrorAPIReceiverValidation = func(receiver *GrafanaReceiver, err error) error {
	return &ReceiverValidationError{
		Cfg: receiver,
		Err: fmt.Errorf("failed to parse notifier %s (UID: %s): %w", receiver.Name, receiver.UID, err),
	}
}

// ValidateAPIReceiver parses, decrypts and validates the APIReceiver. GrafanaReceiverTyped that contains configurations of all notifiers configurations for this receiver
func ValidateAPIReceiver(ctx context.Context, api *APIReceiver, decrypt receivers.GetDecryptedValueFn) (GrafanaReceiverTyped, error) {
	result := GrafanaReceiverTyped{
		Name: api.Name,
	}

	for _, receiver := range api.Receivers {
		// First, decode the secure receiver integrations settings.
		secureSettings, err := decodeSecretsFromBase64(receiver.SecureSettings)
		if err != nil {
			return GrafanaReceiverTyped{}, ErrorAPIReceiverValidation(receiver, err)
		}

		// Then, create the decryption function that's common for all integrations of this receiver.
		decryptFn := func(key, fallback string) string {
			return decrypt(ctx, secureSettings, key, fallback)
		}

		// Finally, parse the receiver configuration.
		err = parseReceiver(&result, receiver, decryptFn)
		if err != nil {
			return GrafanaReceiverTyped{}, ErrorAPIReceiverValidation(receiver, err)
		}
	}

	return result, nil
}

}

func decodeSecretsFromBase64(secrets map[string]string) (map[string][]byte, error) {
// secure settings are already encrypted at this point
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// secure settings are already encrypted at this point
// Secure settings are already encrypted at this point.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If secure settings are already encoded at this point - why do we need to have a decrypt function passed over? Should we call the function noopDecryptFn instead then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if e.Cfg.Name != "" {
name = fmt.Sprintf("%q ", e.Cfg.Name)
}
s := fmt.Sprintf("failed to validate receiver %sof type %q: %s", name, e.Cfg.Type, e.Err.Error())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
s := fmt.Sprintf("failed to validate receiver %sof type %q: %s", name, e.Cfg.Type, e.Err.Error())
s := fmt.Sprintf("failed to validate notifier %sof type %q: %s", name, e.Cfg.Type, e.Err.Error())

Copy link
Collaborator

Choose a reason for hiding this comment

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

Other errors seem to use notifier instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that is expected because this error is returned by ValidateReceiver method... not a particular notifier.

receivers/factory.go Outdated Show resolved Hide resolved
@@ -292,3 +316,189 @@ func ProcessNotifierError(config *GrafanaReceiver, err error) error {

return err
}

// GrafanaReceiverTyped represents a parsed and validated APIReceiver
type GrafanaReceiverTyped struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remind me again how is this going to be used that will benefit from generics?

Copy link
Collaborator

@gotjosh gotjosh left a comment

Choose a reason for hiding this comment

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

LGTM

Settings T
}

// BuildReceiverConfiguration parses, decrypts and validates the APIReceiver. GrafanaReceiverConfig that contains configurations of all notifiers configurations for this receiver
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// BuildReceiverConfiguration parses, decrypts and validates the APIReceiver. GrafanaReceiverConfig that contains configurations of all notifiers configurations for this receiver
// BuildReceiverConfiguration parses, decrypts and validates the APIReceiver.

return result, nil
}

// parseNotifier parses receivers and populates corresponding field in GrafanaReceiverConfig. Returns error if configuration cannot be parsed
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// parseNotifier parses receivers and populates corresponding field in GrafanaReceiverConfig. Returns error if configuration cannot be parsed
// parseNotifier parses receivers and populates the corresponding field in GrafanaReceiverConfig. Returns an error if the configuration cannot be parsed.

@@ -0,0 +1,13 @@
package alertmanager

// FullValidConfigForTesting a string representation of JSON object that contains all fields supported by the notifier Config. Can be used without secrets
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// FullValidConfigForTesting a string representation of JSON object that contains all fields supported by the notifier Config. Can be used without secrets
// FullValidConfigForTesting is a string representation of JSON object that contains all fields supported by the notifier Config. It can be used without secrets.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The same applies to every other instance of this comment.

"basicAuthPassword": "admin"
}`

// FullValidSecretsForTesting is a string representation of JSON object that contains all fields that can be overridden from secrets
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// FullValidSecretsForTesting is a string representation of JSON object that contains all fields that can be overridden from secrets
// FullValidSecretsForTesting is a string representation of a JSON object that contains all fields that can be overridden from secrets.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The same applies to every other instance of this comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request kata:am_unify
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

None yet

2 participants