Alerting: Add TLS, QoS and retain options to the MQTT receiver (#92331)

pull/93378/head
Alexander Akhmetov 9 months ago committed by GitHub
parent b52e6ba552
commit e59ea00518
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .github/CODEOWNERS
  2. 31
      devenv/docker/blocks/mqtt/README.md
  3. 9
      devenv/docker/blocks/mqtt/build/Dockerfile
  4. 18
      devenv/docker/blocks/mqtt/build/gen_certs.sh
  5. 7
      devenv/docker/blocks/mqtt/build/san.cnf
  6. 12
      devenv/docker/blocks/mqtt/docker-compose.yaml
  7. 40
      devenv/docker/blocks/mqtt/nanomq.conf
  8. 7
      devenv/docker/blocks/mqtt/nanomq_acl.conf
  9. 2
      devenv/docker/blocks/mqtt/nanomq_pwd.conf
  10. 23
      docs/sources/administration/provisioning/index.md
  11. 14
      docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md
  12. 6
      go.mod
  13. 12
      go.sum
  14. 4
      go.work.sum
  15. 6
      pkg/services/ngalert/api/compat_contact_points.go
  16. 26
      pkg/services/ngalert/api/compat_contact_points_test.go
  17. 25
      pkg/services/ngalert/api/tooling/definitions/contact_points.go
  18. 69
      pkg/services/ngalert/notifier/channels_config/available_channels.go
  19. 38
      public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx

@ -199,6 +199,7 @@
/devenv/docker/blocks/mariadb/ @grafana/oss-big-tent
/devenv/docker/blocks/memcached/ @grafana/grafana-backend-group
/devenv/docker/blocks/mimir_backend/ @grafana/alerting-backend
/devenv/docker/blocks/mqtt/ @grafana/alerting-backend
/devenv/docker/blocks/mssql/ @grafana/partner-datasources
/devenv/docker/blocks/mssql_arm64/ @grafana/partner-datasources
/devenv/docker/blocks/mssql_tests/ @grafana/partner-datasources

@ -0,0 +1,31 @@
# NanoMQ MQTT broker
Starts a [NanoMQ MQTT broker](https://nanomq.io/docs/en/latest/).
## Authentication
The broker is configured to use a simple username/password authentication.
See [./nanomq_pwd.conf](./nanomq_pwd.conf) for the default credentials.
## TLS Certificates
If you want to configure an MQTT contact point in Grafana Alerting with TLS, you need to provide a certificate and key.
You can find them in `/etc/certs` directory in the container:
``` shell
docker exec devenv-mqtt-1 ls /etc/certs/
```
### CA Certificate
``` shell
docker exec devenv-mqtt-1 cat /etc/certs/ca.pem
```
### Client certificates
``` shell
docker exec devenv-mqtt-1 cat /etc/certs/client.pem
docker exec devenv-mqtt-1 cat /etc/certs/client.key
```

@ -0,0 +1,9 @@
FROM emqx/nanomq:0.21.11-full
RUN apt-get update && apt-get install -y \
openssl \
&& rm -rf /var/lib/apt/lists/*
COPY ./san.cnf /etc/certs/san.cnf
COPY ./gen_certs.sh /etc/certs/gen_certs.sh
RUN /etc/certs/gen_certs.sh

@ -0,0 +1,18 @@
#!/bin/bash
DAYS_VALID=3650
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Create CA certificate
openssl genpkey -algorithm RSA -out "$SCRIPT_DIR/ca.key"
openssl req -new -x509 -days $DAYS_VALID -key "$SCRIPT_DIR/ca.key" -out "$SCRIPT_DIR/ca.pem" -subj "/CN=My CA"
# Create server certificate
openssl genpkey -algorithm RSA -out "$SCRIPT_DIR/server.key"
openssl req -new -key "$SCRIPT_DIR/server.key" -out "$SCRIPT_DIR/server.csr" -subj "/CN=localhost"
openssl x509 -req -days $DAYS_VALID -in "$SCRIPT_DIR/server.csr" -CA "$SCRIPT_DIR/ca.pem" -CAkey "$SCRIPT_DIR/ca.key" -CAcreateserial -out "$SCRIPT_DIR/server.pem" -extfile "$SCRIPT_DIR/san.cnf" -extensions v3_req
# Create client key and certificate
openssl genpkey -algorithm RSA -out "$SCRIPT_DIR/client.key"
openssl req -new -key "$SCRIPT_DIR/client.key" -out "$SCRIPT_DIR/client.csr" -subj "/CN=Client"
openssl x509 -req -days $DAYS_VALID -in "$SCRIPT_DIR/client.csr" -CA "$SCRIPT_DIR/ca.pem" -CAkey "$SCRIPT_DIR/ca.key" -CAcreateserial -out "$SCRIPT_DIR/client.pem" -extfile "$SCRIPT_DIR/san.cnf" -extensions v3_req

@ -0,0 +1,7 @@
[ v3_req ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = ::1

@ -0,0 +1,12 @@
mqtt:
build:
context: docker/blocks/mqtt/build
ports:
- "127.0.0.1:1883:1883" # MQTT
- "127.0.0.1:8883:8883" # MQTT over TLS
- "127.0.0.1:8083:8083" # MQTT over WS
- "127.0.0.1:8443:8443" # MQTT over WSS
volumes:
- ${PWD}/docker/blocks/mqtt/nanomq.conf:/etc/nanomq.conf
- ${PWD}/docker/blocks/mqtt/nanomq_pwd.conf:/etc/nanomq_pwd.conf
- ${PWD}/docker/blocks/mqtt/nanomq_acl.conf:/etc/nanomq_acl.conf

@ -0,0 +1,40 @@
log {
to=console
level=info
}
listeners.tcp {
bind = "0.0.0.0:1883"
}
listeners.ssl {
bind = "0.0.0.0:8883"
keyfile = "/etc/certs/server.key"
certfile = "/etc/certs/server.pem"
cacertfile = "/etc/certs/ca.pem"
# Change these settings to true if you want to deny
# access for clients that don't have a certificate.
verify_peer = false
fail_if_no_peer_cert = false
}
listeners.ws {
bind = "0.0.0.0:8083"
}
listeners.wss {
bind = "0.0.0.0:8443"
}
auth {
allow_anonymous = false
no_match = deny
deny_action = disconnect
password = {include "/etc/nanomq_pwd.conf"}
acl = {
include "/etc/nanomq_acl.conf"
}
}

@ -0,0 +1,7 @@
rules = [
{"permit": "allow", "username": "grafana", "action": "subscribe", "topics": ["#"]}
{"permit": "allow", "username": "grafana", "action": "publish", "topics": ["#"]}
{"permit": "allow", "username": "admin", "action": "subscribe", "topics": ["#"]}
{"permit": "allow", "username": "admin", "action": "publish", "topics": ["#"]}
{"permit": "deny"}
]

@ -0,0 +1,2 @@
admin:admin
grafana:grafana

@ -506,15 +506,26 @@ The following sections detail the supported settings and secure settings for eac
#### Alert notification `MQTT`
| Name | Secure setting |
| ------------- | -------------- |
| brokerUrl | |
| clientId | |
| topic | |
| messageFormat | |
| username | |
| password | yes |
| retain | |
| qos | |
| tlsConfig | |
##### TLS config
| Name | Secure setting |
| ------------------ | -------------- |
| brokerUrl | |
| clientId | |
| topic | |
| messageFormat |
| username | |
| password | yes |
| insecureSkipVerify | |
| clientCertificate | yes |
| clientKey | yes |
| caCertificate | yes |
#### Alert notification `pagerduty`

@ -362,8 +362,20 @@ settings:
username: grafana
# <string>
password: password1
# <string>
qos: 0
# <bool>
insecureSkipVerify: false
retain: false
# <map>
tlsConfig:
# <bool>
insecureSkipVerify: false
# <string>
clientCertificate: certificate in PEM format
# <string>
clientKey: key in PEM format
# <string>
caCertificate: CA certificate in PEM format
```
{{< /collapse >}}

@ -73,9 +73,9 @@ require (
github.com/googleapis/gax-go/v2 v2.13.0 // @grafana/grafana-backend-group
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20240829185616-8454ac21d7e5 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20240906122029-0100695765b9 // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20240903121118-16441568af1e // @grafana/identity-access-team
github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team
github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad
github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics

@ -2255,12 +2255,12 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20240829185616-8454ac21d7e5 h1:gQprHfu5/GT/mpPuRm3QVL+7+0j1QsvKJuPIzQAsezM=
github.com/grafana/alerting v0.0.0-20240829185616-8454ac21d7e5/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4=
github.com/grafana/authlib v0.0.0-20240906122029-0100695765b9 h1:e+kFqd2sECBhbxOV1NoVxsudLygNQuu9bO+7FjNTkXo=
github.com/grafana/authlib v0.0.0-20240906122029-0100695765b9/go.mod h1:PFzXbCrn0GIpN4KwT6NP1l5Z1CPLfmKHnYx8rZzQcyY=
github.com/grafana/authlib/claims v0.0.0-20240903121118-16441568af1e h1:ng5SopWamGS0MHaCj2e5huWYxAfMeCrj1l/dbJnfiow=
github.com/grafana/authlib/claims v0.0.0-20240903121118-16441568af1e/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10 h1:oDbLKM34O+JUF9EQFS+9aYhdYoeNfUpXqNjFCLIxwF4=
github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4=
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 h1:nT4UY61s2flsiLkU2jDqtqFhOLwqh355+8ZhnavKoMQ=
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935/go.mod h1:ER7bMzNNWTN/5Zl3pwqfgS6XEhcanjrvL7lOp8Ow6oc=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=

@ -559,12 +559,14 @@ github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18 h1:3cQ+d+fkNL2EqpARaBVG34KlVz7flDujYfDx3njvdh8=
github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4=
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE=
github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc=
github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26/go.mod h1:Pn9nfzCk7nV0mvNgwusgCjCROZP6nm4GpwTnmEhLT24=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=

@ -401,6 +401,12 @@ func (c contactPointsExtension) UpdateStructDescriptor(structDescriptor *jsonite
desc.Decoder = codec
desc.Encoder = codec
}
if structDescriptor.Type == reflect2.TypeOf(definitions.MqttIntegration{}) {
codec := &numberAsStringCodec{ignoreError: true}
desc := structDescriptor.GetField("QoS")
desc.Decoder = codec
desc.Encoder = codec
}
}
type emailAddressCodec struct{}

@ -184,4 +184,30 @@ func TestContactPointFromContactPointExports(t *testing.T) {
require.Nil(t, result.OnCall[1].MaxAlerts)
require.Nil(t, result.OnCall[2].MaxAlerts)
})
t.Run("mqtt with optional numbers as string", func(t *testing.T) {
export := definitions.ContactPointExport{
Name: "test",
Receivers: []definitions.ReceiverExport{
{
Type: "mqtt",
Settings: definitions.RawMessage(`{ "qos" : "112" }`),
},
{
Type: "mqtt",
Settings: definitions.RawMessage(`{ "qos" : "test" }`),
},
{
Type: "mqtt",
Settings: definitions.RawMessage(`{ "qos" : null }`),
},
},
}
result, err := ContactPointFromContactPointExport(export)
require.NoError(t, err)
require.Len(t, result.Mqtt, 3)
require.Equal(t, int64(112), *result.Mqtt[0].QoS)
require.Nil(t, result.Mqtt[1].QoS)
require.Nil(t, result.Mqtt[2].QoS)
})
}

@ -85,17 +85,26 @@ type LineIntegration struct {
Description *string `json:"description,omitempty" yaml:"description,omitempty" hcl:"description"`
}
type TLSConfig struct {
InsecureSkipVerify *bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" hcl:"insecure_skip_verify"`
TLSCACertificate *Secret `json:"caCertificate,omitempty" yaml:"caCertificate,omitempty" hcl:"ca_certificate"`
TLSClientCertificate *Secret `json:"clientCertificate,omitempty" yaml:"clientCertificate,omitempty" hcl:"client_certificate"`
TLSClientKey *Secret `json:"clientKey,omitempty" yaml:"clientKey,omitempty" hcl:"client_key"`
}
type MqttIntegration struct {
DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"`
BrokerURL *string `json:"brokerUrl,omitempty" yaml:"brokerUrl,omitempty" hcl:"broker_url"`
ClientID *string `json:"clientId,omitempty" yaml:"clientId,omitempty" hcl:"client_id"`
Topic *string `json:"topic,omitempty" yaml:"topic,omitempty" hcl:"topic"`
Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"`
MessageFormat *string `json:"messageFormat,omitempty" yaml:"messageFormat,omitempty" hcl:"message_format"`
Username *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"username"`
Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"password"`
InsecureSkipVerify *bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" hcl:"insecure_skip_verify"`
BrokerURL *string `json:"brokerUrl,omitempty" yaml:"brokerUrl,omitempty" hcl:"broker_url"`
ClientID *string `json:"clientId,omitempty" yaml:"clientId,omitempty" hcl:"client_id"`
Topic *string `json:"topic,omitempty" yaml:"topic,omitempty" hcl:"topic"`
Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"`
MessageFormat *string `json:"messageFormat,omitempty" yaml:"messageFormat,omitempty" hcl:"message_format"`
Username *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"username"`
Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"password"`
QoS *int64 `json:"qos,omitempty" yaml:"qos,omitempty" hcl:"qos"`
Retain *bool `json:"retain,omitempty" yaml:"retain,omitempty" hcl:"retain"`
TLSConfig *TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty" hcl:"tls_config,block"`
}
type OnCallIntegration struct {

@ -1325,12 +1325,75 @@ func GetAvailableNotifiers() []*NotifierPlugin {
Secure: true,
},
{
Label: "Disable certificate verification",
Label: "QoS",
Element: ElementTypeSelect,
SelectOptions: []SelectOption{
{
Value: "0",
Label: "At most once (0)",
},
{
Value: "1",
Label: "At least once (1)",
},
{
Value: "2",
Label: "Exactly once (2)",
},
},
Description: "The quality of service to use when sending the message.",
PropertyName: "qos",
Required: false,
},
{
Label: "Retain",
Description: "If set to true, the message will be retained by the broker.",
Element: ElementTypeCheckbox,
Description: "Do not verify the broker's certificate chain and host name.",
PropertyName: "insecureSkipVerify",
PropertyName: "retain",
Required: false,
},
{
Label: "TLS",
PropertyName: "tlsConfig",
Description: "TLS configuration options",
Element: ElementTypeSubform,
SubformOptions: []NotifierOption{
{
Label: "Disable certificate verification",
Element: ElementTypeCheckbox,
Description: "Do not verify the broker's certificate chain and host name.",
PropertyName: "insecureSkipVerify",
Required: false,
},
{
Label: "CA Certificate",
Element: ElementTypeTextArea,
Description: "Certificate in PEM format to use when verifying the broker's certificate chain.",
InputType: InputTypeText,
PropertyName: "caCertificate",
Required: false,
Secure: true,
},
{
Label: "Client Certificate",
Element: ElementTypeTextArea,
Description: "Client certificate in PEM format to use when connecting to the broker.",
InputType: InputTypeText,
PropertyName: "clientCertificate",
Required: false,
Secure: true,
},
{
Label: "Client Key",
Element: ElementTypeTextArea,
Description: "Client key in PEM format to use when connecting to the broker.",
InputType: InputTypeText,
PropertyName: "clientKey",
Required: false,
Secure: true,
},
},
},
},
},
{

@ -4,7 +4,17 @@ import { FC, useEffect } from 'react';
import { Controller, DeepMap, FieldError, useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { Checkbox, Field, Input, RadioButtonList, SecretInput, Select, TextArea, useStyles2 } from '@grafana/ui';
import {
Checkbox,
Field,
Input,
RadioButtonList,
SecretInput,
SecretTextArea,
Select,
TextArea,
useStyles2,
} from '@grafana/ui';
import { NotificationChannelOption, NotificationChannelSecureFields } from 'app/types';
import { KeyValueMapInput } from './KeyValueMapInput';
@ -222,17 +232,21 @@ const OptionInput: FC<Props & { id: string; pathIndex?: string }> = ({
name={name}
onSelectTemplate={onSelectTemplate}
>
<TextArea
id={id}
readOnly={readOnly || useTemplates}
invalid={invalid}
placeholder={option.placeholder}
{...register(name, {
required: option.required ? 'Required' : false,
validate: (v) =>
option.validationRule !== '' ? validateOption(v, option.validationRule, option.required) : true,
})}
/>
{isEncryptedInput ? (
<SecretTextArea onReset={() => onResetSecureField?.(nestedKey)} isConfigured />
) : (
<TextArea
id={id}
readOnly={readOnly || useTemplates}
invalid={invalid}
placeholder={option.placeholder}
{...register(name, {
required: option.required ? 'Required' : false,
validate: (v) =>
option.validationRule !== '' ? validateOption(v, option.validationRule, option.required) : true,
})}
/>
)}
</WrapWithTemplateSelection>
);
case 'string_array':

Loading…
Cancel
Save