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

Alerting: Add MQTT notifications receiver #1746

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/acc-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,26 @@ jobs:
fail-fast: false # Let all versions run, even if one fails
matrix:
# OSS tests, run on all versions
version: ['11.0.0', '10.4.3', '9.5.18']
version: ['11.5.0', '10.4.3', '9.5.18']
type: ['oss']
subset: ['basic', 'other', 'long']
include:
- version: '11.0.0'
- version: '11.5.0'
type: 'oss'
subset: examples
# TLS proxy tests, run only on latest version
- version: '11.0.0'
- version: '11.5.0'
type: 'tls'
subset: 'basic'
# Sub-path tests. Runs tests on localhost:3000/grafana/
- version: '11.0.0'
- version: '11.5.0'
type: 'subpath'
subset: 'basic'
- version: '11.0.0'
- version: '11.5.0'
type: 'subpath'
subset: 'other'
# Enterprise tests
- version: '11.0.0'
- version: '11.5.0'
type: 'enterprise'
subset: 'enterprise'
- version: '10.4.3'
Expand All @@ -93,7 +93,7 @@ jobs:
type: 'enterprise'
subset: 'enterprise'
# Generate tests
- version: '11.0.0'
- version: '11.5.0'
type: 'enterprise'
subset: 'generate'
- version: '10.4.3'
Expand Down
2 changes: 1 addition & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GRAFANA_VERSION ?= 11.0.0
GRAFANA_VERSION ?= 11.5.0
DOCKER_COMPOSE_ARGS ?= --force-recreate --detach --remove-orphans --wait --renew-anon-volumes

testacc:
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ the in-screen instructions, of following [this guide](https://grafana.com/docs/g

#### Obtaining Cloud Provider API hostname

Having created the token, we can find the correct Cloud Provider API hostname by running the following script, that requires `curl` and [`jq`](https://jqlang.github.io/jq/) installed:
Having created the token, we can find the correct Cloud Provider API hostname by running the following script, that requires `curl` and [`jq`](https://jqlang.org/) installed:
Copy link
Author

Choose a reason for hiding this comment

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

make linkcheck was complaining:

Start checking at 2025-02-02 14:31:59+000

URL        `https://jqlang.github.io/jq/'
Parent URL file:///home/runner/work/terraform-provider-grafana/terraform-provider-grafana/docs/index.md, line 289, col 143
Real URL   https://jqlang.org/
Check time 0.455 seconds
Size       3KB
Warning    [http-redirected] Redirected to `https://jqlang.org/'
           status: 301 Moved Permanently.
Result     Valid: 200 OK


```bash
curl -sH "Authorization: Bearer <Access Token from previous step>" "https://grafana.com/api/instances" | \
Expand Down Expand Up @@ -414,7 +414,7 @@ the in-screen instructions, of following [this guide](https://grafana.com/docs/g

#### Obtaining Connections API hostname

Having created the token, we can find the correct Connections API hostname by running the following script, that requires `curl` and [`jq`](https://jqlang.github.io/jq/) installed:
Having created the token, we can find the correct Connections API hostname by running the following script, that requires `curl` and [`jq`](https://jqlang.org/) installed:

```bash
curl -sH "Authorization: Bearer <Access Token from previous step>" "https://grafana.com/api/instances" | \
Expand Down
37 changes: 37 additions & 0 deletions docs/resources/contact_point.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ resource "grafana_contact_point" "my_contact_point" {
- `googlechat` (Block Set) A contact point that sends notifications to Google Chat. (see [below for nested schema](#nestedblock--googlechat))
- `kafka` (Block Set) A contact point that publishes notifications to Apache Kafka topics. (see [below for nested schema](#nestedblock--kafka))
- `line` (Block Set) A contact point that sends notifications to LINE.me. (see [below for nested schema](#nestedblock--line))
- `mqtt` (Block Set) A contact point that sends notifications to an MQTT broker. (see [below for nested schema](#nestedblock--mqtt))
- `oncall` (Block Set) A contact point that sends notifications to Grafana On-Call. (see [below for nested schema](#nestedblock--oncall))
- `opsgenie` (Block Set) A contact point that sends notifications to OpsGenie. (see [below for nested schema](#nestedblock--opsgenie))
- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
Expand Down Expand Up @@ -212,6 +213,42 @@ Read-Only:
- `uid` (String) The UID of the contact point.


<a id="nestedblock--mqtt"></a>
### Nested Schema for `mqtt`

Required:

- `broker_url` (String) The URL of the MQTT broker.
- `topic` (String) The topic to publish messages to.

Optional:

- `client_id` (String) The client ID to use when connecting to the broker.
- `disable_resolve_message` (Boolean) Whether to disable sending resolve messages. Defaults to `false`.
- `message_format` (String) The format of the message to send. Supported values are `json` and `text`. Defaults to `text`.
- `password` (String, Sensitive) The password to use when connecting to the broker.
- `qos` (Number) The quality of service to use when sending messages. Supported values are 0, 1, and 2. Defaults to `0`.
- `retain` (Boolean) Whether to retain messages on the broker. Defaults to `false`.
- `settings` (Map of String, Sensitive) Additional custom properties to attach to the notifier. Defaults to `map[]`.
- `tls_config` (Block Set) TLS configuration for the connection. (see [below for nested schema](#nestedblock--mqtt--tls_config))
- `username` (String) The username to use when connecting to the broker.

Read-Only:

- `uid` (String) The UID of the contact point.

<a id="nestedblock--mqtt--tls_config"></a>
### Nested Schema for `mqtt.tls_config`

Optional:

- `ca_certificate` (String, Sensitive) The CA certificate to use when verifying the server's certificate.
- `client_certificate` (String, Sensitive) The client certificate to use when connecting to the server.
- `client_key` (String, Sensitive) The client key to use when connecting to the server.
- `insecure_skip_verify` (Boolean) Whether to skip verification of the server's certificate chain and host name. Defaults to `false`.



<a id="nestedblock--oncall"></a>
### Nested Schema for `oncall`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
resource "grafana_contact_point" "receiver_types" {
name = "Receiver Types since v11.3"

# Basic MQTT configuration
mqtt {
broker_url = "tcp://localhost:1883"
topic = "grafana/alerts"
qos = 0
}

# MQTT with authentication
mqtt {
broker_url = "tcp://localhost:1883"
topic = "grafana/alerts"
client_id = "grafana-client"
username = "mqtt-user"
password = "secret123"
message_format = "json"
qos = 1
}

# MQTT with TLS
mqtt {
broker_url = "ssl://localhost:8883"
topic = "grafana/alerts"
client_id = "grafana-secure"
message_format = "json"
qos = 2
retain = true

tls_config {
insecure_skip_verify = false
ca_certificate = "ca cert"
client_certificate = "client cert"
client_key = "client key"
}
}
}
24 changes: 24 additions & 0 deletions internal/resources/grafana/resource_alerting_contact_point.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var (
googleChatNotifier{},
kafkaNotifier{},
lineNotifier{},
mqttNotifier{},
oncallNotifier{},
opsGenieNotifier{},
pagerDutyNotifier{},
Expand Down Expand Up @@ -419,6 +420,23 @@ func packNotifierStringField(gfSettings, tfSettings *map[string]interface{}, gfK
}
}

func packNotifierIntField(gfSettings, tfSettings *map[string]interface{}, gfKey, tfKey string) error {
v, err := strconv.Atoi((*gfSettings)[gfKey].(string))
if err != nil {
return err
}
(*tfSettings)[tfKey] = int64(v)
delete(*gfSettings, gfKey)
return nil
}

func packNotifierBoolField(gfSettings, tfSettings *map[string]interface{}, gfKey, tfKey string) {
if v, ok := (*gfSettings)[gfKey]; ok && v != nil {
(*tfSettings)[tfKey] = v.(bool)
delete(*gfSettings, gfKey)
}
}

func packSecureFields(tfSettings, state map[string]interface{}, secureFields []string) {
for _, tfKey := range secureFields {
if v, ok := state[tfKey]; ok && v != nil {
Expand All @@ -427,6 +445,12 @@ func packSecureFields(tfSettings, state map[string]interface{}, secureFields []s
}
}

func unpackNotifierBoolField(tfSettings, gfSettings *map[string]interface{}, tfKey, gfKey string) {
if v, ok := (*tfSettings)[tfKey]; ok && v != nil {
(*gfSettings)[gfKey] = v.(bool)
}
}

func unpackNotifierStringField(tfSettings, gfSettings *map[string]interface{}, tfKey, gfKey string) {
if v, ok := (*tfSettings)[tfKey]; ok && v != nil {
(*gfSettings)[gfKey] = v.(string)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,192 @@ func (o lineNotifier) unpack(raw interface{}, name string) *models.EmbeddedConta
}
}

type mqttNotifier struct{}

var _ notifier = (*mqttNotifier)(nil)

func (o mqttNotifier) meta() notifierMeta {
return notifierMeta{
field: "mqtt",
typeStr: "mqtt",
desc: "A contact point that sends notifications to an MQTT broker.",
secureFields: []string{"password"},
}
}

func (o mqttNotifier) schema() *schema.Resource {
r := commonNotifierResource()
r.Schema["broker_url"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The URL of the MQTT broker.",
}
r.Schema["topic"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The topic to publish messages to.",
}
r.Schema["client_id"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The client ID to use when connecting to the broker.",
}
r.Schema["message_format"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"json", "text"}, false),
Description: "The format of the message to send. Supported values are `json` and `text`.",
Default: "text",
}
r.Schema["username"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The username to use when connecting to the broker.",
}
r.Schema["password"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The password to use when connecting to the broker.",
}
r.Schema["qos"] = &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 0,
ValidateFunc: validation.IntBetween(0, 2),
Description: "The quality of service to use when sending messages. Supported values are 0, 1, and 2.",
}
r.Schema["retain"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Whether to retain messages on the broker.",
}

r.Schema["tls_config"] = &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Description: "TLS configuration for the connection.",
Elem: (&tlsConfig{}).schema(),
}

return r
}

func (o mqttNotifier) pack(p *models.EmbeddedContactPoint, data *schema.ResourceData) (interface{}, error) {
notifier := packCommonNotifierFields(p)
settings := p.Settings.(map[string]interface{})

packNotifierStringField(&settings, &notifier, "brokerUrl", "broker_url")
packNotifierStringField(&settings, &notifier, "topic", "topic")
packNotifierStringField(&settings, &notifier, "clientId", "client_id")
packNotifierStringField(&settings, &notifier, "messageFormat", "message_format")
packNotifierStringField(&settings, &notifier, "username", "username")
packNotifierIntField(&settings, &notifier, "qos", "qos")
packNotifierBoolField(&settings, &notifier, "retain", "retain")

// Pack TLS config if present
if v, ok := settings["tlsConfig"]; ok && v != nil {
tlsConfig := &tlsConfig{}
if packed, err := tlsConfig.pack(&models.EmbeddedContactPoint{Settings: v}, data); err == nil {
notifier["tls_config"] = []interface{}{packed}
}
delete(settings, "tlsConfig")
}

packSecureFields(notifier, getNotifierConfigFromStateWithUID(data, o, p.UID), o.meta().secureFields)

notifier["settings"] = packSettings(p)
return notifier, nil
}

func (o mqttNotifier) unpack(raw interface{}, name string) *models.EmbeddedContactPoint {
json := raw.(map[string]interface{})
uid, disableResolve, settings := unpackCommonNotifierFields(json)

unpackNotifierStringField(&json, &settings, "broker_url", "brokerUrl")
unpackNotifierStringField(&json, &settings, "topic", "topic")
unpackNotifierStringField(&json, &settings, "client_id", "clientId")
unpackNotifierStringField(&json, &settings, "message_format", "messageFormat")
unpackNotifierStringField(&json, &settings, "username", "username")
unpackNotifierStringField(&json, &settings, "password", "password")

// Unpack TLS config if present
if v, ok := json["tls_config"]; ok && v != nil {
tlsConfigs := v.(*schema.Set).List()
if len(tlsConfigs) > 0 {
settings["tlsConfig"] = (&tlsConfig{}).unpack(tlsConfigs[0])
}
}

return &models.EmbeddedContactPoint{
UID: uid,
Name: name,
Type: common.Ref(o.meta().typeStr),
DisableResolveMessage: disableResolve,
Settings: settings,
}
}

type tlsConfig struct{}

func (t tlsConfig) schema() *schema.Resource {
r := &schema.Resource{
Schema: make(map[string]*schema.Schema),
}

r.Schema["insecure_skip_verify"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Whether to skip verification of the server's certificate chain and host name.",
}
r.Schema["ca_certificate"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The CA certificate to use when verifying the server's certificate.",
}
r.Schema["client_certificate"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The client certificate to use when connecting to the server.",
}
r.Schema["client_key"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The client key to use when connecting to the server.",
}

return r
}

func (t tlsConfig) pack(p *models.EmbeddedContactPoint, data *schema.ResourceData) (interface{}, error) {
settings := p.Settings.(map[string]interface{})
tls := make(map[string]interface{})

packNotifierBoolField(&settings, &tls, "insecureSkipVerify", "insecure_skip_verify")
packNotifierStringField(&settings, &tls, "caCertificate", "ca_certificate")
packNotifierStringField(&settings, &tls, "clientCertificate", "client_certificate")
packNotifierStringField(&settings, &tls, "clientKey", "client_key")

return tls, nil
}

func (t tlsConfig) unpack(raw interface{}) map[string]interface{} {
json := raw.(map[string]interface{})
tls := make(map[string]interface{})

unpackNotifierBoolField(&json, &tls, "insecure_skip_verify", "insecureSkipVerify")
unpackNotifierStringField(&json, &tls, "ca_certificate", "caCertificate")
unpackNotifierStringField(&json, &tls, "client_certificate", "clientCertificate")
unpackNotifierStringField(&json, &tls, "client_key", "clientKey")

return tls
}

type oncallNotifier struct {
}

Expand Down
Loading
Loading