-
Notifications
You must be signed in to change notification settings - Fork 50
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
Conversation
825a2cd
to
9f78cd5
Compare
9f78cd5
to
cc80c05
Compare
DisableResolveMessage: fc.Config.DisableResolveMessage, | ||
Settings: fc.Config.Settings, | ||
SecureSettings: nil, | ||
}, |
There was a problem hiding this comment.
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 | ||
} |
There was a problem hiding this comment.
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 | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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.
notify/receivers.go
Outdated
@@ -292,3 +316,189 @@ func ProcessNotifierError(config *GrafanaReceiver, err error) error { | |||
|
|||
return err | |||
} | |||
|
|||
// GrafanaReceiverTyped represents a parsed and validated APIReceiver | |||
type GrafanaReceiverTyped struct { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"` |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
notify/receivers.go
Outdated
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) | ||
} |
There was a problem hiding this comment.
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
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 |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
}
notify/receivers.go
Outdated
} | ||
|
||
func decodeSecretsFromBase64(secrets map[string]string) (map[string][]byte, error) { | ||
// secure settings are already encrypted at this point |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// secure settings are already encrypted at this point | |
// Secure settings are already encrypted at this point. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a copy from another place as is. I did not add anything here
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()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
notify/receivers.go
Outdated
@@ -292,3 +316,189 @@ func ProcessNotifierError(config *GrafanaReceiver, err error) error { | |||
|
|||
return err | |||
} | |||
|
|||
// GrafanaReceiverTyped represents a parsed and validated APIReceiver | |||
type GrafanaReceiverTyped struct { |
There was a problem hiding this comment.
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?
1f3e279
to
85dbe42
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
notify/receivers.go
Outdated
Settings T | ||
} | ||
|
||
// BuildReceiverConfiguration parses, decrypts and validates the APIReceiver. GrafanaReceiverConfig that contains configurations of all notifiers configurations for this receiver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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. |
notify/receivers.go
Outdated
return result, nil | ||
} | ||
|
||
// parseNotifier parses receivers and populates corresponding field in GrafanaReceiverConfig. Returns error if configuration cannot be parsed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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. |
receivers/alertmanager/testing.go
Outdated
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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. |
There was a problem hiding this comment.
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.
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