├── tests
└── certs
│ ├── ca.srl
│ ├── generate_self_certs.sh
│ ├── server.csr
│ ├── ca.crt
│ ├── server.crt
│ ├── ca.key
│ └── server.key
├── examples
├── data-sources
│ ├── mimir_alertmanager_config
│ │ └── data-source.tf
│ ├── mimir_distributor_tenant_stats
│ │ └── data-source.tf
│ ├── mimir_rule_group_alerting
│ │ └── data-source.tf
│ └── mimir_rule_group_recording
│ │ └── data-source.tf
├── resources
│ ├── mimir_alertmanager_config
│ │ ├── import.sh
│ │ └── resource.tf
│ ├── mimir_rule_group_alerting
│ │ ├── import.sh
│ │ └── resource.tf
│ └── mimir_rule_group_recording
│ │ ├── import.sh
│ │ └── resource.tf
└── provider
│ ├── provider.tf
│ ├── provider-token-auth.tf
│ ├── provider-basic-auth.tf
│ └── provider-custom-headers.tf
├── terraform-registry-manifest.json
├── tools
└── tools.go
├── .gitignore
├── main.go
├── mimir
├── import_mimir_alertmanager_config_test.go
├── import_mimir_rule_group_alerting_test.go
├── import_mimir_rule_group_recording_test.go
├── data_source_mimir_rule_group_alerting_test.go
├── data_source_mimir_rule_group_recording_test.go
├── provider_test.go
├── data_source_mimir_alertmanager_config.go
├── data_source_mimir_alertmanager_config_test.go
├── data_source_mimir_rule_group_alerting.go
├── data_source_mimir_distributor_tenant_stats.go
├── data_source_mimir_rule_group_recording.go
├── shared.go
├── shared_test.go
├── api_client.go
├── api_client_test.go
├── resource_mimir_alertmanager_config.go
├── provider.go
├── resource_mimir_rule_group_recording.go
├── resource_mimir_rule_group_alerting.go
├── resource_mimir_rules_test.go
├── resource_mimir_rule_group_recording_test.go
└── resource_mimir_rule_group_alerting_test.go
├── GNUmakefile
├── templates
└── index.md.tmpl
├── docs
├── data-sources
│ ├── distributor_tenant_stats.md
│ ├── rule_group_alerting.md
│ └── rule_group_recording.md
├── resources
│ ├── rule_group_recording.md
│ ├── rule_group_alerting.md
│ └── rules.md
└── index.md
├── .golangci.toml
├── .github
└── workflows
│ ├── release.yml
│ └── test.yml
├── .goreleaser.yml
├── go.mod
├── README.md
└── LICENSE
/tests/certs/ca.srl:
--------------------------------------------------------------------------------
1 | AF52A7EFDF6A53DD
2 |
--------------------------------------------------------------------------------
/examples/data-sources/mimir_alertmanager_config/data-source.tf:
--------------------------------------------------------------------------------
1 | data "mimir_alertmanager_config" "mytenant" {}
--------------------------------------------------------------------------------
/examples/resources/mimir_alertmanager_config/import.sh:
--------------------------------------------------------------------------------
1 | terraform import mimir_alertmanager_config.mytenant {{org_id}}
--------------------------------------------------------------------------------
/examples/data-sources/mimir_distributor_tenant_stats/data-source.tf:
--------------------------------------------------------------------------------
1 | data "mimir_distributor_tenant_stats" "tenants" {}
2 |
--------------------------------------------------------------------------------
/examples/resources/mimir_rule_group_alerting/import.sh:
--------------------------------------------------------------------------------
1 | terraform import mimir_rule_group_alerting.test {{namespace/name}}
--------------------------------------------------------------------------------
/examples/resources/mimir_rule_group_recording/import.sh:
--------------------------------------------------------------------------------
1 | terraform import mimir_rule_group_recording.test {{namespace/name}}
--------------------------------------------------------------------------------
/terraform-registry-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "metadata": {
4 | "protocol_versions": ["5.0"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/examples/data-sources/mimir_rule_group_alerting/data-source.tf:
--------------------------------------------------------------------------------
1 | data "mimir_rule_group_alerting" "alert" {
2 | name = "test1"
3 | namespace = "namespace1"
4 | }
--------------------------------------------------------------------------------
/examples/data-sources/mimir_rule_group_recording/data-source.tf:
--------------------------------------------------------------------------------
1 | data "mimir_rule_group_recording" "record" {
2 | name = "test1"
3 | namespace = "namespace1"
4 | }
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | //go:build tools
2 |
3 | package tools
4 |
5 | import (
6 | // document generation
7 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs"
8 | )
9 |
--------------------------------------------------------------------------------
/examples/provider/provider.tf:
--------------------------------------------------------------------------------
1 | provider "mimir" {
2 | ruler_uri = "http://127.0.0.1:8080/prometheus"
3 | alertmanager_uri = "http://127.0.0.1:8080"
4 | distributor_uri = "http://127.0.0.1:8080/distributor"
5 | org_id = "mytenant"
6 | }
7 |
--------------------------------------------------------------------------------
/examples/provider/provider-token-auth.tf:
--------------------------------------------------------------------------------
1 | provider "mimir" {
2 | ruler_uri = "http://127.0.0.1:8080/prometheus"
3 | alertmanager_uri = "http://127.0.0.1:8080"
4 | distributor_uri = "http://127.0.0.1:8080/distributor"
5 | org_id = "mytenant"
6 | token = "supersecrettoken"
7 | }
8 |
--------------------------------------------------------------------------------
/examples/provider/provider-basic-auth.tf:
--------------------------------------------------------------------------------
1 | provider "mimir" {
2 | ruler_uri = "http://127.0.0.1:8080/prometheus"
3 | alertmanager_uri = "http://127.0.0.1:8080"
4 | distributor_uri = "http://127.0.0.1:8080/distributor"
5 | org_id = "mytenant"
6 | username = "user"
7 | password = "password"
8 | }
9 |
--------------------------------------------------------------------------------
/examples/provider/provider-custom-headers.tf:
--------------------------------------------------------------------------------
1 | provider "mimir" {
2 | ruler_uri = "http://127.0.0.1:8080/prometheus"
3 | alertmanager_uri = "http://127.0.0.1:8080"
4 | distributor_uri = "http://127.0.0.1:8080/distributor"
5 | org_id = "mytenant"
6 | header = {
7 | "Custom-Auth" = "Custom value"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/resources/mimir_rule_group_recording/resource.tf:
--------------------------------------------------------------------------------
1 | resource "mimir_rule_group_recording" "test" {
2 | name = "test1"
3 | namespace = "namespace1"
4 | interval = "6h"
5 | query_offset = "5m"
6 | rule {
7 | expr = "sum by (job) (http_inprogress_requests)"
8 | record = "job:http_inprogress_requests:sum"
9 | }
10 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | build/
3 | *.exe
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # godoc files
9 | docs/godoc/**
10 | # mkdocs files
11 | mkdocs.yml
12 |
13 | # Test binary, build with `go test -c`
14 | *.test
15 |
16 | # Output of the go coverage tool, specifically when used with LiteIDE
17 | *.out
18 |
19 | .terraform/
20 |
21 | *.tfstate
22 | *.tfstate.backup
23 | crash.log
24 | terraform-provider-mimir.log
25 | terraform-provider-mimir
26 |
--------------------------------------------------------------------------------
/examples/resources/mimir_rule_group_alerting/resource.tf:
--------------------------------------------------------------------------------
1 | resource "mimir_rule_group_alerting" "test" {
2 | name = "test1"
3 | namespace = "namespace1"
4 | rule {
5 | alert = "HighRequestLatency"
6 | expr = "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5"
7 | for = "10m"
8 | labels = {
9 | severity = "warning"
10 | }
11 | annotations = {
12 | summary = "High request latency"
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 |
6 | "github.com/fgouteroux/terraform-provider-mimir/mimir"
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
8 | )
9 |
10 | var (
11 | // these will be set by the goreleaser configuration
12 | // to appropriate values for the compiled binary
13 | version string = "dev"
14 | )
15 |
16 | func main() {
17 | var debugMode bool
18 |
19 | flag.BoolVar(&debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve")
20 | flag.Parse()
21 |
22 | opts := &plugin.ServeOpts{ProviderFunc: mimir.Provider(version), Debug: debugMode, ProviderAddr: "registry.terraform.io/fgouteroux/mimir"}
23 | plugin.Serve(opts)
24 | }
25 |
--------------------------------------------------------------------------------
/mimir/import_mimir_alertmanager_config_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
7 | )
8 |
9 | func TestAccImportAlertmanagerConfig_Basic(t *testing.T) {
10 | resourceName := "mimir_alertmanager_config.mytenant"
11 |
12 | resource.Test(t, resource.TestCase{
13 | PreCheck: func() { testAccPreCheck(t) },
14 | ProviderFactories: testAccProviderFactories,
15 | Steps: []resource.TestStep{
16 | {
17 | Config: testAccResourceAlertmanagerConfig_basic,
18 | },
19 |
20 | {
21 | ResourceName: resourceName,
22 | ImportState: true,
23 | ImportStateVerify: true,
24 | },
25 | },
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/mimir/import_mimir_rule_group_alerting_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
7 | )
8 |
9 | func TestAccImportRuleGroupAlerting_basic(t *testing.T) {
10 | resourceName := "mimir_rule_group_alerting.alert_1"
11 |
12 | resource.Test(t, resource.TestCase{
13 | PreCheck: func() { testAccPreCheck(t) },
14 | ProviderFactories: testAccProviderFactories,
15 | Steps: []resource.TestStep{
16 | {
17 | Config: testAccResourceRuleGroupAlerting_basic,
18 | },
19 |
20 | {
21 | ResourceName: resourceName,
22 | ImportState: true,
23 | ImportStateVerify: true,
24 | },
25 | },
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/mimir/import_mimir_rule_group_recording_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
7 | )
8 |
9 | func TestAccImportRuleGroupRecording_basic(t *testing.T) {
10 | resourceName := "mimir_rule_group_recording.record_1"
11 |
12 | resource.Test(t, resource.TestCase{
13 | PreCheck: func() { testAccPreCheck(t) },
14 | ProviderFactories: testAccProviderFactories,
15 | Steps: []resource.TestStep{
16 | {
17 | Config: testAccResourceRuleGroupRecording_basic,
18 | },
19 |
20 | {
21 | ResourceName: resourceName,
22 | ImportState: true,
23 | ImportStateVerify: true,
24 | },
25 | },
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/GNUmakefile:
--------------------------------------------------------------------------------
1 | default: build
2 |
3 | build:
4 | go build -v ./...
5 |
6 | install: build
7 | mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH}
8 | mv ${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH}
9 |
10 | # See https://golangci-lint.run/
11 | lint:
12 | golangci-lint run -c .golangci.toml ./...
13 |
14 | generate:
15 | go generate ./...
16 |
17 | fmt:
18 | gofmt -s -w -e .
19 |
20 | test:
21 | go test -v -cover -timeout=120s -coverprofile=cover.out -parallel=4 ./...
22 | go tool cover -func=cover.out
23 |
24 | testacc:
25 | TF_ACC=1 go test -v -cover -coverprofile=cover.out -timeout 120m ./...
26 | go tool cover -func=cover.out
27 |
28 | .PHONY: build install lint generate fmt test testacc
--------------------------------------------------------------------------------
/tests/certs/generate_self_certs.sh:
--------------------------------------------------------------------------------
1 | #/bin/sh
2 |
3 | # Create CA key + certificate
4 | openssl req \
5 | -newkey rsa:2048 \
6 | -nodes \
7 | -days 3650 \
8 | -x509 \
9 | -keyout ca.key \
10 | -out ca.crt \
11 | -subj "/CN=*"
12 |
13 | # Create server certificate request and key
14 | openssl req \
15 | -newkey rsa:2048 \
16 | -nodes \
17 | -keyout server.key \
18 | -out server.csr \
19 | -subj "/C=GB/ST=Paris/L=Paris/O=tests/OU=IT Department/CN=*"
20 |
21 | # Sign the server certificate request with the CA key
22 | # adding SAN IP
23 | openssl x509 \
24 | -req \
25 | -days 365 \
26 | -sha256 \
27 | -in server.csr \
28 | -CA ca.crt \
29 | -CAkey ca.key \
30 | -CAcreateserial \
31 | -out server.crt \
32 | -extfile <(echo subjectAltName = IP:127.0.0.1)
33 |
--------------------------------------------------------------------------------
/templates/index.md.tmpl:
--------------------------------------------------------------------------------
1 | ---
2 | layout: ""
3 | page_title: "Provider: Mimir"
4 | description: |-
5 | The Mimir provider provides configuration management resources for Grafana Mimir.
6 | ---
7 |
8 | # Mimir Provider
9 |
10 | The Mimir provider provides configuration management resources for
11 | [Grafana Mimir](https://grafana.com/oss/mimir/).
12 |
13 | ## Example Usage
14 |
15 | ### Creating a Mimir provider
16 |
17 | {{ tffile "examples/provider/provider.tf" }}
18 |
19 | ### Creating a Mimir provider with basic auth
20 |
21 | {{ tffile "examples/provider/provider-basic-auth.tf" }}
22 |
23 | ### Creating a Mimir provider with token auth
24 |
25 | {{ tffile "examples/provider/provider-token-auth.tf" }}
26 |
27 | ### Creating a Mimir provider with custom headers
28 |
29 | {{ tffile "examples/provider/provider-custom-headers.tf" }}
30 |
31 | {{ .SchemaMarkdown | trimspace }}
32 |
--------------------------------------------------------------------------------
/examples/resources/mimir_alertmanager_config/resource.tf:
--------------------------------------------------------------------------------
1 | resource "mimir_alertmanager_config" "mytenant" {
2 | route {
3 | group_by = ["..."]
4 | group_wait = "30s"
5 | group_interval = "5m"
6 | repeat_interval = "1h"
7 | receiver = "pagerduty_dev"
8 | }
9 | child_route {
10 | matchers = ["severity=\"critical\""]
11 |
12 | child_route {
13 | receiver = "pagerduty_prod"
14 | matchers = ["environment=\"prod\""]
15 | }
16 |
17 | child_route {
18 | receiver = "pagerduty_dev"
19 | matchers = ["environment=\"dev\""]
20 | }
21 | }
22 | receiver {
23 | name = "pagerduty_dev"
24 | pagerduty_configs {
25 | routing_key = "secret"
26 | details = {
27 | environment = "dev"
28 | }
29 | }
30 | }
31 | receiver {
32 | name = "pagerduty_prod"
33 | pagerduty_configs {
34 | routing_key = "secret"
35 | details = {
36 | environment = "prod"
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/tests/certs/server.csr:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE REQUEST-----
2 | MIICpjCCAY4CAQAwYTELMAkGA1UEBhMCR0IxDjAMBgNVBAgMBVBhcmlzMQ4wDAYD
3 | VQQHDAVQYXJpczEOMAwGA1UECgwFdGVzdHMxFjAUBgNVBAsMDUlUIERlcGFydG1l
4 | bnQxCjAIBgNVBAMMASowggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+
5 | Y1OVT/8CA7sg0SxId/mypIOIhM33PRXqVptgbmxqa3s1UariKp1H1aG/S/GuZAqF
6 | l5wijed+Q+905HINkIiD41bRBRa/XTnIhsKnghxmEDi5LRdIDYXk1MVYCP2vaRZn
7 | iyLSzclTvWYdV+bX7JlprkpFer9gZXa5O9smfMldJMuJPFNHduaAF3SXWuZNNKNl
8 | hO0yyUhj5CquRbsj6bNOYBj48jmpzKFY0lWMcafWbdubuZcLlVVoJcKWwUtEBvak
9 | Tvnl0IqlgZAsmG02kVBCzh3KfIbKZI7LTfRLprepbUcvZfgi6v7w+xFeeVjeRKa8
10 | Ii0fRIpe0WQsh2gbx5mnAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAd/LoaZnH
11 | sqBTqdJY8Z3Kb0P1ayRJROD9GfgOj2w+AY5y48tW8ZKdA6vAywnfIkI3C1tvK2jt
12 | yZcnXlLUu1/yxLC7Hy43D/K9anDDAhBfth7UgYQWuxf8qDoLwbxl3B98asIIQDuN
13 | FnF1dBwnLVADsnLPHuVvPu/B5eYKGCExpCRvKCAjz1apsuOK95wKlJU+4OfJ8sC1
14 | uedXDMGrRqo6GVx2qeI1OXvdhfW4dTWB5sXHygggVpBkTTVJUVHS1kiXZFkmi7HN
15 | LlYoNawzCIF/uUVKubzHH2BmGF9r72y/d1BePbfWiaTjabTc/FilNRYTsQH8sp49
16 | B5U/KlaiRMzCjw==
17 | -----END CERTIFICATE REQUEST-----
18 |
--------------------------------------------------------------------------------
/tests/certs/ca.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIC6zCCAdOgAwIBAgIJAKFBKiHPMPAoMA0GCSqGSIb3DQEBCwUAMAwxCjAIBgNV
3 | BAMMASowHhcNMjMxMDE4MDkyMDA4WhcNMzMxMDE1MDkyMDA4WjAMMQowCAYDVQQD
4 | DAEqMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtd0UpdWkN8daJ+UR
5 | FGYHRQh5JsdpCi6xZKMTPRVmwkYSRVODaashwAkJou0tP0yHlGpfGJjZty6sVVgf
6 | WktlAFG+0SUgoqY0xMJbsXz/3EekEJVyHA9paDdJfMqDhcFTfuWZ7Ua/6o8Rjroq
7 | QiV3b6l/HEE5zr6bSQhUPy8XiBRDMubB3u4JuY8VE+T6ngITjk61q6LdrNqlPHDn
8 | jMKoE3d72focT1Dq209yKkcnPsKsGoIlUUrKFbkN9lWCiGRROBAdWeBPbl8KsKBr
9 | vsB37xL44I/lJJoAHLSj3B4QQgHdA07TmvP1mmmEu57JCshmPMkkd9sDZp8hQ+sx
10 | nFqhwQIDAQABo1AwTjAdBgNVHQ4EFgQUR8JSkXGK1JAaifAZ6xe9X2GjrjowHwYD
11 | VR0jBBgwFoAUR8JSkXGK1JAaifAZ6xe9X2GjrjowDAYDVR0TBAUwAwEB/zANBgkq
12 | hkiG9w0BAQsFAAOCAQEAdfgY1DJPoRAvWIeAzLr1r6LPp2Rchv9OBm4rP0J7WqTh
13 | jZa0veO5sBHlGWaJmcpIpPevbCVsmhFlApJuW4471z5qf+W8CMXaPCMKF08a7Gzy
14 | ++zbVR0hKWBh+549FtgguHzvwFXhxcKI/qZUrRLro7fg9HGfv8XAu3p9L0uNbibh
15 | 6L29aAAiQZdxiJK9NdjrqqraNQ5eugxhhz01CyX+mGbigGTxgcoG/Le1TUrt6aGO
16 | bKT6+FZrU5FeRSXz1X2+BFGgaISLNQZMDwaaf7+WS0J06Cuw/Z923smpPzXW1YTD
17 | gBXXcfGXi0WOaTPY14Q2QFvbimRWjrIgSJEPjuAptA==
18 | -----END CERTIFICATE-----
19 |
--------------------------------------------------------------------------------
/tests/certs/server.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDAzCCAeugAwIBAgIJAK9Sp+/falPdMA0GCSqGSIb3DQEBCwUAMAwxCjAIBgNV
3 | BAMMASowHhcNMjMxMDE4MDkyMDA4WhcNMjQxMDE3MDkyMDA4WjBhMQswCQYDVQQG
4 | EwJHQjEOMAwGA1UECAwFUGFyaXMxDjAMBgNVBAcMBVBhcmlzMQ4wDAYDVQQKDAV0
5 | ZXN0czEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEKMAgGA1UEAwwBKjCCASIwDQYJ
6 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL5jU5VP/wIDuyDRLEh3+bKkg4iEzfc9
7 | FepWm2BubGprezVRquIqnUfVob9L8a5kCoWXnCKN535D73Tkcg2QiIPjVtEFFr9d
8 | OciGwqeCHGYQOLktF0gNheTUxVgI/a9pFmeLItLNyVO9Zh1X5tfsmWmuSkV6v2Bl
9 | drk72yZ8yV0ky4k8U0d25oAXdJda5k00o2WE7TLJSGPkKq5FuyPps05gGPjyOanM
10 | oVjSVYxxp9Zt25u5lwuVVWglwpbBS0QG9qRO+eXQiqWBkCyYbTaRUELOHcp8hspk
11 | jstN9Eumt6ltRy9l+CLq/vD7EV55WN5EprwiLR9Eil7RZCyHaBvHmacCAwEAAaMT
12 | MBEwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAiFQJj5iQcq39
13 | GnimgMWWbstdn92vek4gHAJ6oDAa2AoRV+3TF4jc8CU3UXkv1L5EQmDb7fIuKHRK
14 | PO5s+GzBKE1klFiekiCeZhKx+ZtifGlJjYiiqZIc05vbLMegQZRP2YXVUc0fmNqm
15 | w+t4+n27b+V8edaydzlN/Ir2DmIBcn6szuq7wuE2KVZdihNat0G63i/kLBAfNVfw
16 | IOCIAKi2+kMiI8fntdUUyabhnT/iTalJps3UD1/DbMMO6M8UYW9n2ARy99chNu04
17 | GdJe+fNMcN/V5qM95WGqaKHm4wUjbMwyvSStpZawWQG/LltNvImdKMrfwO6wxUde
18 | sYtAoon/fg==
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/docs/data-sources/distributor_tenant_stats.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "mimir_distributor_tenant_stats Data Source - terraform-provider-mimir"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # mimir_distributor_tenant_stats (Data Source)
10 |
11 |
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | data "mimir_distributor_tenant_stats" "tenants" {}
17 | ```
18 |
19 |
20 | ## Schema
21 |
22 | ### Optional
23 |
24 | - `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
25 | - `user` (String) Query specific user stats, if not specified, all users are returned
26 |
27 | ### Read-Only
28 |
29 | - `id` (String) The ID of this resource.
30 | - `stats` (List of Object) Stats list, does not account for replication factor (see [below for nested schema](#nestedatt--stats))
31 |
32 |
33 | ### Nested Schema for `stats`
34 |
35 | Read-Only:
36 |
37 | - `api_ingest_rate` (Number)
38 | - `rule_ingest_rate` (Number)
39 | - `series` (Number)
40 | - `total_ingest_rate` (Number)
41 | - `user` (String)
42 |
43 |
44 |
--------------------------------------------------------------------------------
/docs/data-sources/rule_group_alerting.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "mimir_rule_group_alerting Data Source - terraform-provider-mimir"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # mimir_rule_group_alerting (Data Source)
10 |
11 |
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | data "mimir_rule_group_alerting" "alert" {
17 | name = "test1"
18 | namespace = "namespace1"
19 | }
20 | ```
21 |
22 |
23 | ## Schema
24 |
25 | ### Required
26 |
27 | - `name` (String) Alerting Rule group name
28 |
29 | ### Optional
30 |
31 | - `namespace` (String) Alerting Rule group namespace
32 | - `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
33 |
34 | ### Read-Only
35 |
36 | - `id` (String) The ID of this resource.
37 | - `rule` (List of Object) (see [below for nested schema](#nestedatt--rule))
38 | - `source_tenants` (List of String) Allows aggregating data from multiple tenants while evaluating a rule group.
39 |
40 |
41 | ### Nested Schema for `rule`
42 |
43 | Read-Only:
44 |
45 | - `alert` (String)
46 | - `annotations` (Map of String)
47 | - `expr` (String)
48 | - `for` (String)
49 | - `labels` (Map of String)
50 |
51 |
52 |
--------------------------------------------------------------------------------
/.golangci.toml:
--------------------------------------------------------------------------------
1 | version = '2'
2 |
3 | [linters]
4 | default = 'none'
5 | enable = [
6 | 'dogsled',
7 | 'goconst',
8 | 'gocritic',
9 | 'gocyclo',
10 | 'goprintffuncname',
11 | 'gosec',
12 | 'govet',
13 | 'ineffassign',
14 | 'misspell',
15 | 'nakedret',
16 | 'rowserrcheck',
17 | 'staticcheck',
18 | 'unconvert',
19 | 'unparam',
20 | 'unused',
21 | 'whitespace'
22 | ]
23 |
24 | [linters.settings]
25 | [linters.settings.goconst]
26 | min-len = 5
27 | min-occurrences = 5
28 |
29 | [linters.exclusions]
30 | generated = 'lax'
31 | presets = [
32 | 'comments',
33 | 'common-false-positives',
34 | 'legacy',
35 | 'std-error-handling'
36 | ]
37 | paths = [
38 | 'third_party$',
39 | 'builtin$',
40 | 'examples$'
41 | ]
42 |
43 | [[linters.exclusions.rules]]
44 | linters = [
45 | 'staticcheck'
46 | ]
47 | path = '_test.go'
48 | text = 'ST1003: should not use underscores in Go names;'
49 |
50 | [[linters.exclusions.rules]]
51 | linters = [
52 | 'gosec'
53 | ]
54 | text = 'G402: TLS MinVersion too low.'
55 |
56 | [[linters.exclusions.rules]]
57 | linters = [
58 | 'gosec'
59 | ]
60 | text = 'G115: integer overflow conversion'
61 |
62 | [formatters]
63 | enable = [
64 | 'goimports'
65 | ]
66 |
67 | [formatters.exclusions]
68 | generated = 'lax'
69 | paths = [
70 | 'third_party$',
71 | 'builtin$',
72 | 'examples$'
73 | ]
74 |
--------------------------------------------------------------------------------
/docs/data-sources/rule_group_recording.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "mimir_rule_group_recording Data Source - terraform-provider-mimir"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # mimir_rule_group_recording (Data Source)
10 |
11 |
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | data "mimir_rule_group_recording" "record" {
17 | name = "test1"
18 | namespace = "namespace1"
19 | }
20 | ```
21 |
22 |
23 | ## Schema
24 |
25 | ### Required
26 |
27 | - `name` (String) Recording Rule group name
28 |
29 | ### Optional
30 |
31 | - `namespace` (String) Recording Rule group namespace
32 | - `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
33 |
34 | ### Read-Only
35 |
36 | - `evaluation_delay` (String, Deprecated) **Deprecated** The duration by which to delay the execution of the recording rule.
37 | - `id` (String) The ID of this resource.
38 | - `interval` (String) Recording Rule group interval
39 | - `query_offset` (String) The duration by which to delay the execution of the recording rule.
40 | - `rule` (List of Object) (see [below for nested schema](#nestedatt--rule))
41 | - `source_tenants` (List of String) Allows aggregating data from multiple tenants while evaluating a rule group.
42 |
43 |
44 | ### Nested Schema for `rule`
45 |
46 | Read-Only:
47 |
48 | - `expr` (String)
49 | - `labels` (Map of String)
50 | - `record` (String)
51 |
52 |
53 |
--------------------------------------------------------------------------------
/tests/certs/ca.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC13RSl1aQ3x1on
3 | 5REUZgdFCHkmx2kKLrFkoxM9FWbCRhJFU4NpqyHACQmi7S0/TIeUal8YmNm3LqxV
4 | WB9aS2UAUb7RJSCipjTEwluxfP/cR6QQlXIcD2loN0l8yoOFwVN+5ZntRr/qjxGO
5 | uipCJXdvqX8cQTnOvptJCFQ/LxeIFEMy5sHe7gm5jxUT5PqeAhOOTrWrot2s2qU8
6 | cOeMwqgTd3vZ+hxPUOrbT3IqRyc+wqwagiVRSsoVuQ32VYKIZFE4EB1Z4E9uXwqw
7 | oGu+wHfvEvjgj+UkmgActKPcHhBCAd0DTtOa8/WaaYS7nskKyGY8ySR32wNmnyFD
8 | 6zGcWqHBAgMBAAECggEAUAnjq3SSGIZhFEvJL4wTyPsIrtt2TCfupJbswaZomjnS
9 | krZFkLzjVqvjlNQDZOdeiCAkiCPFNcyRxeHJsqiZ5AEWFN/dNBgoAyxyxHN5+81/
10 | 1FihZt4ViutCKWzLY5OJFsEf9IB9uFKM7J7cOE5a+UbEjX/bXDUhDIxg34nvNlDO
11 | eYjWNNqlV2ejJ/Na2cjQmntSaDWuIDKQAgKz+OBZrM4pCfuRVMLwJf57uQZq66v4
12 | VzG+MJ4thSmP/D/4NoYBAZdqUHcEW/j4f0bJauxdbPw9DliIR5ClmVJhVYuIYQ+3
13 | 8ATB4OZujWRKYb690l64anp6otmutgWaGTOUFwtpAQKBgQDiXc+n7WQNdXkVpXFH
14 | ycKrx109hxI07AziwwAYJNM4ThNVtxZqJo81/kukWgZ760SrvGFUgdD/Yxnieji9
15 | UrsUIjL+C5MB6eNm0HZmV7Ln0J0QtQv6HRuaatxkghudxDis8ZI1w/flk8dhajaY
16 | a2tYi90id+QDGp+KWGAdc4OgMQKBgQDNq9nsgwnj9snHF7JqYok2ND0OIunWr4aQ
17 | /NRMHw35CAoyHfwVWbQTBowXhWl4lVUn2FYKGXf63ODAKYrwhLJbqmKKrO5aUPdB
18 | fvJfooWMsuHH8jxQEZO939swOCQs4iIKskFhFs57cMB6Ff4Mzu6dpvSbdzA+eSww
19 | RCbRJJfGkQKBgHcT2EfDJi26TR7o7+VOqlxIvLDAHADA56pl3+fTf1UQJTWi4WSn
20 | shA6HDpxrRWE1pPsvdqBi5q83AH6P1zAZ28Y6nAfUI7iJWCRaqc9nPw7DlSamJlS
21 | tJGPgy5QDwz4CsRXM42a8A3RpkGFaQDXCRoEQSh/nu9PEvvor27utCARAoGBALo2
22 | 01uxlR3ijvSEvxRVXNhH2dPXxpKLX7IcR/cX0uXb4qdXwKpwCnuDXQna0BDOXEh0
23 | Ngp03s2yeIdj1ZlI+9fE3+2g/zmkvwl87+Oo2XP3bjdDWlpAX9z1NpZk7lP49ktq
24 | 84j9Rjw7bPncuaVNInx48lVkR7j4qwqUWfTYRdcRAoGBAIrxyokMDknSM1hNs2QG
25 | vYm0gwEDLtn2Ig2szsdPbGzPwjHLDPuFemIz1dMt8HO3Xo0NiPbvs0BB0s8UD7F5
26 | NpzLZ/+cwuXZlHmITt8M+e5mPsYBGgvAkKCVqR4mD1v7iH+Tc/PaI4Ve9gvRST3A
27 | cAa/ZOGJIE2N7X0WsBWw6fsX
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/tests/certs/server.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+Y1OVT/8CA7sg
3 | 0SxId/mypIOIhM33PRXqVptgbmxqa3s1UariKp1H1aG/S/GuZAqFl5wijed+Q+90
4 | 5HINkIiD41bRBRa/XTnIhsKnghxmEDi5LRdIDYXk1MVYCP2vaRZniyLSzclTvWYd
5 | V+bX7JlprkpFer9gZXa5O9smfMldJMuJPFNHduaAF3SXWuZNNKNlhO0yyUhj5Cqu
6 | Rbsj6bNOYBj48jmpzKFY0lWMcafWbdubuZcLlVVoJcKWwUtEBvakTvnl0IqlgZAs
7 | mG02kVBCzh3KfIbKZI7LTfRLprepbUcvZfgi6v7w+xFeeVjeRKa8Ii0fRIpe0WQs
8 | h2gbx5mnAgMBAAECggEAIikSQzUyTTs8JTxC1NHqFJqeGy2xAw6L0xstD9VgUIvu
9 | dHet0JzS0aPMPNW0jJTYqnW0hyKtCaPI4FFpAXQQepGc7y4ZkcZ5arh7TrDyxYFd
10 | nCQyDDSY9KEUs6IRWprhcTyux+IyoHrYSqWN/uot8J9cJjFLC1HJRp0tyi8Ttkln
11 | knDvtrljU3kv+VpzmANz6AsptTJnwts6+w0E/XLmiJ1FAPj8JCwQXuVjM4M9/uC2
12 | xBWl/F4jUPe3Jvq2snfKP/g96MVQi1A+Kk1lYBu82pQYTsM65Xy+Xc/C4fB9HWag
13 | ASxW5zsIl3cvri6o2NMAJ5AfgIF7EWdsqIeJs+r06QKBgQDkbYUn27LY9SdySQK3
14 | 2auJnbCu0WD1AEeVYvYXqcPTfucvqyx2A+gb0MwKafYrc7HqMUBPDTMh6McwwOZc
15 | YBQc/xMTqW5Ggfjt38h6glXF+Rw/4xqcgokWYPqdYobXjYJ4ZtRScMj8GBaV/I6X
16 | 8BdOlkgf/OfwPs6i+kroP7QpwwKBgQDVXl2q34oZPI1mgQ+DMaglszX6JMu7JFMn
17 | lhI5EwBdLQZRxoMk1BBEw8o/iH1TR763NLEDJ6LYRs8ON5uhpP/U4gdIVW/YGYCL
18 | 85spVk/aWmTYVWvDS7fxqk5yoY59jl7mvEho0By/G7B9nejFmbuBrH8i02Z5ME5i
19 | XVwKOTIuTQKBgEEDvvtqN4wysoh/qUtko8MmY6xOIbd53eXx5bM0eW0P2IMjyCLK
20 | 19xa4EgygoM5ibDrVa9qRVxoYZFJxgcSvYbHPsnA1ocw5QXnRKtBv2H6lgTFAcT8
21 | DkKVVZ9H9LMTLshQFCxmt32w6XTFDlzGVAvlECAynl0tsyiF7p4Ny5x7AoGBAMfn
22 | Loi+AKueQLhNqJP4/Rm9KpAe00WVRdcr0gzpEYk6etY4z3u8tzVF3oHhxdE1wafx
23 | 1Poos3g/9xPR45zxcsMntTz+1te3JOl6Jd/3vr7Lu/m3JkyF72OadP8O/+oAbkdt
24 | FcYlXRlrvtIiFQi0/KkETOT6/OID8RxVbbsiVTJtAoGAen9Zq4ADjAJ1htT3lpIo
25 | KC7O2cdoaAfPc9tXo7jhRbBjNeT1VA8rjpW8HwNj0YBe9HXGMcg3w+UVHHUMnguY
26 | i2uXhxplWBKzW6BNgsyUztiQGoWQ3dCrwbVbO12pyoiCvKZ8LbhhitTFESvcJUZJ
27 | grUuxlBaCS/xCOxe/8nx36M=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # This GitHub action can publish assets for release when a tag is created.
2 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0).
3 | #
4 | # This uses an action (hashicorp/ghaction-import-gpg) that assumes you set your
5 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE`
6 | # secret. If you would rather own your own GPG handling, please fork this action
7 | # or use an alternative one for key handling.
8 | #
9 | # You will need to pass the `--batch` flag to `gpg` in your signing step
10 | # in `goreleaser` to indicate this is being used in a non-interactive mode.
11 | #
12 | name: release
13 | on:
14 | push:
15 | tags:
16 | - 'v*'
17 | permissions:
18 | contents: write
19 | jobs:
20 | goreleaser:
21 | runs-on: ubuntu-latest
22 | steps:
23 | -
24 | name: Checkout
25 | uses: actions/checkout@v3
26 | -
27 | name: Unshallow
28 | run: git fetch --prune --unshallow
29 | -
30 | name: Set up Go
31 | uses: actions/setup-go@v3
32 | with:
33 | go-version-file: 'go.mod'
34 | cache: true
35 | -
36 | name: Import GPG key
37 | uses: crazy-max/ghaction-import-gpg@v5
38 | id: import_gpg
39 | with:
40 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
41 | passphrase: ${{ secrets.PASSPHRASE }}
42 | -
43 | name: Run GoReleaser
44 | uses: goreleaser/goreleaser-action@v3.1.0
45 | with:
46 | version: latest
47 | args: release --clean
48 | env:
49 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
50 | # GitHub sets this automatically
51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 |
--------------------------------------------------------------------------------
/docs/resources/rule_group_recording.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "mimir_rule_group_recording Resource - terraform-provider-mimir"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # mimir_rule_group_recording (Resource)
10 |
11 |
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | resource "mimir_rule_group_recording" "test" {
17 | name = "test1"
18 | namespace = "namespace1"
19 | interval = "6h"
20 | query_offset = "5m"
21 | rule {
22 | expr = "sum by (job) (http_inprogress_requests)"
23 | record = "job:http_inprogress_requests:sum"
24 | }
25 | }
26 | ```
27 |
28 |
29 | ## Schema
30 |
31 | ### Required
32 |
33 | - `name` (String) Recording Rule group name
34 | - `rule` (Block List, Min: 1) (see [below for nested schema](#nestedblock--rule))
35 |
36 | ### Optional
37 |
38 | - `evaluation_delay` (String, Deprecated) **Deprecated** The duration by which to delay the execution of the recording rule.
39 | - `interval` (String) Recording Rule group interval
40 | - `namespace` (String) Recording Rule group namespace
41 | - `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
42 | - `query_offset` (String) The duration by which to delay the execution of the recording rule.
43 | - `source_tenants` (List of String) Allows aggregating data from multiple tenants while evaluating a rule group.
44 |
45 | ### Read-Only
46 |
47 | - `id` (String) The ID of this resource.
48 |
49 |
50 | ### Nested Schema for `rule`
51 |
52 | Required:
53 |
54 | - `expr` (String) The PromQL expression to evaluate.
55 | - `record` (String) The name of the time series to output to.
56 |
57 | Optional:
58 |
59 | - `labels` (Map of String) Labels to add or overwrite before storing the result.
60 |
61 | ## Import
62 |
63 | Import is supported using the following syntax:
64 |
65 | ```shell
66 | terraform import mimir_rule_group_recording.test {{namespace/name}}
67 | ```
68 |
--------------------------------------------------------------------------------
/docs/resources/rule_group_alerting.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "mimir_rule_group_alerting Resource - terraform-provider-mimir"
4 | subcategory: ""
5 | description: |-
6 |
7 | ---
8 |
9 | # mimir_rule_group_alerting (Resource)
10 |
11 |
12 |
13 | ## Example Usage
14 |
15 | ```terraform
16 | resource "mimir_rule_group_alerting" "test" {
17 | name = "test1"
18 | namespace = "namespace1"
19 | rule {
20 | alert = "HighRequestLatency"
21 | expr = "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5"
22 | for = "10m"
23 | labels = {
24 | severity = "warning"
25 | }
26 | annotations = {
27 | summary = "High request latency"
28 | }
29 | }
30 | }
31 | ```
32 |
33 |
34 | ## Schema
35 |
36 | ### Required
37 |
38 | - `name` (String) Alerting Rule group name
39 | - `rule` (Block List, Min: 1) (see [below for nested schema](#nestedblock--rule))
40 |
41 | ### Optional
42 |
43 | - `interval` (String) Alerting Rule group interval
44 | - `namespace` (String) Alerting Rule group namespace
45 | - `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
46 | - `source_tenants` (List of String) Allows aggregating data from multiple tenants while evaluating a rule group.
47 |
48 | ### Read-Only
49 |
50 | - `id` (String) The ID of this resource.
51 |
52 |
53 | ### Nested Schema for `rule`
54 |
55 | Required:
56 |
57 | - `alert` (String) The name of the alert.
58 | - `expr` (String) The PromQL expression to evaluate.
59 |
60 | Optional:
61 |
62 | - `annotations` (Map of String) Annotations to add to each alert.
63 | - `for` (String) The duration for which the condition must be true before an alert fires.
64 | - `keep_firing_for` (String) How long an alert will continue firing after the condition that triggered it has cleared.
65 | - `labels` (Map of String) Labels to add or overwrite for each alert.
66 |
67 | ## Import
68 |
69 | Import is supported using the following syntax:
70 |
71 | ```shell
72 | terraform import mimir_rule_group_alerting.test {{namespace/name}}
73 | ```
74 |
--------------------------------------------------------------------------------
/mimir/data_source_mimir_rule_group_alerting_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8 | )
9 |
10 | func TestAccDataSourceRuleGroupAlerting_basic(t *testing.T) {
11 | resource.Test(t, resource.TestCase{
12 | PreCheck: func() { testAccPreCheck(t) },
13 | ProviderFactories: testAccProviderFactories,
14 | Steps: []resource.TestStep{
15 | {
16 | Config: testAccDataSourceRuleGroupAlerting_basic,
17 | Check: resource.ComposeTestCheckFunc(
18 | resource.TestCheckResourceAttr("data.mimir_rule_group_alerting.alert_1", "name", "alert_1"),
19 | resource.TestCheckResourceAttr("data.mimir_rule_group_alerting.alert_1", "namespace", "namespace_1"),
20 | ),
21 | },
22 | },
23 | })
24 | }
25 |
26 | var testAccDataSourceRuleGroupAlerting_basic = fmt.Sprintf(`
27 | %s
28 |
29 | data "mimir_rule_group_alerting" "alert_1" {
30 | name = "${mimir_rule_group_alerting.alert_1.name}"
31 | namespace = "${mimir_rule_group_alerting.alert_1.namespace}"
32 | }
33 | `, testAccResourceRuleGroupAlerting_basic)
34 |
35 | func TestAccDataSourceRuleGroupAlerting_withOrgID(t *testing.T) {
36 | resource.Test(t, resource.TestCase{
37 | PreCheck: func() { testAccPreCheck(t) },
38 | ProviderFactories: testAccProviderFactories,
39 | Steps: []resource.TestStep{
40 | {
41 | Config: testAccDataSourceRuleGroupAlerting_withOrgID,
42 | Check: resource.ComposeTestCheckFunc(
43 | resource.TestCheckResourceAttr("data.mimir_rule_group_alerting.alert_1_withOrgID", "org_id", "another_tenant"),
44 | resource.TestCheckResourceAttr("data.mimir_rule_group_alerting.alert_1_withOrgID", "name", "alert_1_withOrgID"),
45 | resource.TestCheckResourceAttr("data.mimir_rule_group_alerting.alert_1_withOrgID", "namespace", "namespace_1"),
46 | ),
47 | },
48 | },
49 | })
50 | }
51 |
52 | var testAccDataSourceRuleGroupAlerting_withOrgID = fmt.Sprintf(`
53 | %s
54 |
55 | data "mimir_rule_group_alerting" "alert_1_withOrgID" {
56 | org_id = "${mimir_rule_group_alerting.alert_1_withOrgID.org_id}"
57 | name = "${mimir_rule_group_alerting.alert_1_withOrgID.name}"
58 | namespace = "${mimir_rule_group_alerting.alert_1_withOrgID.namespace}"
59 | }
60 | `, testAccResourceRuleGroupAlerting_withOrgID)
61 |
--------------------------------------------------------------------------------
/mimir/data_source_mimir_rule_group_recording_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8 | )
9 |
10 | func TestAccDataSourceRuleGroupRecording_basic(t *testing.T) {
11 | resource.Test(t, resource.TestCase{
12 | PreCheck: func() { testAccPreCheck(t) },
13 | ProviderFactories: testAccProviderFactories,
14 | Steps: []resource.TestStep{
15 | {
16 | Config: testAccDataSourceRuleGroupRecording_basic,
17 | Check: resource.ComposeTestCheckFunc(
18 | resource.TestCheckResourceAttr("data.mimir_rule_group_recording.record_1", "name", "record_1"),
19 | resource.TestCheckResourceAttr("data.mimir_rule_group_recording.record_1", "namespace", "namespace_1"),
20 | ),
21 | },
22 | },
23 | })
24 | }
25 |
26 | var testAccDataSourceRuleGroupRecording_basic = fmt.Sprintf(`
27 | %s
28 |
29 | data "mimir_rule_group_recording" "record_1" {
30 | name = "${mimir_rule_group_recording.record_1.name}"
31 | namespace = "${mimir_rule_group_recording.record_1.namespace}"
32 | }
33 | `, testAccResourceRuleGroupRecording_basic)
34 |
35 | func TestAccDataSourceRuleGroupRecording_withOrgID(t *testing.T) {
36 | resource.Test(t, resource.TestCase{
37 | PreCheck: func() { testAccPreCheck(t) },
38 | ProviderFactories: testAccProviderFactories,
39 | Steps: []resource.TestStep{
40 | {
41 | Config: testAccDataSourceRuleGroupRecording_withOrgID,
42 | Check: resource.ComposeTestCheckFunc(
43 | resource.TestCheckResourceAttr("data.mimir_rule_group_recording.record_1_withOrgID", "org_id", "another_tenant"),
44 | resource.TestCheckResourceAttr("data.mimir_rule_group_recording.record_1_withOrgID", "name", "record_1_withOrgID"),
45 | resource.TestCheckResourceAttr("data.mimir_rule_group_recording.record_1_withOrgID", "namespace", "namespace_1"),
46 | ),
47 | },
48 | },
49 | })
50 | }
51 |
52 | var testAccDataSourceRuleGroupRecording_withOrgID = fmt.Sprintf(`
53 | %s
54 |
55 | data "mimir_rule_group_recording" "record_1_withOrgID" {
56 | org_id = "${mimir_rule_group_recording.record_1_withOrgID.org_id}"
57 | name = "${mimir_rule_group_recording.record_1_withOrgID.name}"
58 | namespace = "${mimir_rule_group_recording.record_1_withOrgID.namespace}"
59 | }
60 | `, testAccResourceRuleGroupRecording_withOrgID)
61 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 | # Visit https://goreleaser.com for documentation on how to customize this
4 | # behavior.
5 | before:
6 | hooks:
7 | # this is just an example and not a requirement for provider building/publishing
8 | - go mod tidy
9 | builds:
10 | - env:
11 | # goreleaser does not work with CGO, it could also complicate
12 | # usage by users in CI/CD systems like Terraform Cloud where
13 | # they are unable to install libraries.
14 | - CGO_ENABLED=0
15 | mod_timestamp: '{{ .CommitTimestamp }}'
16 | flags:
17 | - -trimpath
18 | ldflags:
19 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
20 | goos:
21 | - freebsd
22 | - windows
23 | - linux
24 | - darwin
25 | goarch:
26 | - amd64
27 | - '386'
28 | - arm
29 | - arm64
30 | ignore:
31 | - goos: darwin
32 | goarch: '386'
33 | - goos: windows
34 | goarch: 'arm'
35 | binary: '{{ .ProjectName }}_v{{ .Version }}'
36 | archives:
37 | - format: zip
38 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
39 | checksum:
40 | extra_files:
41 | - glob: 'terraform-registry-manifest.json'
42 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
43 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
44 | algorithm: sha256
45 | signs:
46 | - artifacts: checksum
47 | args:
48 | # if you are using this in a GitHub action or some other automated pipeline, you
49 | # need to pass the batch flag to indicate its not interactive.
50 | - "--batch"
51 | - "--local-user"
52 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
53 | - "--output"
54 | - "${signature}"
55 | - "--detach-sign"
56 | - "${artifact}"
57 | release:
58 | extra_files:
59 | - glob: 'terraform-registry-manifest.json'
60 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json'
61 | # If you want to manually examine the release before its live, uncomment this line:
62 | # draft: true
63 | changelog:
64 | disable: false
65 | sort: asc
66 | groups:
67 | - title: Features
68 | regexp: "^.*feat[(\\w)]*:+.*$"
69 | order: 0
70 | - title: 'Enhancements'
71 | regexp: "^.*enhancement[(\\w)]*:+.*$"
72 | order: 1
73 | - title: 'Bug fixes'
74 | regexp: "^.*fix[(\\w)]*:+.*$"
75 | order: 2
76 |
77 | filters:
78 | # Commit messages matching the regexp listed here will be removed from
79 | exclude:
80 | - '^doc:'
81 | - '^ci:'
82 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 | paths-ignore:
7 | - 'README.md'
8 | - 'CHANGELOG.md'
9 | - 'website/*'
10 | push:
11 | branches: [ main ]
12 | paths-ignore:
13 | - 'README.md'
14 | - 'CHANGELOG.md'
15 | - 'website/*'
16 |
17 | jobs:
18 | build:
19 | name: Build
20 | runs-on: ubuntu-latest
21 | timeout-minutes: 5
22 | steps:
23 |
24 | - name: Set up Go
25 | uses: actions/setup-go@v3
26 | with:
27 | go-version: '1.23'
28 | id: go
29 |
30 | - name: Check out code into the Go module directory
31 | uses: actions/checkout@v3
32 |
33 | - name: Run linters
34 | uses: golangci/golangci-lint-action@v7
35 | with:
36 | version: v2.1.6
37 | args: --timeout 10m
38 |
39 | - name: Generate
40 | run: make generate
41 |
42 | - name: Confirm no diff
43 | run: |
44 | git diff --compact-summary --exit-code || \
45 | (echo "*** Unexpected differences after code generation. Run 'make generate' and commit."; exit 1)
46 |
47 | - name: Build
48 | run: make build
49 |
50 | test:
51 | name: 'Acc. Tests (OS: ${{ matrix.os }} / TF: ${{ matrix.terraform }})'
52 | needs: build
53 | runs-on: ${{ matrix.os }}
54 | timeout-minutes: 15
55 | strategy:
56 | fail-fast: false
57 | matrix:
58 | os:
59 | - ubuntu-latest
60 | terraform:
61 | - '0.12.*'
62 | - '1.5.*'
63 | mimir-version:
64 | - '2.15.3'
65 | - '2.16.1'
66 | - '2.17.1'
67 | steps:
68 |
69 | - name: Setup Go
70 | uses: actions/setup-go@v3
71 | with:
72 | go-version: '1.23'
73 | check-latest: true
74 |
75 | - name: Check out code
76 | uses: actions/checkout@v3
77 |
78 | - name: Setup Terraform ${{ matrix.terraform }}
79 | uses: hashicorp/setup-terraform@v2
80 | with:
81 | terraform_version: ${{ matrix.terraform }}
82 | terraform_wrapper: false
83 |
84 | - name: Setup Grafana Mimir from ${{ matrix.mimir-version }}
85 | run: |
86 | docker pull grafana/mimir:${{ matrix.mimir-version }}
87 | docker run \
88 | -d \
89 | -p 8080:8080 \
90 | --name mimir \
91 | grafana/mimir:${{ matrix.mimir-version }} \
92 | -target=alertmanager,ruler \
93 | -tenant-federation.enabled=true \
94 | -ruler.tenant-federation.enabled=true
95 |
96 | - name: Run acceptance test
97 | run: MIMIR_VERSION=${{ matrix.mimir-version }} make testacc
98 |
--------------------------------------------------------------------------------
/docs/resources/rules.md:
--------------------------------------------------------------------------------
1 | ---
2 | # generated by https://github.com/hashicorp/terraform-plugin-docs
3 | page_title: "mimir_rules Resource - terraform-provider-mimir"
4 | subcategory: ""
5 | description: |-
6 | Manages multiple Grafana Mimir rule groups within a namespace.
7 | This resource is designed to handle YAML files containing multiple rule groups,
8 | such as those exported from mimirtool or monitoring mixins. Each rule group
9 | is managed individually via the Mimir API, but they are tracked together as
10 | a single Terraform resource for easier bulk management.
11 | ---
12 |
13 | # mimir_rules (Resource)
14 |
15 | Manages multiple Grafana Mimir rule groups within a namespace.
16 | This resource is designed to handle YAML files containing multiple rule groups,
17 | such as those exported from mimirtool or monitoring mixins. Each rule group
18 | is managed individually via the Mimir API, but they are tracked together as
19 | a single Terraform resource for easier bulk management.
20 |
21 |
22 |
23 |
24 | ## Schema
25 |
26 | ### Required
27 |
28 | - `namespace` (String) The namespace for the rule groups
29 |
30 | ### Optional
31 |
32 | - `content` (String) YAML content containing rule groups. Mutually exclusive with 'content_file'.
33 | - `content_file` (String) Path to YAML file containing rule groups. Mutually exclusive with 'content'.
34 | - `ignore_groups` (Set of String) List of rule group names to ignore from the content. Useful when you want to manage most groups but exclude specific ones.
35 | - `only_groups` (Set of String) Explicit list of rule group names to manage. If not specified, all groups in the content will be managed. Use this to manage only specific groups from a larger YAML file.
36 | - `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
37 |
38 | ### Read-Only
39 |
40 | - `content_hash` (String) Hash of the rule configuration content
41 | - `groups` (List of Object) Details of all managed rule groups (see [below for nested schema](#nestedatt--groups))
42 | - `groups_count` (Number) Number of rule groups managed by this resource
43 | - `id` (String) The ID of this resource.
44 | - `managed_groups` (List of String) List of rule group names actually managed by this resource
45 | - `rule_names` (List of String) List of all rule names actually managed by this resource
46 | - `total_rules` (Number) Total number of rules across all managed groups
47 |
48 |
49 | ### Nested Schema for `groups`
50 |
51 | Read-Only:
52 |
53 | - `alerting_rules_count` (Number)
54 | - `interval` (String)
55 | - `name` (String)
56 | - `recording_rules_count` (Number)
57 | - `rules_count` (Number)
58 |
59 |
60 |
--------------------------------------------------------------------------------
/mimir/provider_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "sync"
6 | "testing"
7 |
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
10 | )
11 |
12 | var (
13 | mimirOrgID = getSetEnv("MIMIR_ORG_ID", "mytenant")
14 | mimirURI = getSetEnv("MIMIR_URI", "http://localhost:8080")
15 | mimirRulerURI = getSetEnv("MIMIR_RULER_URI", "http://localhost:8080/prometheus")
16 | mimirAlertmanagerURI = getSetEnv("MIMIR_ALERTMANAGER_URI", "http://localhost:8080")
17 | )
18 |
19 | // testAccProviderFactories is a static map containing only the main provider instance
20 | var testAccProviderFactories map[string]func() (*schema.Provider, error)
21 |
22 | // testAccProvider is the "main" provider instance
23 | //
24 | // This Provider can be used in testing code for API calls without requiring
25 | // the use of saving and referencing specific ProviderFactories instances.
26 | //
27 | // testAccPreCheck(t) must be called before using this provider instance.
28 | var testAccProvider *schema.Provider
29 |
30 | var testAccProviders map[string]*schema.Provider
31 |
32 | // testAccProviderConfigure ensures testAccProvider is only configured once
33 | //
34 | // The testAccPreCheck(t) function is invoked for every test and this prevents
35 | // extraneous reconfiguration to the same values each time. However, this does
36 | // not prevent reconfiguration that may happen should the address of
37 | // testAccProvider be errantly reused in ProviderFactories.
38 | var testAccProviderConfigure sync.Once
39 |
40 | func init() {
41 | testAccProvider = Provider("testacc")()
42 | testAccProviders = map[string]*schema.Provider{
43 | "mimir": testAccProvider,
44 | }
45 |
46 | // Always allocate a new provider instance each invocation, otherwise gRPC
47 | // ProviderConfigure() can overwrite configuration during concurrent testing.
48 | testAccProviderFactories = map[string]func() (*schema.Provider, error){
49 | "mimir": func() (*schema.Provider, error) {
50 | return testAccProvider, nil
51 | },
52 | }
53 | }
54 |
55 | func TestProvider(t *testing.T) {
56 | if err := Provider("dev")().InternalValidate(); err != nil {
57 | t.Fatalf("err: %s", err)
58 | }
59 | }
60 |
61 | // testAccPreCheck verifies required provider testing configuration. It should
62 | // be present in every acceptance test.
63 | //
64 | // These verifications and configuration are preferred at this level to prevent
65 | // provider developers from experiencing less clear errors for every test.
66 | func testAccPreCheck(t *testing.T) {
67 | testAccProviderConfigure.Do(func() {
68 | // Since we are outside the scope of the Terraform configuration we must
69 | // call Configure() to properly initialize the provider configuration.
70 | err := testAccProvider.Configure(context.Background(), terraform.NewResourceConfigRaw(nil))
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 | })
75 | }
76 |
--------------------------------------------------------------------------------
/mimir/data_source_mimir_alertmanager_config.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "strings"
6 |
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | "gopkg.in/yaml.v3"
10 | )
11 |
12 | func dataSourcemimirAlertmanagerConfig() *schema.Resource {
13 | return &schema.Resource{
14 | ReadContext: dataSourcemimirAlertmanagerConfigRead,
15 | Schema: dataSourceMimirAlertmanagerConfigSchemaV1(),
16 | }
17 | }
18 |
19 | func dataSourcemimirAlertmanagerConfigRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
20 | client := meta.(*apiClient)
21 | name := d.Get("name").(string)
22 | orgID := d.Get("org_id").(string)
23 |
24 | headers := make(map[string]string)
25 | if orgID != "" {
26 | headers["X-Scope-OrgID"] = orgID
27 | }
28 | resp, err := client.sendRequest("alertmanager", "GET", apiAlertsPath, "", headers)
29 | baseMsg := "Cannot read alertmanager config"
30 | err = handleHTTPError(err, baseMsg)
31 | if err != nil {
32 | if strings.Contains(err.Error(), "response code '404'") {
33 | d.SetId("")
34 | return nil
35 | }
36 | return diag.FromErr(err)
37 | }
38 |
39 | d.SetId(client.headers["X-Scope-OrgID"])
40 |
41 | if name == "" {
42 | name = client.headers["X-Scope-OrgID"]
43 | }
44 |
45 | if err := d.Set("name", name); err != nil {
46 | return diag.Errorf("error setting item: %v", err)
47 | }
48 |
49 | var alertmanagerUserConf alertmanagerUserConfig
50 | err = yaml.Unmarshal([]byte(resp), &alertmanagerUserConf)
51 | if err != nil {
52 | return diag.FromErr(err)
53 | }
54 |
55 | var alertmanagerConf alertmanagerConfig
56 | err = yaml.Unmarshal([]byte(alertmanagerUserConf.AlertmanagerConfig), &alertmanagerConf)
57 | if err != nil {
58 | return diag.FromErr(err)
59 | }
60 |
61 | if alertmanagerConf.Global != nil {
62 | if err := d.Set("global", flattenGlobalConfig(alertmanagerConf.Global)); err != nil {
63 | return diag.Errorf("error setting item: %v", err)
64 | }
65 | }
66 | if err := d.Set("time_interval", flattenMuteTimeIntervalConfig(alertmanagerConf.MuteTimeIntervals)); err != nil {
67 | return diag.Errorf("error setting item: %v", err)
68 | }
69 | if err := d.Set("inhibit_rule", flattenInhibitRuleConfig(alertmanagerConf.InhibitRules)); err != nil {
70 | return diag.Errorf("error setting item: %v", err)
71 | }
72 | if err := d.Set("receiver", flattenReceiverConfig(alertmanagerConf.Receivers)); err != nil {
73 | return diag.Errorf("error setting item: %v", err)
74 | }
75 | if err := d.Set("route", flattenRouteConfig(alertmanagerConf.Route)); err != nil {
76 | return diag.Errorf("error setting item: %v", err)
77 | }
78 | if err := d.Set("templates", alertmanagerConf.Templates); err != nil {
79 | return diag.Errorf("error setting item: %v", err)
80 | }
81 | if err := d.Set("templates_files", alertmanagerUserConf.TemplateFiles); err != nil {
82 | return diag.Errorf("error setting item: %v", err)
83 | }
84 |
85 | return nil
86 | }
87 |
--------------------------------------------------------------------------------
/mimir/data_source_mimir_alertmanager_config_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8 | )
9 |
10 | func TestAccDataSourceAlertmanagerConfig_basic(t *testing.T) {
11 | resource.Test(t, resource.TestCase{
12 | PreCheck: func() { testAccPreCheck(t) },
13 | ProviderFactories: testAccProviderFactories,
14 | Steps: []resource.TestStep{
15 | {
16 | Config: testAccDataSourceAlertmanagerConfig_basic,
17 | Check: resource.ComposeTestCheckFunc(
18 | resource.TestCheckResourceAttr("data.mimir_alertmanager_config.mytenant", "route.0.group_by.0", "..."),
19 | resource.TestCheckResourceAttr("data.mimir_alertmanager_config.mytenant", "route.0.group_wait", "30s"),
20 | resource.TestCheckResourceAttr("data.mimir_alertmanager_config.mytenant", "route.0.group_interval", "5m"),
21 | resource.TestCheckResourceAttr("data.mimir_alertmanager_config.mytenant", "route.0.repeat_interval", "1h"),
22 | resource.TestCheckResourceAttr("data.mimir_alertmanager_config.mytenant", "route.0.receiver", "pagerduty"),
23 | resource.TestCheckResourceAttr("data.mimir_alertmanager_config.mytenant", "receiver.0.name", "pagerduty"),
24 | resource.TestCheckResourceAttr("data.mimir_alertmanager_config.mytenant", "receiver.0.pagerduty_configs.0.routing_key", "secret"),
25 | resource.TestCheckResourceAttr("data.mimir_alertmanager_config.mytenant", "receiver.0.pagerduty_configs.0.details.environment", "dev"),
26 | ),
27 | },
28 | },
29 | })
30 | }
31 |
32 | var testAccDataSourceAlertmanagerConfig_basic = fmt.Sprintf(`
33 | %s
34 |
35 | data "mimir_alertmanager_config" "mytenant" {
36 | name = "${mimir_alertmanager_config.mytenant.id}"
37 | }
38 | `, testAccResourceAlertmanagerConfig_basic)
39 |
40 | func TestAccDataSourceAlertmanagerConfig_WithOrgID(t *testing.T) {
41 | // Init client
42 | client, err := NewAPIClient(setupClient())
43 | if err != nil {
44 | t.Fatal(err)
45 | }
46 |
47 | resource.Test(t, resource.TestCase{
48 | PreCheck: func() { testAccPreCheck(t) },
49 | ProviderFactories: testAccProviderFactories,
50 | CheckDestroy: testAccCheckMimirAlertmanagerConfigDestroy,
51 | Steps: []resource.TestStep{
52 | {
53 | Config: testAccDataSourceAlertmanagerConfig_WithOrgID,
54 | Check: resource.ComposeTestCheckFunc(
55 | testAccCheckMimirAlertmanagerConfigExists("mimir_alertmanager_config.another_tenant", client),
56 | resource.TestCheckResourceAttr("mimir_alertmanager_config.another_tenant", "org_id", "another_tenant"),
57 | resource.TestCheckResourceAttr("mimir_alertmanager_config.another_tenant", "route.0.group_by.0", "..."),
58 | resource.TestCheckResourceAttr("mimir_alertmanager_config.another_tenant", "route.0.group_wait", "30s"),
59 | resource.TestCheckResourceAttr("mimir_alertmanager_config.another_tenant", "route.0.group_interval", "5m"),
60 | resource.TestCheckResourceAttr("mimir_alertmanager_config.another_tenant", "route.0.repeat_interval", "1h"),
61 | resource.TestCheckResourceAttr("mimir_alertmanager_config.another_tenant", "route.0.receiver", "test"),
62 | resource.TestCheckResourceAttr("mimir_alertmanager_config.another_tenant", "receiver.0.name", "test"),
63 | ),
64 | },
65 | },
66 | })
67 | }
68 |
69 | var testAccDataSourceAlertmanagerConfig_WithOrgID = fmt.Sprintf(`
70 | %s
71 |
72 | data "mimir_alertmanager_config" "another_tenant" {
73 | org_id = "another_tenant"
74 | name = "${mimir_alertmanager_config.another_tenant.id}"
75 | }
76 | `, testAccResourceAlertmanagerConfig_WithOrgID)
77 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: ""
3 | page_title: "Provider: Mimir"
4 | description: |-
5 | The Mimir provider provides configuration management resources for Grafana Mimir.
6 | ---
7 |
8 | # Mimir Provider
9 |
10 | The Mimir provider provides configuration management resources for
11 | [Grafana Mimir](https://grafana.com/oss/mimir/).
12 |
13 | ## Example Usage
14 |
15 | ### Creating a Mimir provider
16 |
17 | ```terraform
18 | provider "mimir" {
19 | ruler_uri = "http://127.0.0.1:8080/prometheus"
20 | alertmanager_uri = "http://127.0.0.1:8080"
21 | distributor_uri = "http://127.0.0.1:8080/distributor"
22 | org_id = "mytenant"
23 | }
24 | ```
25 |
26 | ### Creating a Mimir provider with basic auth
27 |
28 | ```terraform
29 | provider "mimir" {
30 | ruler_uri = "http://127.0.0.1:8080/prometheus"
31 | alertmanager_uri = "http://127.0.0.1:8080"
32 | distributor_uri = "http://127.0.0.1:8080/distributor"
33 | org_id = "mytenant"
34 | username = "user"
35 | password = "password"
36 | }
37 | ```
38 |
39 | ### Creating a Mimir provider with token auth
40 |
41 | ```terraform
42 | provider "mimir" {
43 | ruler_uri = "http://127.0.0.1:8080/prometheus"
44 | alertmanager_uri = "http://127.0.0.1:8080"
45 | distributor_uri = "http://127.0.0.1:8080/distributor"
46 | org_id = "mytenant"
47 | token = "supersecrettoken"
48 | }
49 | ```
50 |
51 | ### Creating a Mimir provider with custom headers
52 |
53 | ```terraform
54 | provider "mimir" {
55 | ruler_uri = "http://127.0.0.1:8080/prometheus"
56 | alertmanager_uri = "http://127.0.0.1:8080"
57 | distributor_uri = "http://127.0.0.1:8080/distributor"
58 | org_id = "mytenant"
59 | header = {
60 | "Custom-Auth" = "Custom value"
61 | }
62 | }
63 | ```
64 |
65 |
66 | ## Schema
67 |
68 | ### Required
69 |
70 | - `org_id` (String) The default organization id to operate on within mimir. For resources that have an org_id attribute, the resource-level attribute has priority. May alternatively be set via the MIMIR_ORG_ID environment variable.
71 |
72 | ### Optional
73 |
74 | - `alertmanager_read_delay_after_change` (String) When set, add a delay (time duration) to read the alertmanager config after a change.
75 | - `alertmanager_read_retry_after_change` (Number) Max retries to read the alertmanager config after a change.
76 | - `alertmanager_uri` (String) mimir alertmanager base url
77 | - `ca` (String) Client ca (filepath or inline) for TLS client authentication.
78 | - `cert` (String) Client cert (filepath or inline) for TLS client authentication.
79 | - `debug` (Boolean) Enable debug mode to trace requests executed.
80 | - `distributor_uri` (String) mimir distributor base url
81 | - `format_promql_expr` (Boolean) Enable the formatting of PromQL expression.
82 | - `headers` (Map of String) A map of header names and values to set on all outbound requests.
83 | - `insecure` (Boolean) When using https, this disables TLS verification of the host.
84 | - `key` (String) Client key (filepath or inline) for TLS client authentication.
85 | - `overwrite_alertmanager_config` (Boolean) Overwrite the current alertmanager config on create.
86 | - `overwrite_rule_group_config` (Boolean) Overwrite the current rule group (alerting/recording) config on create.
87 | - `password` (String) When set, will use this password for BASIC auth to the API.
88 | - `proxy_url` (String) URL to the proxy to be used for all API requests
89 | - `rule_group_read_delay_after_change` (String) When set, add a delay (time duration) to read the rule group after a change.
90 | - `rule_group_read_retry_after_change` (Number) Max retries to read the rule group after a change.
91 | - `ruler_uri` (String) mimir ruler base url
92 | - `timeout` (Number) When set, will cause requests taking longer than this time (in seconds) to be aborted.
93 | - `token` (String) When set, will use this token for Bearer auth to the API.
94 | - `uri` (String) mimir base url
95 | - `username` (String) When set, will use this username for BASIC auth to the API.
96 |
--------------------------------------------------------------------------------
/mimir/data_source_mimir_rule_group_alerting.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10 | "gopkg.in/yaml.v3"
11 | )
12 |
13 | func dataSourcemimirRuleGroupAlerting() *schema.Resource {
14 | return &schema.Resource{
15 | ReadContext: dataSourcemimirRuleGroupAlertingRead,
16 |
17 | Schema: map[string]*schema.Schema{
18 | "org_id": {
19 | Type: schema.TypeString,
20 | ForceNew: true,
21 | Optional: true,
22 | Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
23 | },
24 | "namespace": {
25 | Type: schema.TypeString,
26 | Description: "Alerting Rule group namespace",
27 | ForceNew: true,
28 | Optional: true,
29 | Default: "default",
30 | },
31 | "name": {
32 | Type: schema.TypeString,
33 | Description: "Alerting Rule group name",
34 | Required: true,
35 | ForceNew: true,
36 | ValidateFunc: validateGroupRuleName,
37 | },
38 | "source_tenants": {
39 | Type: schema.TypeList,
40 | Computed: true,
41 | Description: "Allows aggregating data from multiple tenants while evaluating a rule group.",
42 | Elem: &schema.Schema{Type: schema.TypeString},
43 | },
44 | "rule": {
45 | Type: schema.TypeList,
46 | Computed: true,
47 | Elem: &schema.Resource{
48 | Schema: map[string]*schema.Schema{
49 | "alert": {
50 | Type: schema.TypeString,
51 | Description: "Alerting Rule name",
52 | Computed: true,
53 | },
54 | "expr": {
55 | Type: schema.TypeString,
56 | Description: "Alerting Rule query",
57 | Computed: true,
58 | },
59 | "for": {
60 | Type: schema.TypeString,
61 | Description: "Alerting Rule duration",
62 | Computed: true,
63 | },
64 | "annotations": {
65 | Type: schema.TypeMap,
66 | Description: "Alerting Rule annotations",
67 | Elem: &schema.Schema{Type: schema.TypeString},
68 | Computed: true,
69 | },
70 | "labels": {
71 | Type: schema.TypeMap,
72 | Description: "Alerting Rule labels",
73 | Elem: &schema.Schema{Type: schema.TypeString},
74 | Computed: true,
75 | },
76 | },
77 | },
78 | },
79 | }, /* End schema */
80 |
81 | }
82 | }
83 |
84 | func dataSourcemimirRuleGroupAlertingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
85 | client := meta.(*apiClient)
86 | name := d.Get("name").(string)
87 | namespace := d.Get("namespace").(string)
88 | orgID := d.Get("org_id").(string)
89 |
90 | id := fmt.Sprintf("%s/%s", namespace, name)
91 |
92 | headers := make(map[string]string)
93 | if orgID != "" {
94 | headers["X-Scope-OrgID"] = orgID
95 | id = fmt.Sprintf("%s/%s/%s", orgID, namespace, name)
96 | }
97 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
98 | jobraw, err := client.sendRequest("ruler", "GET", path, "", headers)
99 |
100 | baseMsg := fmt.Sprintf("Cannot read alerting rule group '%s' -", name)
101 | err = handleHTTPError(err, baseMsg)
102 | if err != nil {
103 | if strings.Contains(err.Error(), "response code '404'") {
104 | d.SetId("")
105 | return nil
106 | }
107 | return diag.FromErr(err)
108 | }
109 |
110 | d.SetId(id)
111 |
112 | var data alertingRuleGroup
113 | err = yaml.Unmarshal([]byte(jobraw), &data)
114 | if err != nil {
115 | return diag.FromErr(fmt.Errorf("unable to decode alerting rule group '%s' data: %v", name, err))
116 | }
117 | if err := d.Set("rule", flattenAlertingRules(data.Rules)); err != nil {
118 | return diag.FromErr(err)
119 | }
120 | if err := d.Set("source_tenants", data.SourceTenants); err != nil {
121 | return diag.FromErr(err)
122 | }
123 |
124 | return nil
125 | }
126 |
--------------------------------------------------------------------------------
/mimir/data_source_mimir_distributor_tenant_stats.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "math"
8 | "strings"
9 |
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13 | )
14 |
15 | type Stats struct {
16 | User string `json:"UserID"`
17 | Series int `json:"numSeries"`
18 | TotalIngestRate float64 `json:"ingestionRate"`
19 | APIIngestRate float64 `json:"APIIngestionRate"`
20 | RuleIngestRate float64 `json:"RuleIngestionRate"`
21 | }
22 |
23 | func dataSourcemimirDistributorTenantStats() *schema.Resource {
24 | return &schema.Resource{
25 | ReadContext: dataSourcemimirDistributorTenantStatsRead,
26 |
27 | Schema: map[string]*schema.Schema{
28 | "org_id": {
29 | Type: schema.TypeString,
30 | ForceNew: true,
31 | Optional: true,
32 | Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
33 | },
34 | "user": {
35 | Type: schema.TypeString,
36 | Description: "Query specific user stats, if not specified, all users are returned",
37 | ForceNew: true,
38 | Optional: true,
39 | },
40 | "stats": {
41 | Type: schema.TypeList,
42 | Description: "Stats list, does not account for replication factor",
43 | Computed: true,
44 | Elem: &schema.Resource{
45 | Schema: map[string]*schema.Schema{
46 | "user": {
47 | Type: schema.TypeString,
48 | Computed: true,
49 | },
50 | "series": {
51 | Type: schema.TypeInt,
52 | Computed: true,
53 | },
54 | "total_ingest_rate": {
55 | Type: schema.TypeFloat,
56 | Computed: true,
57 | },
58 | "api_ingest_rate": {
59 | Type: schema.TypeFloat,
60 | Computed: true,
61 | },
62 | "rule_ingest_rate": {
63 | Type: schema.TypeFloat,
64 | Computed: true,
65 | },
66 | },
67 | },
68 | },
69 | }, /* End schema */
70 |
71 | }
72 | }
73 |
74 | func dataSourcemimirDistributorTenantStatsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
75 | client := meta.(*apiClient)
76 | user := d.Get("user").(string)
77 | orgID := d.Get("org_id").(string)
78 |
79 | headers := map[string]string{"Accept": "json"}
80 | if orgID != "" {
81 | headers["X-Scope-OrgID"] = orgID
82 | }
83 |
84 | jobraw, err := client.sendRequest("distributor", "GET", "/all_user_stats", "", headers)
85 |
86 | baseMsg := "Cannot read user stats"
87 | err = handleHTTPError(err, baseMsg)
88 | if err != nil {
89 | if strings.Contains(err.Error(), "response code '404'") {
90 | d.SetId("")
91 | return nil
92 | }
93 | return diag.FromErr(err)
94 | }
95 |
96 | // unmarshal the json using json/encoding into Stats struct
97 | var output []Stats
98 | err = json.Unmarshal([]byte(jobraw), &output)
99 | if err != nil {
100 | return diag.FromErr(fmt.Errorf("unable to unmarshal json: %v", err))
101 | }
102 |
103 | var stats []map[string]interface{}
104 | // transform the output into a list of maps
105 | for _, stat := range output {
106 | // trim float to 2 decimal places
107 | stat.TotalIngestRate = math.Round(stat.TotalIngestRate*100) / 100
108 | stat.APIIngestRate = math.Round(stat.APIIngestRate*100) / 100
109 | stat.RuleIngestRate = math.Round(stat.RuleIngestRate*100) / 100
110 | stats = append(stats, map[string]interface{}{
111 | "user": stat.User,
112 | "series": stat.Series,
113 | "total_ingest_rate": stat.TotalIngestRate,
114 | "api_ingest_rate": stat.APIIngestRate,
115 | "rule_ingest_rate": stat.RuleIngestRate,
116 | })
117 | }
118 |
119 | // if user is specified then filter the stats
120 | if user != "" {
121 | var filteredStats []map[string]interface{}
122 | for _, stat := range stats {
123 | if stat["user"] == user {
124 | filteredStats = append(filteredStats, stat)
125 | }
126 | }
127 | stats = filteredStats
128 | }
129 |
130 | if err := d.Set("stats", stats); err != nil {
131 | return diag.FromErr(err)
132 | }
133 |
134 | d.SetId(id.UniqueId())
135 |
136 | return nil
137 | }
138 |
--------------------------------------------------------------------------------
/mimir/data_source_mimir_rule_group_recording.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10 | "gopkg.in/yaml.v3"
11 | )
12 |
13 | func dataSourcemimirRuleGroupRecording() *schema.Resource {
14 | return &schema.Resource{
15 | ReadContext: dataSourcemimirRuleGroupRecordingRead,
16 |
17 | Schema: map[string]*schema.Schema{
18 | "org_id": {
19 | Type: schema.TypeString,
20 | ForceNew: true,
21 | Optional: true,
22 | Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
23 | },
24 | "namespace": {
25 | Type: schema.TypeString,
26 | Description: "Recording Rule group namespace",
27 | ForceNew: true,
28 | Optional: true,
29 | Default: "default",
30 | },
31 | "name": {
32 | Type: schema.TypeString,
33 | Description: "Recording Rule group name",
34 | Required: true,
35 | ForceNew: true,
36 | ValidateFunc: validateGroupRuleName,
37 | },
38 | "interval": {
39 | Type: schema.TypeString,
40 | Description: "Recording Rule group interval",
41 | Computed: true,
42 | },
43 | "query_offset": {
44 | Type: schema.TypeString,
45 | Description: "The duration by which to delay the execution of the recording rule.",
46 | Computed: true,
47 | },
48 | "evaluation_delay": {
49 | Type: schema.TypeString,
50 | Description: "**Deprecated** The duration by which to delay the execution of the recording rule.",
51 | Deprecated: "With Mimir >= 2.13, replaced by query_offset. This attribute will be removed when it is no longer supported in Mimir.",
52 | Computed: true,
53 | },
54 | "source_tenants": {
55 | Type: schema.TypeList,
56 | Computed: true,
57 | Description: "Allows aggregating data from multiple tenants while evaluating a rule group.",
58 | Elem: &schema.Schema{Type: schema.TypeString},
59 | },
60 | "rule": {
61 | Type: schema.TypeList,
62 | Computed: true,
63 | Elem: &schema.Resource{
64 | Schema: map[string]*schema.Schema{
65 | "record": {
66 | Type: schema.TypeString,
67 | Computed: true,
68 | },
69 | "expr": {
70 | Type: schema.TypeString,
71 | Computed: true,
72 | },
73 | "labels": {
74 | Type: schema.TypeMap,
75 | Description: "Recording Rule labels",
76 | Elem: &schema.Schema{Type: schema.TypeString},
77 | Computed: true,
78 | },
79 | },
80 | },
81 | },
82 | }, /* End schema */
83 |
84 | }
85 | }
86 |
87 | func dataSourcemimirRuleGroupRecordingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
88 | client := meta.(*apiClient)
89 | name := d.Get("name").(string)
90 | namespace := d.Get("namespace").(string)
91 | orgID := d.Get("org_id").(string)
92 |
93 | id := fmt.Sprintf("%s/%s", namespace, name)
94 |
95 | headers := make(map[string]string)
96 | if orgID != "" {
97 | headers["X-Scope-OrgID"] = orgID
98 | id = fmt.Sprintf("%s/%s/%s", orgID, namespace, name)
99 | }
100 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
101 | jobraw, err := client.sendRequest("ruler", "GET", path, "", headers)
102 |
103 | baseMsg := fmt.Sprintf("Cannot read recording rule group '%s' -", name)
104 | err = handleHTTPError(err, baseMsg)
105 | if err != nil {
106 | if strings.Contains(err.Error(), "response code '404'") {
107 | d.SetId("")
108 | return nil
109 | }
110 | return diag.FromErr(err)
111 | }
112 |
113 | d.SetId(id)
114 |
115 | var data recordingRuleGroup
116 | err = yaml.Unmarshal([]byte(jobraw), &data)
117 | if err != nil {
118 | return diag.FromErr(fmt.Errorf("unable to decode recording rule group '%s' data: %v", name, err))
119 | }
120 | if err := d.Set("rule", flattenRecordingRules(data.Rules)); err != nil {
121 | return diag.FromErr(err)
122 | }
123 | if err := d.Set("interval", data.Interval); err != nil {
124 | return diag.FromErr(err)
125 | }
126 | if err := d.Set("query_offset", data.QueryOffset); err != nil {
127 | return diag.FromErr(err)
128 | }
129 | if err := d.Set("evaluation_delay", data.EvaluationDelay); err != nil {
130 | return diag.FromErr(err)
131 | }
132 | if err := d.Set("source_tenants", data.SourceTenants); err != nil {
133 | return diag.FromErr(err)
134 | }
135 |
136 | return nil
137 | }
138 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/fgouteroux/terraform-provider-mimir
2 |
3 | go 1.23.0
4 |
5 | require (
6 | github.com/hashicorp/go-version v1.6.0
7 | github.com/hashicorp/terraform-plugin-docs v0.19.0
8 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.27.0
9 | github.com/prometheus/alertmanager v0.27.0
10 | github.com/prometheus/common v0.54.0
11 | github.com/prometheus/prometheus v0.53.0
12 | gopkg.in/yaml.v3 v3.0.1
13 | )
14 |
15 | require (
16 | github.com/BurntSushi/toml v1.2.1 // indirect
17 | github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect
18 | github.com/Masterminds/goutils v1.1.1 // indirect
19 | github.com/Masterminds/semver/v3 v3.2.0 // indirect
20 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect
21 | github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
22 | github.com/agext/levenshtein v1.2.2 // indirect
23 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
24 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
25 | github.com/armon/go-radix v1.0.0 // indirect
26 | github.com/aws/aws-sdk-go v1.53.16 // indirect
27 | github.com/beorn7/perks v1.0.1 // indirect
28 | github.com/bgentry/speakeasy v0.1.0 // indirect
29 | github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
30 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
31 | github.com/cloudflare/circl v1.3.7 // indirect
32 | github.com/dennwc/varint v1.0.0 // indirect
33 | github.com/fatih/color v1.16.0 // indirect
34 | github.com/go-kit/log v0.2.1 // indirect
35 | github.com/go-logfmt/logfmt v0.6.0 // indirect
36 | github.com/golang/protobuf v1.5.4 // indirect
37 | github.com/google/go-cmp v0.6.0 // indirect
38 | github.com/google/uuid v1.6.0 // indirect
39 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
40 | github.com/hashicorp/cli v1.1.6 // indirect
41 | github.com/hashicorp/errwrap v1.1.0 // indirect
42 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect
43 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
44 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
45 | github.com/hashicorp/go-hclog v1.5.0 // indirect
46 | github.com/hashicorp/go-multierror v1.1.1 // indirect
47 | github.com/hashicorp/go-plugin v1.4.10 // indirect
48 | github.com/hashicorp/go-uuid v1.0.3 // indirect
49 | github.com/hashicorp/hc-install v0.6.4 // indirect
50 | github.com/hashicorp/hcl/v2 v2.17.0 // indirect
51 | github.com/hashicorp/logutils v1.0.0 // indirect
52 | github.com/hashicorp/terraform-exec v0.20.0 // indirect
53 | github.com/hashicorp/terraform-json v0.21.0 // indirect
54 | github.com/hashicorp/terraform-plugin-go v0.16.0 // indirect
55 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
56 | github.com/hashicorp/terraform-registry-address v0.2.1 // indirect
57 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect
58 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
59 | github.com/huandu/xstrings v1.3.3 // indirect
60 | github.com/imdario/mergo v0.3.16 // indirect
61 | github.com/jmespath/go-jmespath v0.4.0 // indirect
62 | github.com/jpillora/backoff v1.0.0 // indirect
63 | github.com/mattn/go-colorable v0.1.13 // indirect
64 | github.com/mattn/go-isatty v0.0.20 // indirect
65 | github.com/mattn/go-runewidth v0.0.9 // indirect
66 | github.com/mitchellh/copystructure v1.2.0 // indirect
67 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect
68 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect
69 | github.com/mitchellh/mapstructure v1.5.0 // indirect
70 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
71 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
72 | github.com/oklog/run v1.1.0 // indirect
73 | github.com/posener/complete v1.2.3 // indirect
74 | github.com/prometheus/client_golang v1.19.1 // indirect
75 | github.com/prometheus/client_model v0.6.1 // indirect
76 | github.com/prometheus/common/sigv4 v0.1.0 // indirect
77 | github.com/prometheus/procfs v0.12.0 // indirect
78 | github.com/shopspring/decimal v1.3.1 // indirect
79 | github.com/spf13/cast v1.5.0 // indirect
80 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
81 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
82 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
83 | github.com/yuin/goldmark v1.7.0 // indirect
84 | github.com/yuin/goldmark-meta v1.1.0 // indirect
85 | github.com/zclconf/go-cty v1.14.4 // indirect
86 | go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
87 | go.uber.org/atomic v1.11.0 // indirect
88 | golang.org/x/crypto v0.24.0 // indirect
89 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
90 | golang.org/x/mod v0.18.0 // indirect
91 | golang.org/x/net v0.26.0 // indirect
92 | golang.org/x/oauth2 v0.21.0 // indirect
93 | golang.org/x/sys v0.21.0 // indirect
94 | golang.org/x/text v0.16.0 // indirect
95 | google.golang.org/appengine v1.6.8 // indirect
96 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
97 | google.golang.org/grpc v1.65.0 // indirect
98 | google.golang.org/protobuf v1.34.1 // indirect
99 | gopkg.in/yaml.v2 v2.4.0 // indirect
100 | )
101 |
--------------------------------------------------------------------------------
/mimir/shared.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strconv"
7 | "strings"
8 | "unicode/utf8"
9 |
10 | "github.com/prometheus/common/model"
11 | "github.com/prometheus/prometheus/promql/parser"
12 | )
13 |
14 | var (
15 | groupRuleNameRegexp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-_.]*$`)
16 | labelNameRegexp = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
17 | metricNameRegexp = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`)
18 | validTime = "^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)"
19 | validTimeRE = regexp.MustCompile(validTime)
20 | )
21 |
22 | func handleHTTPError(err error, baseMsg string) error {
23 | if err != nil {
24 | return fmt.Errorf("%s %v", baseMsg, err)
25 | }
26 |
27 | return nil
28 | }
29 |
30 | // Array to String Array
31 | func expandStringArray(v []interface{}) []string {
32 | var m []string
33 | for _, val := range v {
34 | m = append(m, val.(string))
35 | }
36 |
37 | return m
38 | }
39 |
40 | // Map to String Map
41 | func expandStringMap(v map[string]interface{}) map[string]string {
42 | m := make(map[string]string)
43 | for key, val := range v {
44 | m[key] = val.(string)
45 | }
46 |
47 | return m
48 | }
49 |
50 | func validateGroupRuleName(v interface{}, k string) (ws []string, errors []error) {
51 | value := v.(string)
52 |
53 | if !groupRuleNameRegexp.MatchString(value) {
54 | errors = append(errors, fmt.Errorf(
55 | "\"%s\": Invalid Group Rule Name %q. Must match the regex %s", k, value, groupRuleNameRegexp))
56 | }
57 |
58 | return
59 | }
60 |
61 | func validatePromQLExpr(v interface{}, k string) (ws []string, errors []error) {
62 | value := v.(string)
63 |
64 | if _, err := parser.ParseExpr(value); err != nil {
65 | errors = append(errors, fmt.Errorf(
66 | "\"%s\": Invalid PromQL expression %q: %v", k, value, err))
67 | }
68 |
69 | return
70 | }
71 |
72 | func validateLabels(v interface{}, k string) (ws []string, errors []error) {
73 | m := v.(map[string]interface{})
74 | for lname, lvalue := range m {
75 | if !labelNameRegexp.MatchString(lname) {
76 | errors = append(errors, fmt.Errorf(
77 | "\"%s\": Invalid Label Name %q. Must match the regex %s", k, lname, labelNameRegexp))
78 | }
79 |
80 | if !utf8.ValidString(lvalue.(string)) {
81 | errors = append(errors, fmt.Errorf(
82 | "\"%s\": Invalid Label Value %q: not a valid UTF8 string", k, lvalue))
83 | }
84 | }
85 | return
86 | }
87 |
88 | func validateAnnotations(v interface{}, k string) (ws []string, errors []error) {
89 | m := v.(map[string]interface{})
90 | for aname := range m {
91 | if !labelNameRegexp.MatchString(aname) {
92 | errors = append(errors, fmt.Errorf(
93 | "\"%s\": Invalid Annotation Name %q. Must match the regex %s", k, aname, labelNameRegexp))
94 | }
95 | }
96 | return
97 | }
98 |
99 | func validateDuration(v interface{}, k string) (ws []string, errors []error) {
100 | value := v.(string)
101 |
102 | if value == "" {
103 | return
104 | }
105 |
106 | if _, err := model.ParseDuration(value); err != nil {
107 | errors = append(errors, fmt.Errorf("\"%s\": %v", k, err))
108 | }
109 |
110 | return
111 | }
112 |
113 | func formatDuration(v interface{}) string {
114 | value, _ := model.ParseDuration(v.(string))
115 | str := value.String()
116 | if str == "0s" {
117 | return ""
118 | }
119 | return str
120 | }
121 |
122 | func formatPromQLExpr(v interface{}) string {
123 | if enablePromQLExprFormat {
124 | value, _ := parser.ParseExpr(v.(string))
125 | // remove spaces causing decoding issues with multiline yaml marshal/unmarshall
126 | return strings.TrimLeft(parser.Prettify(value), " ")
127 | }
128 | return v.(string)
129 | }
130 |
131 | // Converts a string of the form "HH:MM" into the number of minutes elapsed in the day.
132 | func parseTime(in string) (mins int) {
133 | timestampComponents := strings.Split(in, ":")
134 | timeStampHours, _ := strconv.Atoi(timestampComponents[0])
135 | timeStampMinutes, _ := strconv.Atoi(timestampComponents[1])
136 |
137 | // Timestamps are stored as minutes elapsed in the day, so multiply hours by 60.
138 | mins = timeStampHours*60 + timeStampMinutes
139 | return mins
140 | }
141 |
142 | func validateTime(v interface{}, k string) (ws []string, errors []error) {
143 | in := v.(string)
144 |
145 | if in == "" {
146 | return
147 | }
148 |
149 | if !validTimeRE.MatchString(in) {
150 | errors = append(errors, fmt.Errorf("\"%s\": couldn't parse timestamp %s, invalid format", k, in))
151 | return
152 | }
153 | timestampComponents := strings.Split(in, ":")
154 | if len(timestampComponents) != 2 {
155 | errors = append(errors, fmt.Errorf("\"%s\": invalid timestamp format: %s", k, in))
156 | return
157 | }
158 | timeStampHours, err := strconv.Atoi(timestampComponents[0])
159 | if err != nil {
160 | errors = append(errors, fmt.Errorf("\"%s\": %v", k, err))
161 | }
162 | timeStampMinutes, err := strconv.Atoi(timestampComponents[1])
163 | if err != nil {
164 | errors = append(errors, fmt.Errorf("\"%s\": %v", k, err))
165 | }
166 | if timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 {
167 | errors = append(errors, fmt.Errorf("\"%s\": timestamp %s out of range", k, in))
168 | }
169 | return
170 | }
171 |
--------------------------------------------------------------------------------
/mimir/shared_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
11 | )
12 |
13 | func getSetEnv(key, fallback string) string {
14 | value, exists := os.LookupEnv(key)
15 | if !exists {
16 | value = fallback
17 | os.Setenv(key, fallback)
18 | }
19 | return value
20 | }
21 |
22 | func testAccCheckMimirRuleGroupExists(n string, name string, client *apiClient) resource.TestCheckFunc {
23 | return func(s *terraform.State) error {
24 | rs, ok := s.RootModule().Resources[n]
25 | if !ok {
26 | keys := make([]string, 0, len(s.RootModule().Resources))
27 | for k := range s.RootModule().Resources {
28 | keys = append(keys, k)
29 | }
30 | return fmt.Errorf("mimir object not found in terraform state: %s. Found: %s", n, strings.Join(keys, ", "))
31 | }
32 |
33 | if rs.Primary.ID == "" {
34 | return fmt.Errorf("mimir object name %s not set in terraform", name)
35 | }
36 |
37 | orgID := rs.Primary.Attributes["org_id"]
38 | name := rs.Primary.Attributes["name"]
39 | namespace := rs.Primary.Attributes["namespace"]
40 |
41 | /* Make a throw-away API object to read from the API */
42 | headers := make(map[string]string)
43 | if orgID != "" {
44 | headers["X-Scope-OrgID"] = orgID
45 | }
46 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
47 | _, err := client.sendRequest("ruler", "GET", path, "", headers)
48 | if err != nil {
49 | return err
50 | }
51 |
52 | return nil
53 | }
54 | }
55 |
56 | func testAccCheckMimirNamespaceExists(n string, name string, client *apiClient) resource.TestCheckFunc {
57 | return func(s *terraform.State) error {
58 | rs, ok := s.RootModule().Resources[n]
59 | if !ok {
60 | keys := make([]string, 0, len(s.RootModule().Resources))
61 | for k := range s.RootModule().Resources {
62 | keys = append(keys, k)
63 | }
64 | return fmt.Errorf("mimir object not found in terraform state: %s. Found: %s", n, strings.Join(keys, ", "))
65 | }
66 |
67 | if rs.Primary.ID == "" {
68 | return fmt.Errorf("mimir object name %s not set in terraform", name)
69 | }
70 |
71 | orgID := rs.Primary.Attributes["org_id"]
72 | namespace := rs.Primary.Attributes["namespace"]
73 |
74 | /* Make a throw-away API object to read from the API */
75 | headers := make(map[string]string)
76 | if orgID != "" {
77 | headers["X-Scope-OrgID"] = orgID
78 | }
79 | path := fmt.Sprintf("/config/v1/rules/%s", namespace)
80 | _, err := client.sendRequest("ruler", "GET", path, "", headers)
81 | if err != nil {
82 | return err
83 | }
84 |
85 | return nil
86 | }
87 | }
88 |
89 | func testAccCheckMimirRuleGroupDestroy(s *terraform.State) error {
90 | // retrieve the connection established in Provider configuration
91 | client := testAccProvider.Meta().(*apiClient)
92 |
93 | // loop through the resources in state, verifying each widget
94 | // is destroyed
95 | for _, rs := range s.RootModule().Resources {
96 | if !strings.HasPrefix(rs.Type, "mimir_rule_group") {
97 | continue
98 | }
99 |
100 | orgID := rs.Primary.Attributes["org_id"]
101 | name := rs.Primary.Attributes["name"]
102 | namespace := rs.Primary.Attributes["namespace"]
103 |
104 | headers := make(map[string]string)
105 | if orgID != "" {
106 | headers["X-Scope-OrgID"] = orgID
107 | }
108 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
109 | _, err := client.sendRequest("ruler", "GET", path, "", headers)
110 |
111 | // If the error is equivalent to 404 not found, the widget is destroyed.
112 | // Otherwise return the error
113 | if !strings.Contains(err.Error(), "response code '404'") {
114 | return err
115 | }
116 | }
117 |
118 | return nil
119 | }
120 |
121 | func testAccCheckMimirRuleDestroy(s *terraform.State) error {
122 | // retrieve the connection established in Provider configuration
123 | client := testAccProvider.Meta().(*apiClient)
124 |
125 | // loop through the resources in state, verifying each is destroyed
126 | for _, rs := range s.RootModule().Resources {
127 | if rs.Type != "mimir_rules" {
128 | continue
129 | }
130 |
131 | orgID := rs.Primary.Attributes["org_id"]
132 | namespace := rs.Primary.Attributes["namespace"]
133 |
134 | headers := make(map[string]string)
135 | if orgID != "" {
136 | headers["X-Scope-OrgID"] = orgID
137 | }
138 |
139 | // Parse managed_groups from state attributes
140 | // Terraform stores list items as: managed_groups.0, managed_groups.1, etc.
141 | managedGroupsCount, _ := strconv.Atoi(rs.Primary.Attributes["managed_groups.#"])
142 |
143 | for i := 0; i < managedGroupsCount; i++ {
144 | groupName := rs.Primary.Attributes[fmt.Sprintf("managed_groups.%d", i)]
145 |
146 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, groupName)
147 | _, err := client.sendRequest("ruler", "GET", path, "", headers)
148 |
149 | // If the error is equivalent to 404 not found, the group is destroyed.
150 | // Otherwise return the error
151 | if err != nil && !strings.Contains(err.Error(), "response code '404'") {
152 | return err
153 | }
154 | }
155 | }
156 |
157 | return nil
158 | }
159 | func setupClient() *apiClientOpt {
160 | headers := make(map[string]string)
161 | headers["X-Scope-OrgID"] = mimirOrgID
162 |
163 | opt := &apiClientOpt{
164 | uri: mimirURI,
165 | rulerURI: mimirRulerURI,
166 | alertmanagerURI: mimirAlertmanagerURI,
167 | insecure: false,
168 | username: "",
169 | password: "",
170 | proxyURL: "",
171 | token: "",
172 | cert: "",
173 | key: "",
174 | ca: "",
175 | headers: headers,
176 | timeout: 2,
177 | debug: true,
178 | }
179 | return opt
180 | }
181 |
--------------------------------------------------------------------------------
/mimir/api_client.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | // Largely copied from https://github.com/Mastercard/terraform-provider-restapi/blob/master/restapi/api_client.go
4 |
5 | import (
6 | "bytes"
7 | "crypto/tls"
8 | "crypto/x509"
9 | "fmt"
10 | "io"
11 | "log"
12 | "net/http"
13 | "net/http/httputil"
14 | "net/url"
15 | "os"
16 | "strings"
17 | "time"
18 | )
19 |
20 | type apiClientOpt struct {
21 | uri string
22 | rulerURI string
23 | alertmanagerURI string
24 | distributorURI string
25 | cert string
26 | key string
27 | ca string
28 | token string
29 | insecure bool
30 | username string
31 | password string
32 | proxyURL string
33 | headers map[string]string
34 | timeout int
35 | debug bool
36 | }
37 |
38 | type apiClient struct {
39 | httpClient *http.Client
40 | uri string
41 | rulerURI string
42 | alertmanagerURI string
43 | distributorURI string
44 | insecure bool
45 | token string
46 | username string
47 | password string
48 | headers map[string]string
49 | debug bool
50 | }
51 |
52 | // Make a new api client for RESTful calls
53 | func NewAPIClient(opt *apiClientOpt) (*apiClient, error) {
54 | /* Remove any trailing slashes since we will append
55 | to this URL with our own root-prefixed location */
56 | opt.uri = strings.TrimSuffix(opt.uri, "/")
57 |
58 | // Setup HTTPS client
59 | tlsConfig := &tls.Config{}
60 |
61 | // Set insecure verify
62 | if opt.insecure {
63 | tlsConfig.InsecureSkipVerify = true
64 | }
65 |
66 | if opt.cert != "" && opt.key != "" {
67 | var cert tls.Certificate
68 | var err error
69 | if strings.HasPrefix(opt.cert, "-----BEGIN") && strings.HasPrefix(opt.key, "-----BEGIN") {
70 | cert, err = tls.X509KeyPair([]byte(opt.cert), []byte(opt.key))
71 | } else {
72 | cert, err = tls.LoadX509KeyPair(opt.cert, opt.key)
73 | }
74 | if err != nil {
75 | return nil, err
76 | }
77 | tlsConfig.Certificates = []tls.Certificate{cert}
78 | }
79 |
80 | if opt.ca != "" {
81 | var caCert []byte
82 | var err error
83 | if strings.HasPrefix(opt.ca, "-----BEGIN") {
84 | caCert = []byte(opt.ca)
85 | } else {
86 | caCert, err = os.ReadFile(opt.ca)
87 |
88 | if err != nil {
89 | return nil, err
90 | }
91 | }
92 | caCertPool := x509.NewCertPool()
93 | caCertPool.AppendCertsFromPEM(caCert)
94 | tlsConfig.RootCAs = caCertPool
95 | }
96 |
97 | tr := &http.Transport{
98 | TLSClientConfig: tlsConfig,
99 | Proxy: http.ProxyFromEnvironment,
100 | }
101 |
102 | if opt.proxyURL != "" {
103 | log.Printf("api_client.go: Using proxy: %s\n", opt.proxyURL)
104 | proxy, err := url.Parse(opt.proxyURL)
105 | if err != nil {
106 | return nil, fmt.Errorf("error parsing proxy url: %s", err)
107 | }
108 | tr.Proxy = http.ProxyURL(proxy)
109 | }
110 |
111 | client := apiClient{
112 | httpClient: &http.Client{
113 | Timeout: time.Second * time.Duration(opt.timeout),
114 | Transport: tr,
115 | },
116 | uri: opt.uri,
117 | rulerURI: opt.rulerURI,
118 | alertmanagerURI: opt.alertmanagerURI,
119 | distributorURI: opt.distributorURI,
120 | insecure: opt.insecure,
121 | token: opt.token,
122 | username: opt.username,
123 | password: opt.password,
124 | headers: opt.headers,
125 | debug: opt.debug,
126 | }
127 |
128 | return &client, nil
129 | }
130 |
131 | /*
132 | Helper function that handles sending/receiving and handling
133 |
134 | of HTTP data in and out.
135 | */
136 | func (client *apiClient) sendRequest(component, method string, path, data string, headers map[string]string) (string, error) {
137 | var fullURI string
138 |
139 | switch {
140 | case component == "ruler" && client.rulerURI != "":
141 | fullURI = client.rulerURI + path
142 | case component == "alertmanager" && client.alertmanagerURI != "":
143 | fullURI = client.alertmanagerURI + path
144 | case component == "distributor" && client.distributorURI != "":
145 | fullURI = client.distributorURI + path
146 | default:
147 | fullURI = client.uri + path
148 | }
149 |
150 | var req *http.Request
151 | var err error
152 |
153 | buffer := bytes.NewBuffer([]byte(data))
154 |
155 | if data == "" {
156 | req, err = http.NewRequest(method, fullURI, nil)
157 | } else {
158 | req, err = http.NewRequest(method, fullURI, buffer)
159 | }
160 |
161 | if err != nil {
162 | log.Fatal(err)
163 | }
164 |
165 | if client.token != "" {
166 | req.Header.Set("Authorization", "Bearer "+client.token)
167 | }
168 |
169 | // Set client headers from provider
170 | if len(client.headers) > 0 {
171 | for n, v := range client.headers {
172 | req.Header.Set(n, v)
173 | }
174 | }
175 |
176 | // Set client headers from resource
177 | if len(headers) > 0 {
178 | for n, v := range headers {
179 | req.Header.Set(n, v)
180 | }
181 | }
182 |
183 | if client.username != "" && client.password != "" {
184 | /* ... and fall back to basic auth if configured */
185 | req.SetBasicAuth(client.username, client.password)
186 | }
187 |
188 | if client.debug {
189 | reqDump, err := httputil.DumpRequestOut(req, true)
190 | if err != nil {
191 | log.Fatal(err)
192 | }
193 |
194 | log.Printf("REQUEST:\n%s", string(reqDump))
195 | }
196 |
197 | resp, err := client.httpClient.Do(req)
198 |
199 | if err != nil {
200 | if client.debug {
201 | log.Printf("api_client.go: Error detected: %s\n", err)
202 | }
203 | return "", err
204 | }
205 |
206 | if client.debug {
207 | respDump, err := httputil.DumpResponse(resp, true)
208 | if err != nil {
209 | log.Fatal(err)
210 | }
211 |
212 | log.Printf("RESPONSE:\n%s", string(respDump))
213 | }
214 |
215 | bodyBytes, err2 := io.ReadAll(resp.Body)
216 | resp.Body.Close()
217 |
218 | if err2 != nil {
219 | return "", err2
220 | }
221 | body := string(bodyBytes)
222 |
223 | if resp.StatusCode < 200 || resp.StatusCode >= 300 {
224 | return body, fmt.Errorf("unexpected response code '%d': %s", resp.StatusCode, body)
225 | }
226 |
227 | return body, nil
228 | }
229 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Terraform provider for grafana mimir
2 |
3 | This terraform provider allows you to interact with grafana mimir.
4 |
5 | Currently only these components could be managed with the api:
6 | - [alertmanager](https://grafana.com/docs/mimir/latest/references/architecture/components/alertmanager/)
7 | - [ruler](https://grafana.com/docs/mimir/latest/references/architecture/components/ruler/)
8 |
9 |
10 | See [Mimir API Reference](https://grafana.com/docs/mimir/latest/references/http-api/)
11 |
12 | ## Provider `mimir`
13 |
14 | Example:
15 |
16 | ```
17 | provider "mimir" {
18 | ruler_uri = "http://localhost:8080/prometheus"
19 | alertmanager_uri = "http://localhost:8080"
20 | org_id = "mytenant"
21 | }
22 | ```
23 |
24 | ### URI path
25 |
26 | - ruler_uri (default prefix: /prometheus)
27 | - alertmanager_uri (default prefix: /)
28 |
29 | > **Warning**
30 | > You may check and adapt provider uri path: `ruler_uri` and `alertmanager_uri`.
31 |
32 |
33 | ### Authentication
34 |
35 | Grafana Mimir have no authentication support, so this is delegated to a reverse proxy.
36 |
37 | See [Grafana Mimir authentication and authorization](https://grafana.com/docs/mimir/v2.7.x/operators-guide/securing/authentication-and-authorization/)
38 |
39 | The provider support basic auth, token.
40 |
41 | #### Basic auth
42 |
43 | ```
44 | provider "mimir" {
45 | ruler_uri = "http://localhost:8080/prometheus"
46 | alertmanager_uri = "http://localhost:8080"
47 | org_id = "mytenant"
48 | username = "user"
49 | password = "password"
50 | }
51 | ```
52 |
53 | #### Token
54 |
55 | ```
56 | provider "mimir" {
57 | ruler_uri = "http://localhost:8080/prometheus"
58 | alertmanager_uri = "http://localhost:8080"
59 | org_id = "mytenant"
60 | token = "supersecrettoken"
61 | }
62 | ```
63 |
64 | ### Headers
65 |
66 | ```
67 | provider "mimir" {
68 | ruler_uri = "http://localhost:8080/prometheus"
69 | alertmanager_uri = "http://localhost:8080"
70 | org_id = "mytenant"
71 | header = {
72 | "Custom-Auth" = "Custom value"
73 | }
74 | }
75 | ```
76 |
77 | ## Resource `mimir_rule_group_alerting`
78 |
79 | Example:
80 |
81 | ```
82 | resource "mimir_rule_group_alerting" "test" {
83 | name = "test1"
84 | namespace = "namespace1"
85 | rule {
86 | alert = "HighRequestLatency"
87 | expr = "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5"
88 | for = "10m"
89 | labels = {
90 | severity = "warning"
91 | }
92 | annotations = {
93 | summary = "High request latency"
94 | }
95 | }
96 | }
97 | ```
98 |
99 | ## Resource `mimir_rule_group_recording`
100 |
101 | Example:
102 |
103 | ```
104 | resource "mimir_rule_group_recording" "record" {
105 | name = "test1"
106 | namespace = "namespace1"
107 | interval = "6h"
108 | query_offset = "5m"
109 | rule {
110 | expr = "sum by (job) (http_inprogress_requests)"
111 | record = "job:http_inprogress_requests:sum"
112 | }
113 | }
114 | ```
115 |
116 | ## Resource `mimir_alertmanager_config`
117 |
118 | Notification integrations Supported: https://prometheus.io/docs/alerting/latest/configuration/#receiver
119 |
120 | Example:
121 |
122 | ```
123 | resource "mimir_alertmanager_config" "test" {
124 |
125 | route {
126 | group_by = ["..."]
127 | group_wait = "30s"
128 | group_interval = "5m"
129 | repeat_interval = "1h"
130 | receiver = "pagerduty"
131 | child_route {
132 | group_by = ["..."]
133 | group_wait = "30s"
134 | group_interval = "5m"
135 | repeat_interval = "1h"
136 | receiver = "pagerduty"
137 | }
138 | }
139 |
140 | receiver {
141 | name = "pagerduty"
142 | pagerduty_configs {
143 | routing_key = "secret"
144 | severity = "info"
145 | details = {
146 | environment = "test"
147 | platform = "sandbox"
148 | }
149 | }
150 | }
151 | }
152 | ```
153 |
154 | ## Importing existing resources
155 | This provider supports importing existing resources into the terraform state. Import is done according to the various provider/resource configuation settings to contact the API server and obtain data.
156 |
157 | ### mimir alerting rule group
158 |
159 | To import mimir rule group alerting
160 | The id is build as `/`
161 |
162 | Example:
163 |
164 | ```
165 | terraform import 'mimir_rule_group_alerting.alert1' namespace1/alert1
166 | mimir_rule_group_alerting.alert1: Importing from ID "namespace1/alert1"...
167 | mimir_rule_group_alerting.alert1: Import prepared!
168 | Prepared mimir_rule_group_alerting for import
169 | mimir_rule_group_alerting.alert1: Refreshing state... [id=namespace1/alert1]
170 |
171 | Import successful!
172 |
173 | The resources that were imported are shown above. These resources are now in
174 | your Terraform state and will henceforth be managed by Terraform.
175 |
176 | ```
177 |
178 | ### mimir recording rule group
179 |
180 | To import mimir rule group recording
181 | The id is build as `/`
182 |
183 | Example:
184 |
185 | ```
186 | terraform import 'mimir_rule_group_recording.record1' namespace1/record1
187 | mimir_rule_group_recording.record1: Importing from ID "namespace1/record1"...
188 | mimir_rule_group_recording.record1: Import prepared!
189 | Prepared mimir_rule_group_recording for import
190 | mimir_rule_group_recording.record1: Refreshing state... [id=namespace1/record1]
191 |
192 | Import successful!
193 |
194 | The resources that were imported are shown above. These resources are now in
195 | your Terraform state and will henceforth be managed by Terraform.
196 |
197 | ```
198 |
199 | ### mimir alertmanager config
200 |
201 | To import mimir alertmanager config
202 | The id is build as ``
203 |
204 | Example:
205 |
206 | ```
207 | terraform import 'mimir_alertmanager_config.test' test
208 | mimir_alertmanager_config.test: Importing from ID "test"...
209 | mimir_alertmanager_config.test: Import prepared!
210 | Prepared mimir_alertmanager_config for import
211 | mimir_alertmanager_config.test: Refreshing state... [id=test]
212 |
213 | Import successful!
214 |
215 | The resources that were imported are shown above. These resources are now in
216 | your Terraform state and will henceforth be managed by Terraform.
217 |
218 | ```
219 |
220 | ## Contributing
221 | Pull requests are always welcome! Please be sure the following things are taken care of with your pull request:
222 | * `go fmt` is run before pushing
223 | * Be sure to add a test case for new functionality (or explain why this cannot be done)
224 |
225 |
--------------------------------------------------------------------------------
/mimir/api_client_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | // Largely copied from https://github.com/Mastercard/terraform-provider-restapi/blob/master/restapi/api_client_test.go
4 |
5 | import (
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "testing"
10 | "time"
11 | )
12 |
13 | var apiClientServer *http.Server
14 |
15 | func TestAPIClient(t *testing.T) {
16 | debug := false
17 | address := "127.0.0.1:8082"
18 |
19 | if debug {
20 | log.Println("api_client_test.go: Starting HTTP server")
21 | }
22 | setupAPIClientServer(debug, address)
23 | defer shutdownAPIClientServer()
24 |
25 | /* Notice the intentional trailing / */
26 | opt := &apiClientOpt{
27 | uri: "http://127.0.0.1:8082/",
28 | headers: make(map[string]string, 0),
29 | timeout: 2,
30 | debug: debug,
31 | }
32 | client, err := NewAPIClient(opt)
33 | if err != nil {
34 | t.Fatalf("api_client_test.go: Failed to init api client, err: %v", err)
35 | }
36 |
37 | var res string
38 |
39 | if debug {
40 | log.Printf("api_client_test.go: Testing standard OK request\n")
41 | }
42 | var headers map[string]string
43 | res, err = client.sendRequest("", "GET", "/ok", "", headers)
44 | if err != nil {
45 | t.Fatalf("api_client_test.go: %s", err)
46 | }
47 | if res != "It works!" {
48 | t.Fatalf("api_client_test.go: Got back '%s' but expected 'It works!'\n", res)
49 | }
50 |
51 | if debug {
52 | log.Printf("api_client_test.go: Testing redirect request\n")
53 | }
54 | res, err = client.sendRequest("", "GET", "/redirect", "", headers)
55 | if err != nil {
56 | t.Fatalf("api_client_test.go: %s", err)
57 | }
58 | if res != "It works!" {
59 | t.Fatalf("api_client_test.go: Got back '%s' but expected 'It works!'\n", res)
60 | }
61 |
62 | /* Verify timeout works */
63 | if debug {
64 | log.Printf("api_client_test.go: Testing timeout aborts requests\n")
65 | }
66 | _, err = client.sendRequest("", "GET", "/slow", "", headers)
67 | if err != nil {
68 | if debug {
69 | log.Println("api_client_test.go: slow request expected")
70 | }
71 | } else {
72 | t.Fatalf("api_client_test.go: Timeout did not trigger on slow request")
73 | }
74 |
75 | if debug {
76 | log.Println("api_client_test.go: Stopping HTTP server")
77 | }
78 | }
79 |
80 | func TestAPIClientTLSUnsecure(t *testing.T) {
81 | debug := false
82 | address := "127.0.0.1:8083"
83 | setupAPIClientServer(debug, address)
84 | defer shutdownAPIClientServer()
85 |
86 | /* Notice the intentional trailing / */
87 | opt := &apiClientOpt{
88 | uri: fmt.Sprintf("https://%s/", address),
89 | insecure: true,
90 | cert: "../tests/certs/server.crt",
91 | key: "../tests/certs/server.key",
92 | ca: "../tests/certs/ca.crt",
93 | headers: make(map[string]string, 0),
94 | timeout: 2,
95 | debug: debug,
96 | }
97 | _, err := NewAPIClient(opt)
98 | if err != nil {
99 | t.Fatalf("api_client_test.go: Failed to init api client, err: %v", err)
100 | }
101 | }
102 |
103 | func TestAPIClientTLS(t *testing.T) {
104 | debug := false
105 | address := "127.0.0.1:8084"
106 | setupAPIClientServer(debug, address)
107 | defer shutdownAPIClientServer()
108 |
109 | /* Notice the intentional trailing / */
110 | opt := &apiClientOpt{
111 | uri: fmt.Sprintf("https://%s/", address),
112 | cert: "../tests/certs/server.crt",
113 | key: "../tests/certs/server.key",
114 | ca: "../tests/certs/ca.crt",
115 | headers: make(map[string]string, 0),
116 | timeout: 2,
117 | debug: debug,
118 | }
119 | _, err := NewAPIClient(opt)
120 | if err != nil {
121 | t.Fatalf("api_client_test.go: Failed to init api client, err: %v", err)
122 | }
123 | }
124 |
125 | func TestAPIClientProxy(t *testing.T) {
126 | debug := false
127 | address := "127.0.0.1:8085"
128 | setupAPIClientServer(debug, address)
129 | defer shutdownAPIClientServer()
130 |
131 | /* Notice the intentional trailing / */
132 | opt := &apiClientOpt{
133 | uri: fmt.Sprintf("http://%s/", address),
134 | insecure: false,
135 | headers: make(map[string]string, 0),
136 | timeout: 2,
137 | debug: debug,
138 | proxyURL: "http://localhost:3128",
139 | }
140 | _, err := NewAPIClient(opt)
141 | if err != nil {
142 | t.Fatalf("api_client_test.go: Failed to init api client, err: %v", err)
143 | }
144 | }
145 |
146 | func TestAPIClientBasicAuth(t *testing.T) {
147 | debug := false
148 | address := "127.0.0.1:8086"
149 | setupAPIClientServer(debug, address)
150 | defer shutdownAPIClientServer()
151 |
152 | /* Notice the intentional trailing / */
153 | opt := &apiClientOpt{
154 | uri: fmt.Sprintf("http://%s/", address),
155 | insecure: false,
156 | username: "loki",
157 | password: "password",
158 | headers: make(map[string]string, 0),
159 | timeout: 2,
160 | debug: debug,
161 | }
162 | client, err := NewAPIClient(opt)
163 | if err != nil {
164 | t.Fatalf("api_client_test.go: Failed to init api client, err: %v", err)
165 | }
166 | var headers map[string]string
167 | _, err = client.sendRequest("", "GET", "/ok", "", headers)
168 | if err != nil {
169 | t.Fatalf("api_client_test.go: %s", err)
170 | }
171 | }
172 |
173 | func TestAPIClientBearerAuth(t *testing.T) {
174 | debug := false
175 | address := "127.0.0.1:8087"
176 | setupAPIClientServer(debug, address)
177 | defer shutdownAPIClientServer()
178 |
179 | /* Notice the intentional trailing / */
180 | opt := &apiClientOpt{
181 | uri: fmt.Sprintf("http://%s/", address),
182 | token: "supersecret",
183 | headers: make(map[string]string, 0),
184 | timeout: 2,
185 | debug: debug,
186 | }
187 | client, err := NewAPIClient(opt)
188 | if err != nil {
189 | t.Fatalf("api_client_test.go: Failed to init api client, err: %v", err)
190 | }
191 | var headers map[string]string
192 | _, err = client.sendRequest("", "GET", "/ok", "", headers)
193 | if err != nil {
194 | t.Fatalf("api_client_test.go: %s", err)
195 | }
196 | }
197 |
198 | func TestAPIClientDebug(t *testing.T) {
199 | debug := true
200 | address := "127.0.0.1:8088"
201 | setupAPIClientServer(debug, address)
202 | defer shutdownAPIClientServer()
203 |
204 | /* Notice the intentional trailing / */
205 | opt := &apiClientOpt{
206 | uri: fmt.Sprintf("http://%s/", address),
207 | headers: make(map[string]string, 0),
208 | timeout: 2,
209 | debug: debug,
210 | }
211 | client, err := NewAPIClient(opt)
212 | if err != nil {
213 | t.Fatalf("api_client_test.go: Failed to init api client, err: %v", err)
214 | }
215 | var headers map[string]string
216 | _, err = client.sendRequest("", "GET", "/ok", "", headers)
217 | if err != nil {
218 | t.Fatalf("api_client_test.go: %s", err)
219 | }
220 | }
221 |
222 | func TestAPIClientHeaders(t *testing.T) {
223 | debug := false
224 | address := "127.0.0.1:8089"
225 | setupAPIClientServer(debug, address)
226 | defer shutdownAPIClientServer()
227 |
228 | headers := map[string]string{
229 | "Custom": "header set",
230 | }
231 | /* Notice the intentional trailing / */
232 | opt := &apiClientOpt{
233 | uri: fmt.Sprintf("http://%s/", address),
234 | headers: headers,
235 | timeout: 2,
236 | debug: debug,
237 | }
238 | client, err := NewAPIClient(opt)
239 | if err != nil {
240 | t.Fatalf("api_client_test.go: Failed to init api client, err: %v", err)
241 | }
242 | _, err = client.sendRequest("", "GET", "/ok", "", headers)
243 | if err != nil {
244 | t.Fatalf("api_client_test.go: %s", err)
245 | }
246 | }
247 |
248 | func setupAPIClientServer(debug bool, address string) {
249 | serverMux := http.NewServeMux()
250 | serverMux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
251 | _, _ = w.Write([]byte("It works!"))
252 | })
253 | serverMux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
254 | time.Sleep(9999 * time.Second)
255 | _, _ = w.Write([]byte("This will never return!!!!!"))
256 | })
257 | serverMux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
258 | http.Redirect(w, r, "/ok", http.StatusPermanentRedirect)
259 | })
260 |
261 | apiClientServer = &http.Server{
262 | Addr: address,
263 | Handler: serverMux,
264 | ReadTimeout: 1 * time.Second,
265 | WriteTimeout: 1 * time.Second,
266 | IdleTimeout: 30 * time.Second,
267 | ReadHeaderTimeout: 2 * time.Second,
268 | }
269 | go func() {
270 | err := apiClientServer.ListenAndServe()
271 | if err != nil && debug {
272 | log.Println(err)
273 | }
274 | }()
275 | /* let the server start */
276 | time.Sleep(1 * time.Second)
277 | }
278 |
279 | func shutdownAPIClientServer() {
280 | apiClientServer.Close()
281 | }
282 |
--------------------------------------------------------------------------------
/mimir/resource_mimir_alertmanager_config.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "strings"
8 | "time"
9 |
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12 | "gopkg.in/yaml.v3"
13 | )
14 |
15 | func resourcemimirAlertmanagerConfig() *schema.Resource {
16 | return &schema.Resource{
17 | CreateContext: resourcemimirAlertmanagerConfigCreate,
18 | ReadContext: resourcemimirAlertmanagerConfigRead,
19 | UpdateContext: resourcemimirAlertmanagerConfigUpdate,
20 | DeleteContext: resourcemimirAlertmanagerConfigDelete,
21 | Importer: &schema.ResourceImporter{
22 | StateContext: schema.ImportStatePassthroughContext,
23 | },
24 | Schema: resourceMimirAlertmanagerConfigSchemaV1(),
25 | }
26 | }
27 |
28 | func resourcemimirAlertmanagerConfigCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
29 | client := meta.(*apiClient)
30 | orgID := d.Get("org_id").(string)
31 |
32 | if !overwriteAlertmanagerConfig {
33 | alertmanagerConfigExists := true
34 | headers := make(map[string]string)
35 | if orgID != "" {
36 | headers["X-Scope-OrgID"] = orgID
37 | }
38 | resp, err := client.sendRequest("alertmanager", "GET", apiAlertsPath, "", headers)
39 | baseMsg := "Cannot read alertmanager config"
40 | err = handleHTTPError(err, baseMsg)
41 | if err != nil {
42 | if strings.Contains(err.Error(), "response code '404'") {
43 | alertmanagerConfigExists = false
44 | } else {
45 | return diag.FromErr(err)
46 | }
47 | }
48 |
49 | // Check if an empty config has been set
50 | if _, isEmpty := alertmanagerEmptyConfigCheck(d, resp); isEmpty {
51 | alertmanagerConfigExists = false
52 | }
53 |
54 | if alertmanagerConfigExists {
55 | return diag.Errorf("alertmanager config already exists")
56 | }
57 | }
58 |
59 | _, err := alertmanagerConfigCreateUpdate(client, d, apiAlertsPath)
60 | baseMsg := "Cannot create alertmanager config"
61 | err = handleHTTPError(err, baseMsg)
62 | if err != nil {
63 | return diag.FromErr(err)
64 | }
65 | d.SetId(client.headers["X-Scope-OrgID"])
66 |
67 | // Retry read as mimir api could return a 404 status code caused by the event change notification propagation.
68 | // Add delay of * time.Second) between each retry with a max retries.
69 | for i := 1; i <= alertmanagerReadRetryAfterChange; i++ {
70 | result := resourcemimirAlertmanagerConfigRead(ctx, d, meta)
71 | if len(result) > 0 && !result.HasError() {
72 | log.Printf("[WARN] Alertmanager config previously created not found (%d/3)", i)
73 | time.Sleep(alertmanagerReadDelayAfterChangeDuration)
74 | continue
75 | }
76 | return result
77 | }
78 | return resourcemimirAlertmanagerConfigRead(ctx, d, meta)
79 | }
80 |
81 | func resourcemimirAlertmanagerConfigRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
82 | orgID := d.Get("org_id").(string)
83 | var diags diag.Diagnostics
84 | resp, err := alertmanagerConfigRead(meta, orgID)
85 | if err != nil {
86 | if d.IsNewResource() && strings.Contains(err.Error(), "response code '404'") {
87 | diags = append(diags, diag.Diagnostic{
88 | Severity: diag.Warning,
89 | Summary: fmt.Sprintf("Alertmanager config not found. You should increase the provider parameter 'alertmanager_read_delay_after_change' (current: %s)", alertmanagerReadDelayAfterChange),
90 | })
91 | return diags
92 | } else if !d.IsNewResource() && strings.Contains(err.Error(), "response code '404'") {
93 | diags = append(diags, diag.Diagnostic{
94 | Severity: diag.Warning,
95 | Summary: fmt.Sprintf("Alertmanager config (id: %s) not found, removing from state", d.Id()),
96 | })
97 | d.SetId("")
98 | return diags
99 | }
100 | return diag.FromErr(err)
101 | }
102 |
103 | // Check if an empty config has been set
104 | if diag, isEmpty := alertmanagerEmptyConfigCheck(d, resp); isEmpty {
105 | return diag
106 | }
107 |
108 | var alertmanagerUserConf alertmanagerUserConfig
109 | err = yaml.Unmarshal([]byte(resp), &alertmanagerUserConf)
110 | if err != nil {
111 | return diag.FromErr(err)
112 | }
113 |
114 | var alertmanagerConf alertmanagerConfig
115 | err = yaml.Unmarshal([]byte(alertmanagerUserConf.AlertmanagerConfig), &alertmanagerConf)
116 | if err != nil {
117 | return diag.FromErr(err)
118 | }
119 |
120 | err = d.Set("org_id", orgID)
121 | if err != nil {
122 | return diag.FromErr(err)
123 | }
124 |
125 | if alertmanagerConf.Global != nil {
126 | if err := d.Set("global", flattenGlobalConfig(alertmanagerConf.Global)); err != nil {
127 | return diag.Errorf("error setting item: %v", err)
128 | }
129 | }
130 | if err := d.Set("time_interval", flattenMuteTimeIntervalConfig(alertmanagerConf.MuteTimeIntervals)); err != nil {
131 | return diag.Errorf("error setting item: %v", err)
132 | }
133 | if err := d.Set("inhibit_rule", flattenInhibitRuleConfig(alertmanagerConf.InhibitRules)); err != nil {
134 | return diag.Errorf("error setting item: %v", err)
135 | }
136 | if err := d.Set("receiver", flattenReceiverConfig(alertmanagerConf.Receivers)); err != nil {
137 | return diag.Errorf("error setting item: %v", err)
138 | }
139 | if err := d.Set("route", flattenRouteConfig(alertmanagerConf.Route)); err != nil {
140 | return diag.Errorf("error setting item: %v", err)
141 | }
142 | if err := d.Set("templates", alertmanagerConf.Templates); err != nil {
143 | return diag.Errorf("error setting item: %v", err)
144 | }
145 | if err := d.Set("templates_files", alertmanagerUserConf.TemplateFiles); err != nil {
146 | return diag.Errorf("error setting item: %v", err)
147 | }
148 |
149 | return diags
150 | }
151 |
152 | func resourcemimirAlertmanagerConfigUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
153 | client := meta.(*apiClient)
154 | _, err := alertmanagerConfigCreateUpdate(client, d, apiAlertsPath)
155 | baseMsg := "Cannot update alertmanager config"
156 | err = handleHTTPError(err, baseMsg)
157 | if err != nil {
158 | return diag.FromErr(err)
159 | }
160 | // Add time delay before read to wait the event change notification propagation to finish
161 | time.Sleep(alertmanagerReadDelayAfterChangeDuration)
162 | return resourcemimirAlertmanagerConfigRead(ctx, d, meta)
163 | }
164 |
165 | func resourcemimirAlertmanagerConfigDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
166 | client := meta.(*apiClient)
167 | orgID := d.Get("org_id").(string)
168 | headers := make(map[string]string)
169 | if orgID != "" {
170 | headers["X-Scope-OrgID"] = orgID
171 | }
172 | _, err := client.sendRequest("alertmanager", "DELETE", apiAlertsPath, "", headers)
173 | if err != nil {
174 | return diag.FromErr(fmt.Errorf(
175 | "cannot delete alertmanager config from %s: %v",
176 | fmt.Sprintf("%s%s", client.uri, apiAlertsPath),
177 | err))
178 | }
179 |
180 | // Retry read as mimir api could return a 200 status code but the alertmanager config still exist because of the event change notification propagation latency.
181 | // Add delay of * time.Second) between each retry with a max retries.
182 | for i := 1; i <= alertmanagerReadRetryAfterChange; i++ {
183 | _, err := alertmanagerConfigRead(meta, orgID)
184 | if err == nil {
185 | log.Printf("[WARN] Alertmanager config previously deleted still exist (%d/3)", i)
186 | time.Sleep(alertmanagerReadDelayAfterChangeDuration)
187 | continue
188 | } else if strings.Contains(err.Error(), "response code '404'") {
189 | break
190 | }
191 | return diag.FromErr(err)
192 | }
193 | d.SetId("")
194 | return diag.Diagnostics{}
195 | }
196 |
197 | func alertmanagerConfigRead(meta interface{}, orgID string) (string, error) {
198 | client := meta.(*apiClient)
199 | headers := make(map[string]string)
200 | if orgID != "" {
201 | headers["X-Scope-OrgID"] = orgID
202 | }
203 | resp, err := client.sendRequest("alertmanager", "GET", apiAlertsPath, "", headers)
204 | baseMsg := "Cannot read alertmanager config"
205 | return resp, handleHTTPError(err, baseMsg)
206 | }
207 |
208 | func alertmanagerConfigCreateUpdate(client *apiClient, d *schema.ResourceData, path string) (string, error) {
209 | headers := map[string]string{"Content-Type": "application/yaml"}
210 | orgID := d.Get("org_id").(string)
211 | if orgID != "" {
212 | headers["X-Scope-OrgID"] = orgID
213 | }
214 |
215 | alertmanagerConf := &alertmanagerConfig{
216 | Global: expandGlobalConfig(d.Get("global").([]interface{})),
217 | MuteTimeIntervals: expandMuteTimeIntervalConfig(d.Get("time_interval").([]interface{})),
218 | InhibitRules: expandInhibitRuleConfig(d.Get("inhibit_rule").([]interface{})),
219 | Receivers: expandReceiverConfig(d.Get("receiver").([]interface{})),
220 | Route: expandRouteConfig(d.Get("route").([]interface{})),
221 | Templates: expandStringArray(d.Get("templates").([]interface{})),
222 | }
223 | alertmanagerConfBytes, _ := yaml.Marshal(&alertmanagerConf)
224 |
225 | alertmanagerUserConf := &alertmanagerUserConfig{
226 | TemplateFiles: expandStringMap(d.Get("templates_files").(map[string]interface{})),
227 | AlertmanagerConfig: string(alertmanagerConfBytes),
228 | }
229 |
230 | dataBytes, _ := yaml.Marshal(&alertmanagerUserConf)
231 |
232 | resp, err := client.sendRequest("alertmanager", "POST", path, string(dataBytes), headers)
233 |
234 | return resp, err
235 | }
236 |
237 | func alertmanagerEmptyConfigCheck(d *schema.ResourceData, data string) (diag.Diagnostics, bool) {
238 | var isEmpty bool
239 | var diags diag.Diagnostics
240 | var alertmanagerUserConf alertmanagerUserConfig
241 | err := yaml.Unmarshal([]byte(data), &alertmanagerUserConf)
242 | if err != nil {
243 | return diag.FromErr(err), isEmpty
244 | }
245 |
246 | if alertmanagerUserConf.AlertmanagerConfig == "" {
247 | diags = append(diags, diag.Diagnostic{
248 | Severity: diag.Warning,
249 | Summary: fmt.Sprintf("Alertmanager config (id: %s) is empty, removing from state", d.Id()),
250 | })
251 | d.SetId("")
252 | isEmpty = true
253 | }
254 | return diags, isEmpty
255 | }
256 |
--------------------------------------------------------------------------------
/mimir/provider.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9 | )
10 |
11 | var (
12 | apiAlertsPath = "/api/v1/alerts"
13 | enablePromQLExprFormat bool
14 | overwriteAlertmanagerConfig bool
15 | overwriteRuleGroupConfig bool
16 | ruleGroupReadDelayAfterChange string
17 | alertmanagerReadDelayAfterChange string
18 | ruleGroupReadDelayAfterChangeDuration time.Duration
19 | alertmanagerReadDelayAfterChangeDuration time.Duration
20 | ruleGroupReadRetryAfterChange int
21 | alertmanagerReadRetryAfterChange int
22 | )
23 |
24 | func Provider(version string) func() *schema.Provider {
25 | return func() *schema.Provider {
26 | p := &schema.Provider{
27 | Schema: map[string]*schema.Schema{
28 | "uri": {
29 | Type: schema.TypeString,
30 | Optional: true,
31 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_URI", nil),
32 | Description: "mimir base url",
33 | },
34 | "ruler_uri": {
35 | Type: schema.TypeString,
36 | Optional: true,
37 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_RULER_URI", nil),
38 | Description: "mimir ruler base url",
39 | },
40 | "alertmanager_uri": {
41 | Type: schema.TypeString,
42 | Optional: true,
43 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_ALERTMANAGER_URI", nil),
44 | Description: "mimir alertmanager base url",
45 | },
46 | "distributor_uri": {
47 | Type: schema.TypeString,
48 | Optional: true,
49 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_DISTRIBUTOR_URI", nil),
50 | Description: "mimir distributor base url",
51 | },
52 | "org_id": {
53 | Type: schema.TypeString,
54 | Required: true,
55 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_ORG_ID", nil),
56 | Description: "The default organization id to operate on within mimir. For resources that have an org_id attribute, the resource-level attribute has priority. May alternatively be set via the MIMIR_ORG_ID environment variable.",
57 | },
58 | "token": {
59 | Type: schema.TypeString,
60 | Optional: true,
61 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_TOKEN", nil),
62 | Description: "When set, will use this token for Bearer auth to the API.",
63 | },
64 | "username": {
65 | Type: schema.TypeString,
66 | Optional: true,
67 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_USERNAME", nil),
68 | Description: "When set, will use this username for BASIC auth to the API.",
69 | },
70 | "password": {
71 | Type: schema.TypeString,
72 | Optional: true,
73 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_PASSWORD", nil),
74 | Description: "When set, will use this password for BASIC auth to the API.",
75 | },
76 | "proxy_url": {
77 | Type: schema.TypeString,
78 | Optional: true,
79 | Description: "URL to the proxy to be used for all API requests",
80 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_PROXY_URL", nil),
81 | },
82 | "insecure": {
83 | Type: schema.TypeBool,
84 | Optional: true,
85 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_INSECURE", nil),
86 | Description: "When using https, this disables TLS verification of the host.",
87 | },
88 | "cert": {
89 | Type: schema.TypeString,
90 | Optional: true,
91 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_TLS_CERT", nil),
92 | Description: "Client cert (filepath or inline) for TLS client authentication.",
93 | },
94 | "key": {
95 | Type: schema.TypeString,
96 | Optional: true,
97 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_TLS_KEY", nil),
98 | Description: "Client key (filepath or inline) for TLS client authentication.",
99 | },
100 | "ca": {
101 | Type: schema.TypeString,
102 | Optional: true,
103 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_TLS_CA", nil),
104 | Description: "Client ca (filepath or inline) for TLS client authentication.",
105 | },
106 | "headers": {
107 | Type: schema.TypeMap,
108 | Elem: schema.TypeString,
109 | Optional: true,
110 | Description: "A map of header names and values to set on all outbound requests.",
111 | },
112 | "timeout": {
113 | Type: schema.TypeInt,
114 | Optional: true,
115 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_TIMEOUT", 60),
116 | Description: "When set, will cause requests taking longer than this time (in seconds) to be aborted.",
117 | },
118 | "debug": {
119 | Type: schema.TypeBool,
120 | Optional: true,
121 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_DEBUG", false),
122 | Description: "Enable debug mode to trace requests executed.",
123 | },
124 | "format_promql_expr": {
125 | Type: schema.TypeBool,
126 | Optional: true,
127 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_FORMAT_PROMQL_EXPR", false),
128 | Description: "Enable the formatting of PromQL expression.",
129 | },
130 | "overwrite_alertmanager_config": {
131 | Type: schema.TypeBool,
132 | Optional: true,
133 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_OVERWRITE_ALERTMANAGER_CONFIG", false),
134 | Description: "Overwrite the current alertmanager config on create.",
135 | },
136 | "overwrite_rule_group_config": {
137 | Type: schema.TypeBool,
138 | Optional: true,
139 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_OVERWRITE_RULE_GROUP_CONFIG", false),
140 | Description: "Overwrite the current rule group (alerting/recording) config on create.",
141 | },
142 | "rule_group_read_delay_after_change": {
143 | Type: schema.TypeString,
144 | Optional: true,
145 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_RULE_GROUP_READ_DELAY_AFTER_CHANGE", "1s"),
146 | Description: "When set, add a delay (time duration) to read the rule group after a change.",
147 | ValidateFunc: validateDuration,
148 | },
149 | "rule_group_read_retry_after_change": {
150 | Type: schema.TypeInt,
151 | Optional: true,
152 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_RULE_GROUP_READ_RETRY_AFTER_CHANGE", 3),
153 | Description: "Max retries to read the rule group after a change.",
154 | },
155 | "alertmanager_read_delay_after_change": {
156 | Type: schema.TypeString,
157 | Optional: true,
158 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_ALERTMANAGER_READ_DELAY_AFTER_CHANGE", "1s"),
159 | Description: "When set, add a delay (time duration) to read the alertmanager config after a change.",
160 | ValidateFunc: validateDuration,
161 | },
162 | "alertmanager_read_retry_after_change": {
163 | Type: schema.TypeInt,
164 | Optional: true,
165 | DefaultFunc: schema.EnvDefaultFunc("MIMIR_ALERTMANAGER_READ_RETRY_AFTER_CHANGE", 3),
166 | Description: "Max retries to read the alertmanager config after a change.",
167 | },
168 | },
169 | DataSourcesMap: map[string]*schema.Resource{
170 | "mimir_alertmanager_config": dataSourcemimirAlertmanagerConfig(),
171 | "mimir_rule_group_alerting": dataSourcemimirRuleGroupAlerting(),
172 | "mimir_rule_group_recording": dataSourcemimirRuleGroupRecording(),
173 | "mimir_distributor_tenant_stats": dataSourcemimirDistributorTenantStats(),
174 | },
175 | ResourcesMap: map[string]*schema.Resource{
176 | "mimir_alertmanager_config": resourcemimirAlertmanagerConfig(),
177 | "mimir_rule_group_alerting": resourcemimirRuleGroupAlerting(),
178 | "mimir_rule_group_recording": resourcemimirRuleGroupRecording(),
179 | "mimir_rules": resourceMimirRules(),
180 | },
181 | }
182 | p.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
183 | p.UserAgent("terraform-provider-mimir", version)
184 | return providerConfigure(version, p, d)
185 | }
186 | return p
187 | }
188 | }
189 |
190 | func providerConfigure(version string, p *schema.Provider, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
191 | headers := make(map[string]string)
192 | if initHeaders := d.Get("headers"); initHeaders != nil {
193 | for k, v := range initHeaders.(map[string]interface{}) {
194 | headers[k] = v.(string)
195 | }
196 | }
197 | orgID := d.Get("org_id").(string)
198 | if orgID != "" {
199 | headers["X-Scope-OrgID"] = orgID
200 | }
201 | headers["User-Agent"] = p.UserAgent("terraform-provider-mimir", version)
202 |
203 | opt := &apiClientOpt{
204 | token: d.Get("token").(string),
205 | username: d.Get("username").(string),
206 | password: d.Get("password").(string),
207 | proxyURL: d.Get("proxy_url").(string),
208 | cert: d.Get("cert").(string),
209 | key: d.Get("key").(string),
210 | ca: d.Get("ca").(string),
211 | insecure: d.Get("insecure").(bool),
212 | uri: d.Get("uri").(string),
213 | rulerURI: d.Get("ruler_uri").(string),
214 | alertmanagerURI: d.Get("alertmanager_uri").(string),
215 | distributorURI: d.Get("distributor_uri").(string),
216 | headers: headers,
217 | timeout: d.Get("timeout").(int),
218 | debug: d.Get("debug").(bool),
219 | }
220 |
221 | enablePromQLExprFormat = d.Get("format_promql_expr").(bool)
222 | overwriteAlertmanagerConfig = d.Get("overwrite_alertmanager_config").(bool)
223 | overwriteRuleGroupConfig = d.Get("overwrite_rule_group_config").(bool)
224 | ruleGroupReadDelayAfterChange = d.Get("rule_group_read_delay_after_change").(string)
225 | alertmanagerReadDelayAfterChange = d.Get("alertmanager_read_delay_after_change").(string)
226 | ruleGroupReadDelayAfterChangeDuration, _ = time.ParseDuration(d.Get("rule_group_read_delay_after_change").(string))
227 | alertmanagerReadDelayAfterChangeDuration, _ = time.ParseDuration(d.Get("alertmanager_read_delay_after_change").(string))
228 | ruleGroupReadRetryAfterChange = d.Get("rule_group_read_retry_after_change").(int)
229 | alertmanagerReadRetryAfterChange = d.Get("alertmanager_read_retry_after_change").(int)
230 |
231 | client, err := NewAPIClient(opt)
232 | return client, diag.FromErr(err)
233 | }
234 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/mimir/resource_mimir_rule_group_recording.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "strings"
8 | "time"
9 |
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12 | "gopkg.in/yaml.v3"
13 | )
14 |
15 | func resourcemimirRuleGroupRecording() *schema.Resource {
16 | return &schema.Resource{
17 | CreateContext: resourcemimirRuleGroupRecordingCreate,
18 | ReadContext: resourcemimirRuleGroupRecordingRead,
19 | UpdateContext: resourcemimirRuleGroupRecordingUpdate,
20 | DeleteContext: resourcemimirRuleGroupRecordingDelete,
21 | Importer: &schema.ResourceImporter{
22 | StateContext: schema.ImportStatePassthroughContext,
23 | },
24 | Schema: map[string]*schema.Schema{
25 | "org_id": {
26 | Type: schema.TypeString,
27 | ForceNew: true,
28 | Optional: true,
29 | Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
30 | },
31 | "namespace": {
32 | Type: schema.TypeString,
33 | Description: "Recording Rule group namespace",
34 | ForceNew: true,
35 | Optional: true,
36 | Default: "default",
37 | },
38 | "name": {
39 | Type: schema.TypeString,
40 | Description: "Recording Rule group name",
41 | Required: true,
42 | ForceNew: true,
43 | ValidateFunc: validateGroupRuleName,
44 | },
45 | "interval": {
46 | Type: schema.TypeString,
47 | Description: "Recording Rule group interval",
48 | Optional: true,
49 | ValidateFunc: validateDuration,
50 | },
51 | "query_offset": {
52 | Type: schema.TypeString,
53 | Description: "The duration by which to delay the execution of the recording rule.",
54 | Optional: true,
55 | ConflictsWith: []string{"evaluation_delay"},
56 | ValidateFunc: validateDuration,
57 | },
58 | "evaluation_delay": {
59 | Type: schema.TypeString,
60 | Description: "**Deprecated** The duration by which to delay the execution of the recording rule.",
61 | Optional: true,
62 | Deprecated: "With Mimir >= 2.13, replaced by query_offset. This attribute will be removed when it is no longer supported in Mimir.",
63 | ConflictsWith: []string{"query_offset"},
64 | ValidateFunc: validateDuration,
65 | },
66 | "source_tenants": {
67 | Type: schema.TypeList,
68 | Optional: true,
69 | Description: "Allows aggregating data from multiple tenants while evaluating a rule group.",
70 | Elem: &schema.Schema{Type: schema.TypeString},
71 | },
72 | "rule": {
73 | Type: schema.TypeList,
74 | Required: true,
75 | Elem: &schema.Resource{
76 | Schema: map[string]*schema.Schema{
77 | "record": {
78 | Type: schema.TypeString,
79 | Required: true,
80 | Description: "The name of the time series to output to.",
81 | ValidateFunc: validateRecordingRuleName,
82 | },
83 | "expr": {
84 | Type: schema.TypeString,
85 | Required: true,
86 | Description: "The PromQL expression to evaluate.",
87 | ValidateFunc: validatePromQLExpr,
88 | StateFunc: formatPromQLExpr,
89 | },
90 | "labels": {
91 | Type: schema.TypeMap,
92 | Description: "Labels to add or overwrite before storing the result.",
93 | Optional: true,
94 | Elem: &schema.Schema{Type: schema.TypeString},
95 | ValidateFunc: validateLabels,
96 | },
97 | },
98 | },
99 | },
100 | }, /* End schema */
101 | }
102 | }
103 |
104 | func resourcemimirRuleGroupRecordingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
105 | client := meta.(*apiClient)
106 | name := d.Get("name").(string)
107 | namespace := d.Get("namespace").(string)
108 | orgID := d.Get("org_id").(string)
109 |
110 | if !overwriteRuleGroupConfig {
111 | ruleGroupConfigExists := true
112 |
113 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
114 | headers := make(map[string]string)
115 | if orgID != "" {
116 | headers["X-Scope-OrgID"] = orgID
117 | }
118 | _, err := client.sendRequest("ruler", "GET", path, "", headers)
119 | baseMsg := fmt.Sprintf("Cannot create recording rule group '%s' (namespace: %s) -", name, namespace)
120 | err = handleHTTPError(err, baseMsg)
121 | if err != nil {
122 | if strings.Contains(err.Error(), "response code '404'") {
123 | ruleGroupConfigExists = false
124 | } else {
125 | return diag.FromErr(err)
126 | }
127 | }
128 |
129 | if ruleGroupConfigExists {
130 | return diag.Errorf("recording rule group '%s' (namespace: %s) already exists", name, namespace)
131 | }
132 | }
133 |
134 | rules := &recordingRuleGroup{
135 | Name: name,
136 | Interval: d.Get("interval").(string),
137 | EvaluationDelay: d.Get("evaluation_delay").(string),
138 | QueryOffset: d.Get("query_offset").(string),
139 | SourceTenants: expandStringArray(d.Get("source_tenants").([]interface{})),
140 | Rules: expandRecordingRules(d.Get("rule").([]interface{})),
141 | }
142 | // if ed, ok := d.GetOk("evaluation_delay"); ok {
143 | // rules.EvaluationDelay = ed.(string)
144 | // } else {
145 | // rules.QueryOffset = d.Get("query_offset").(string)
146 | // }
147 | data, _ := yaml.Marshal(rules)
148 | headers := map[string]string{"Content-Type": "application/yaml"}
149 | if orgID != "" {
150 | headers["X-Scope-OrgID"] = orgID
151 | }
152 |
153 | path := fmt.Sprintf("/config/v1/rules/%s", namespace)
154 | _, err := client.sendRequest("ruler", "POST", path, string(data), headers)
155 | baseMsg := fmt.Sprintf("Cannot create recording rule group '%s' (namespace: %s) -", name, namespace)
156 | err = handleHTTPError(err, baseMsg)
157 | if err != nil {
158 | return diag.FromErr(err)
159 | }
160 | if orgID != "" {
161 | d.SetId(fmt.Sprintf("%s/%s/%s", orgID, namespace, name))
162 | } else {
163 | d.SetId(fmt.Sprintf("%s/%s", namespace, name))
164 | }
165 |
166 | // Retry read as mimir api could return a 404 status code caused by the event change notification propagation.
167 | // Add delay of * time.Second) between each retry with a max retries.
168 | for i := 1; i <= ruleGroupReadRetryAfterChange; i++ {
169 | result := resourcemimirRuleGroupRecordingRead(ctx, d, meta)
170 | if len(result) > 0 && !result.HasError() {
171 | log.Printf("[WARN] Recording rule group previously created'%s' not found (%d/3)", name, i)
172 | time.Sleep(ruleGroupReadDelayAfterChangeDuration)
173 | continue
174 | }
175 | return result
176 | }
177 | return resourcemimirRuleGroupRecordingRead(ctx, d, meta)
178 | }
179 |
180 | func resourcemimirRuleGroupRecordingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
181 | var diags diag.Diagnostics
182 |
183 | // use id as read is also called by import
184 | idArr := strings.Split(d.Id(), "/")
185 |
186 | var name, namespace, orgID string
187 |
188 | switch len(idArr) {
189 | case 2:
190 | namespace = idArr[0]
191 | name = idArr[1]
192 | case 3:
193 | orgID = idArr[0]
194 | namespace = idArr[1]
195 | name = idArr[2]
196 | default:
197 | return diag.FromErr(fmt.Errorf("invalid id format: expected 'namespace/name' or 'org_id/namespace/name', got '%s'", d.Id()))
198 | }
199 |
200 | jobraw, err := ruleGroupRecordingRead(meta, name, namespace, orgID)
201 | if err != nil {
202 | if d.IsNewResource() && strings.Contains(err.Error(), "response code '404'") {
203 | diags = append(diags, diag.Diagnostic{
204 | Severity: diag.Warning,
205 | Summary: fmt.Sprintf("Recording rule group '%s' (namespace: %s) not found. You should increase the provider parameter 'rule_group_read_delay_after_change' (current: %s)", name, namespace, ruleGroupReadDelayAfterChange),
206 | })
207 | return diags
208 | } else if !d.IsNewResource() && strings.Contains(err.Error(), "response code '404'") {
209 | diags = append(diags, diag.Diagnostic{
210 | Severity: diag.Warning,
211 | Summary: fmt.Sprintf("Recording rule group '%s' (id: %s) not found, removing from state", name, d.Id()),
212 | })
213 | d.SetId("")
214 | return diags
215 | }
216 | return diag.FromErr(err)
217 | }
218 |
219 | var data recordingRuleGroup
220 | err = yaml.Unmarshal([]byte(jobraw), &data)
221 | if err != nil {
222 | return diag.FromErr(fmt.Errorf("unable to decode recording namespace rule group '%s' data: %v", name, err))
223 | }
224 |
225 | if err := d.Set("rule", flattenRecordingRules(data.Rules)); err != nil {
226 | return diag.FromErr(err)
227 | }
228 |
229 | err = d.Set("org_id", orgID)
230 | if err != nil {
231 | return diag.FromErr(err)
232 | }
233 | err = d.Set("namespace", namespace)
234 | if err != nil {
235 | return diag.FromErr(err)
236 | }
237 | err = d.Set("name", name)
238 | if err != nil {
239 | return diag.FromErr(err)
240 | }
241 | err = d.Set("interval", data.Interval)
242 | if err != nil {
243 | return diag.FromErr(err)
244 | }
245 | if _, ok := d.GetOk("evaluation_delay"); ok {
246 | err = d.Set("evaluation_delay", data.EvaluationDelay)
247 | if err != nil {
248 | return diag.FromErr(err)
249 | }
250 | } else {
251 | err = d.Set("query_offset", data.QueryOffset)
252 | if err != nil {
253 | return diag.FromErr(err)
254 | }
255 | }
256 | err = d.Set("source_tenants", data.SourceTenants)
257 | if err != nil {
258 | return diag.FromErr(err)
259 | }
260 | return diags
261 | }
262 |
263 | func resourcemimirRuleGroupRecordingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
264 | if d.HasChanges("rule", "interval", "query_offset", "evaluation_delay", "source_tenants") {
265 | client := meta.(*apiClient)
266 | name := d.Get("name").(string)
267 | namespace := d.Get("namespace").(string)
268 | orgID := d.Get("org_id").(string)
269 |
270 | rules := &recordingRuleGroup{
271 | Name: name,
272 | Interval: d.Get("interval").(string),
273 | SourceTenants: expandStringArray(d.Get("source_tenants").([]interface{})),
274 | Rules: expandRecordingRules(d.Get("rule").([]interface{})),
275 | }
276 | if ed, ok := d.GetOk("evaluation_delay"); ok {
277 | rules.EvaluationDelay = ed.(string)
278 | } else {
279 | rules.QueryOffset = d.Get("query_offset").(string)
280 | }
281 | data, _ := yaml.Marshal(rules)
282 | headers := map[string]string{"Content-Type": "application/yaml"}
283 | if orgID != "" {
284 | headers["X-Scope-OrgID"] = orgID
285 | }
286 |
287 | path := fmt.Sprintf("/config/v1/rules/%s", namespace)
288 | _, err := client.sendRequest("ruler", "POST", path, string(data), headers)
289 | baseMsg := fmt.Sprintf("Cannot update recording rule group '%s' (namespace: %s) -", name, namespace)
290 | err = handleHTTPError(err, baseMsg)
291 | if err != nil {
292 | return diag.FromErr(err)
293 | }
294 | }
295 | // Add time delay before read to wait the event change notification propagation to finish
296 | time.Sleep(ruleGroupReadDelayAfterChangeDuration)
297 | return resourcemimirRuleGroupRecordingRead(ctx, d, meta)
298 | }
299 |
300 | func resourcemimirRuleGroupRecordingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
301 | client := meta.(*apiClient)
302 | name := d.Get("name").(string)
303 | namespace := d.Get("namespace").(string)
304 | orgID := d.Get("org_id").(string)
305 |
306 | headers := make(map[string]string)
307 | if orgID != "" {
308 | headers["X-Scope-OrgID"] = orgID
309 | }
310 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
311 | _, err := client.sendRequest("ruler", "DELETE", path, "", headers)
312 | if err != nil {
313 | return diag.FromErr(fmt.Errorf(
314 | "cannot delete recording rule group '%s' from %s: %v",
315 | name,
316 | fmt.Sprintf("%s%s", client.uri, path),
317 | err))
318 | }
319 | // Retry read as mimir api could return a 200 status code but the rule group still exist because of the event change notification propagation latency.
320 | // Add delay of * time.Second) between each retry with a max retries.
321 | for i := 1; i <= ruleGroupReadRetryAfterChange; i++ {
322 | _, err := ruleGroupRecordingRead(meta, name, namespace, orgID)
323 | if err == nil {
324 | log.Printf("[WARN] Recording rule group previously deleted '%s' still exist (%d/3)", name, i)
325 | time.Sleep(ruleGroupReadDelayAfterChangeDuration)
326 | continue
327 | } else if strings.Contains(err.Error(), "response code '404'") {
328 | break
329 | }
330 | return diag.FromErr(err)
331 | }
332 | d.SetId("")
333 | return diag.Diagnostics{}
334 | }
335 |
336 | func ruleGroupRecordingRead(meta interface{}, name, namespace, orgID string) (string, error) {
337 | headers := make(map[string]string)
338 | if orgID != "" {
339 | headers["X-Scope-OrgID"] = orgID
340 | }
341 | client := meta.(*apiClient)
342 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
343 | jobraw, err := client.sendRequest("ruler", "GET", path, "", headers)
344 | baseMsg := fmt.Sprintf("Cannot read recording rule group '%s' (namespace: %s) -", name, namespace)
345 | return jobraw, handleHTTPError(err, baseMsg)
346 | }
347 |
348 | func expandRecordingRules(v []interface{}) []recordingRule {
349 | var rules []recordingRule
350 |
351 | for _, v := range v {
352 | var rule recordingRule
353 | data := v.(map[string]interface{})
354 |
355 | if raw, ok := data["record"]; ok {
356 | rule.Record = raw.(string)
357 | }
358 |
359 | if raw, ok := data["expr"]; ok {
360 | rule.Expr = formatPromQLExpr(raw)
361 | }
362 |
363 | if raw, ok := data["labels"]; ok {
364 | if len(raw.(map[string]interface{})) > 0 {
365 | rule.Labels = expandStringMap(raw.(map[string]interface{}))
366 | }
367 | }
368 |
369 | rules = append(rules, rule)
370 | }
371 |
372 | return rules
373 | }
374 |
375 | func flattenRecordingRules(v []recordingRule) []map[string]interface{} {
376 | var rules []map[string]interface{}
377 |
378 | if v == nil {
379 | return rules
380 | }
381 |
382 | for _, v := range v {
383 | rule := make(map[string]interface{})
384 | rule["record"] = v.Record
385 | rule["expr"] = formatPromQLExpr(v.Expr)
386 |
387 | if v.Labels != nil {
388 | rule["labels"] = v.Labels
389 | }
390 |
391 | rules = append(rules, rule)
392 | }
393 |
394 | return rules
395 | }
396 |
397 | func validateRecordingRuleName(v interface{}, k string) (ws []string, errors []error) {
398 | value := v.(string)
399 |
400 | if !metricNameRegexp.MatchString(value) {
401 | errors = append(errors, fmt.Errorf(
402 | "\"%s\": Invalid Recording Rule Name %q. Must match the regex %s", k, value, metricNameRegexp))
403 | }
404 |
405 | return
406 | }
407 |
408 | type recordingRule struct {
409 | Record string `json:"record"`
410 | Expr string `json:"expr"`
411 | Labels map[string]string `yaml:"labels,omitempty"`
412 | }
413 |
414 | type recordingRuleGroup struct {
415 | Name string `yaml:"name"`
416 | Interval string `yaml:"interval,omitempty"`
417 | QueryOffset string `yaml:"query_offset,omitempty"`
418 | EvaluationDelay string `yaml:"evaluation_delay,omitempty"`
419 | Rules []recordingRule `yaml:"rules"`
420 | SourceTenants []string `yaml:"source_tenants,omitempty"`
421 | }
422 |
--------------------------------------------------------------------------------
/mimir/resource_mimir_rule_group_alerting.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "strings"
8 | "time"
9 |
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12 | "gopkg.in/yaml.v3"
13 | )
14 |
15 | func resourcemimirRuleGroupAlerting() *schema.Resource {
16 | return &schema.Resource{
17 | CreateContext: resourcemimirRuleGroupAlertingCreate,
18 | ReadContext: resourcemimirRuleGroupAlertingRead,
19 | UpdateContext: resourcemimirRuleGroupAlertingUpdate,
20 | DeleteContext: resourcemimirRuleGroupAlertingDelete,
21 | Importer: &schema.ResourceImporter{
22 | StateContext: schema.ImportStatePassthroughContext,
23 | },
24 | Schema: map[string]*schema.Schema{
25 | "org_id": {
26 | Type: schema.TypeString,
27 | ForceNew: true,
28 | Optional: true,
29 | Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
30 | },
31 | "namespace": {
32 | Type: schema.TypeString,
33 | Description: "Alerting Rule group namespace",
34 | ForceNew: true,
35 | Optional: true,
36 | Default: "default",
37 | },
38 | "name": {
39 | Type: schema.TypeString,
40 | Description: "Alerting Rule group name",
41 | Required: true,
42 | ForceNew: true,
43 | ValidateFunc: validateGroupRuleName,
44 | },
45 | "interval": {
46 | Type: schema.TypeString,
47 | Description: "Alerting Rule group interval",
48 | Optional: true,
49 | ValidateFunc: validateDuration,
50 | },
51 | "source_tenants": {
52 | Type: schema.TypeList,
53 | Optional: true,
54 | Description: "Allows aggregating data from multiple tenants while evaluating a rule group.",
55 | Elem: &schema.Schema{Type: schema.TypeString},
56 | },
57 | "rule": {
58 | Type: schema.TypeList,
59 | Required: true,
60 | Elem: &schema.Resource{
61 | Schema: map[string]*schema.Schema{
62 | "alert": {
63 | Type: schema.TypeString,
64 | Description: "The name of the alert.",
65 | Required: true,
66 | ValidateFunc: validateAlertingRuleName,
67 | },
68 | "expr": {
69 | Type: schema.TypeString,
70 | Description: "The PromQL expression to evaluate.",
71 | Required: true,
72 | ValidateFunc: validatePromQLExpr,
73 | StateFunc: formatPromQLExpr,
74 | },
75 | "for": {
76 | Type: schema.TypeString,
77 | Description: "The duration for which the condition must be true before an alert fires.",
78 | Optional: true,
79 | ValidateFunc: validateDuration,
80 | StateFunc: formatDuration,
81 | },
82 | "keep_firing_for": {
83 | Type: schema.TypeString,
84 | Description: "How long an alert will continue firing after the condition that triggered it has cleared.",
85 | Optional: true,
86 | ValidateFunc: validateDuration,
87 | StateFunc: formatDuration,
88 | },
89 | "annotations": {
90 | Type: schema.TypeMap,
91 | Description: "Annotations to add to each alert.",
92 | Optional: true,
93 | Elem: &schema.Schema{Type: schema.TypeString},
94 | ValidateFunc: validateAnnotations,
95 | },
96 | "labels": {
97 | Type: schema.TypeMap,
98 | Description: "Labels to add or overwrite for each alert.",
99 | Optional: true,
100 | Elem: &schema.Schema{Type: schema.TypeString},
101 | ValidateFunc: validateLabels,
102 | },
103 | },
104 | },
105 | },
106 | }, /* End schema */
107 | }
108 | }
109 |
110 | func resourcemimirRuleGroupAlertingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
111 | client := meta.(*apiClient)
112 | name := d.Get("name").(string)
113 | namespace := d.Get("namespace").(string)
114 | orgID := d.Get("org_id").(string)
115 |
116 | if !overwriteRuleGroupConfig {
117 | ruleGroupConfigExists := true
118 |
119 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
120 | headers := make(map[string]string)
121 | if orgID != "" {
122 | headers["X-Scope-OrgID"] = orgID
123 | }
124 | _, err := client.sendRequest("ruler", "GET", path, "", headers)
125 | baseMsg := fmt.Sprintf("Cannot create alerting rule group '%s' (namespace: %s) -", name, namespace)
126 | err = handleHTTPError(err, baseMsg)
127 | if err != nil {
128 | if strings.Contains(err.Error(), "response code '404'") {
129 | ruleGroupConfigExists = false
130 | } else {
131 | return diag.FromErr(err)
132 | }
133 | }
134 |
135 | if ruleGroupConfigExists {
136 | return diag.Errorf("alerting rule group '%s' (namespace: %s) already exists", name, namespace)
137 | }
138 | }
139 |
140 | rules := &alertingRuleGroup{
141 | Name: name,
142 | Interval: d.Get("interval").(string),
143 | SourceTenants: expandStringArray(d.Get("source_tenants").([]interface{})),
144 | Rules: expandAlertingRules(d.Get("rule").([]interface{})),
145 | }
146 | data, _ := yaml.Marshal(rules)
147 | headers := map[string]string{"Content-Type": "application/yaml"}
148 | if orgID != "" {
149 | headers["X-Scope-OrgID"] = orgID
150 | }
151 |
152 | path := fmt.Sprintf("/config/v1/rules/%s", namespace)
153 | _, err := client.sendRequest("ruler", "POST", path, string(data), headers)
154 | baseMsg := fmt.Sprintf("Cannot create alerting rule group '%s' (namespace: %s) -", name, namespace)
155 | err = handleHTTPError(err, baseMsg)
156 | if err != nil {
157 | return diag.FromErr(err)
158 | }
159 | if orgID != "" {
160 | d.SetId(fmt.Sprintf("%s/%s/%s", orgID, namespace, name))
161 | } else {
162 | d.SetId(fmt.Sprintf("%s/%s", namespace, name))
163 | }
164 |
165 | // Retry read as mimir api could return a 404 status code caused by the event change notification propagation.
166 | // Add delay of * time.Second) between each retry with a max retries.
167 | for i := 1; i <= ruleGroupReadRetryAfterChange; i++ {
168 | result := resourcemimirRuleGroupAlertingRead(ctx, d, meta)
169 | if len(result) > 0 && !result.HasError() {
170 | log.Printf("[WARN] Alerting rule group previously created'%s' not found (%d/3)", name, i)
171 | time.Sleep(ruleGroupReadDelayAfterChangeDuration)
172 | continue
173 | }
174 | return result
175 | }
176 | return resourcemimirRuleGroupAlertingRead(ctx, d, meta)
177 | }
178 |
179 | func resourcemimirRuleGroupAlertingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
180 | var diags diag.Diagnostics
181 |
182 | // use id as read is also called by import
183 | idArr := strings.Split(d.Id(), "/")
184 |
185 | var name, namespace, orgID string
186 |
187 | switch len(idArr) {
188 | case 2:
189 | namespace = idArr[0]
190 | name = idArr[1]
191 | case 3:
192 | orgID = idArr[0]
193 | namespace = idArr[1]
194 | name = idArr[2]
195 | default:
196 | return diag.FromErr(fmt.Errorf("invalid id format: expected 'namespace/name' or 'org_id/namespace/name', got '%s'", d.Id()))
197 | }
198 |
199 | jobraw, err := ruleGroupAlertingRead(meta, name, namespace, orgID)
200 | if err != nil {
201 | if d.IsNewResource() && strings.Contains(err.Error(), "response code '404'") {
202 | diags = append(diags, diag.Diagnostic{
203 | Severity: diag.Warning,
204 | Summary: fmt.Sprintf("Alerting rule group '%s' not found. You should increase the provider parameter 'rule_group_read_delay_after_change' (current: %s)", name, ruleGroupReadDelayAfterChange),
205 | })
206 | return diags
207 | } else if !d.IsNewResource() && strings.Contains(err.Error(), "response code '404'") {
208 | diags = append(diags, diag.Diagnostic{
209 | Severity: diag.Warning,
210 | Summary: fmt.Sprintf("Alerting rule group '%s' (id: %s) not found, removing from state", name, d.Id()),
211 | })
212 | d.SetId("")
213 | return diags
214 | }
215 | return diag.FromErr(err)
216 | }
217 |
218 | var data alertingRuleGroup
219 | err = yaml.Unmarshal([]byte(jobraw), &data)
220 | if err != nil {
221 | return diag.FromErr(fmt.Errorf("unable to decode alerting namespace rule group '%s' data: %v", name, err))
222 | }
223 |
224 | if err := d.Set("rule", flattenAlertingRules(data.Rules)); err != nil {
225 | return diag.FromErr(err)
226 | }
227 |
228 | err = d.Set("org_id", orgID)
229 | if err != nil {
230 | return diag.FromErr(err)
231 | }
232 | err = d.Set("namespace", namespace)
233 | if err != nil {
234 | return diag.FromErr(err)
235 | }
236 | err = d.Set("name", name)
237 | if err != nil {
238 | return diag.FromErr(err)
239 | }
240 | err = d.Set("interval", data.Interval)
241 | if err != nil {
242 | return diag.FromErr(err)
243 | }
244 | err = d.Set("source_tenants", data.SourceTenants)
245 | if err != nil {
246 | return diag.FromErr(err)
247 | }
248 |
249 | return diags
250 | }
251 |
252 | func resourcemimirRuleGroupAlertingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
253 | if d.HasChanges("rule", "interval", "source_tenants") {
254 | client := meta.(*apiClient)
255 | name := d.Get("name").(string)
256 | namespace := d.Get("namespace").(string)
257 | orgID := d.Get("org_id").(string)
258 |
259 | rules := &alertingRuleGroup{
260 | Name: name,
261 | Interval: d.Get("interval").(string),
262 | SourceTenants: expandStringArray(d.Get("source_tenants").([]interface{})),
263 | Rules: expandAlertingRules(d.Get("rule").([]interface{})),
264 | }
265 | data, _ := yaml.Marshal(rules)
266 | headers := map[string]string{"Content-Type": "application/yaml"}
267 | if orgID != "" {
268 | headers["X-Scope-OrgID"] = orgID
269 | }
270 |
271 | path := fmt.Sprintf("/config/v1/rules/%s", namespace)
272 | _, err := client.sendRequest("ruler", "POST", path, string(data), headers)
273 | baseMsg := fmt.Sprintf("Cannot update alerting rule group '%s' (namespace: %s) -", name, namespace)
274 |
275 | err = handleHTTPError(err, baseMsg)
276 | if err != nil {
277 | return diag.FromErr(err)
278 | }
279 | }
280 | // Add time delay before read to wait the event change notification propagation to finish
281 | time.Sleep(ruleGroupReadDelayAfterChangeDuration)
282 | return resourcemimirRuleGroupAlertingRead(ctx, d, meta)
283 | }
284 |
285 | func resourcemimirRuleGroupAlertingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
286 | client := meta.(*apiClient)
287 | name := d.Get("name").(string)
288 | namespace := d.Get("namespace").(string)
289 | orgID := d.Get("org_id").(string)
290 |
291 | headers := make(map[string]string)
292 | if orgID != "" {
293 | headers["X-Scope-OrgID"] = orgID
294 | }
295 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
296 | _, err := client.sendRequest("ruler", "DELETE", path, "", headers)
297 | if err != nil {
298 | return diag.FromErr(fmt.Errorf(
299 | "cannot delete alerting rule group '%s' from %s: %v",
300 | name,
301 | fmt.Sprintf("%s%s", client.uri, path),
302 | err))
303 | }
304 | // Retry read as mimir api could return a 200 status code but the rule group still exist because of the event change notification propagation latency.
305 | // Add delay of * time.Second) between each retry with a max retries.
306 | for i := 1; i <= ruleGroupReadRetryAfterChange; i++ {
307 | _, err := ruleGroupAlertingRead(meta, name, namespace, orgID)
308 | if err == nil {
309 | log.Printf("[WARN] Alerting rule group previously deleted '%s' still exist (%d/3)", name, i)
310 | time.Sleep(ruleGroupReadDelayAfterChangeDuration)
311 | continue
312 | } else if strings.Contains(err.Error(), "response code '404'") {
313 | break
314 | }
315 | return diag.FromErr(err)
316 | }
317 | d.SetId("")
318 | return diag.Diagnostics{}
319 | }
320 |
321 | func ruleGroupAlertingRead(meta interface{}, name, namespace, orgID string) (string, error) {
322 | headers := make(map[string]string)
323 | if orgID != "" {
324 | headers["X-Scope-OrgID"] = orgID
325 | }
326 | client := meta.(*apiClient)
327 | path := fmt.Sprintf("/config/v1/rules/%s/%s", namespace, name)
328 | jobraw, err := client.sendRequest("ruler", "GET", path, "", headers)
329 | baseMsg := fmt.Sprintf("Cannot read alerting rule group '%s' (namespace: %s) -", name, namespace)
330 | return jobraw, handleHTTPError(err, baseMsg)
331 | }
332 |
333 | func expandAlertingRules(v []interface{}) []alertingRule {
334 | var rules []alertingRule
335 |
336 | for _, v := range v {
337 | var rule alertingRule
338 | data := v.(map[string]interface{})
339 |
340 | if raw, ok := data["alert"]; ok {
341 | rule.Alert = raw.(string)
342 | }
343 |
344 | if raw, ok := data["expr"]; ok {
345 | rule.Expr = formatPromQLExpr(raw)
346 | }
347 |
348 | if raw, ok := data["for"]; ok {
349 | if raw.(string) != "" {
350 | rule.For = raw.(string)
351 | }
352 | }
353 |
354 | if raw, ok := data["keep_firing_for"]; ok {
355 | if raw.(string) != "" {
356 | rule.KeepFiringFor = raw.(string)
357 | }
358 | }
359 |
360 | if raw, ok := data["labels"]; ok {
361 | if len(raw.(map[string]interface{})) > 0 {
362 | rule.Labels = expandStringMap(raw.(map[string]interface{}))
363 | }
364 | }
365 |
366 | if raw, ok := data["annotations"]; ok {
367 | if len(raw.(map[string]interface{})) > 0 {
368 | rule.Annotations = expandStringMap(raw.(map[string]interface{}))
369 | }
370 | }
371 |
372 | rules = append(rules, rule)
373 | }
374 |
375 | return rules
376 | }
377 |
378 | func flattenAlertingRules(v []alertingRule) []map[string]interface{} {
379 | var rules []map[string]interface{}
380 |
381 | if v == nil {
382 | return rules
383 | }
384 |
385 | for _, v := range v {
386 | rule := make(map[string]interface{})
387 | rule["alert"] = v.Alert
388 | rule["expr"] = formatPromQLExpr(v.Expr)
389 |
390 | if v.For != "" {
391 | rule["for"] = v.For
392 | }
393 | if v.KeepFiringFor != "" {
394 | rule["keep_firing_for"] = v.KeepFiringFor
395 | }
396 | if v.Labels != nil {
397 | rule["labels"] = v.Labels
398 | }
399 | if v.Annotations != nil {
400 | rule["annotations"] = v.Annotations
401 | }
402 |
403 | rules = append(rules, rule)
404 | }
405 |
406 | return rules
407 | }
408 |
409 | func validateAlertingRuleName(v interface{}, k string) (ws []string, errors []error) {
410 | value := v.(string)
411 |
412 | if !groupRuleNameRegexp.MatchString(value) {
413 | errors = append(errors, fmt.Errorf(
414 | "\"%s\": Invalid Alerting Rule Name %q. Must match the regex %s", k, value, groupRuleNameRegexp))
415 | }
416 |
417 | return
418 | }
419 |
420 | type alertingRule struct {
421 | Alert string `yaml:"alert"`
422 | Expr string `yaml:"expr"`
423 | For string `yaml:"for,omitempty"`
424 | KeepFiringFor string `yaml:"keep_firing_for,omitempty"`
425 | Labels map[string]string `yaml:"labels,omitempty"`
426 | Annotations map[string]string `yaml:"annotations,omitempty"`
427 | }
428 |
429 | type alertingRuleGroup struct {
430 | Name string `yaml:"name"`
431 | Interval string `yaml:"interval,omitempty"`
432 | Rules []alertingRule `yaml:"rules"`
433 | SourceTenants []string `yaml:"source_tenants,omitempty"`
434 | }
435 |
--------------------------------------------------------------------------------
/mimir/resource_mimir_rules_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "testing"
7 |
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
9 | )
10 |
11 | func TestValidateRuleGroupsContent_AllowsPrometheusDurations(t *testing.T) {
12 | ruleGroups := RuleGroups{
13 | Groups: []RuleGroup{
14 | {
15 | Name: "alert_group",
16 | Interval: "1m",
17 | Rules: []Rule{
18 | {
19 | Alert: "HighErrorRate",
20 | Expr: `rate(http_requests_total{status=~"5.."}[5m]) > 0`,
21 | For: "1d",
22 | },
23 | },
24 | },
25 | },
26 | }
27 |
28 | if err := validateRuleGroupsContent(ruleGroups); err != nil {
29 | t.Fatalf("expected Prometheus-style durations to be valid, got: %v", err)
30 | }
31 | }
32 |
33 | func TestAccResourceMimirRules_Basic(t *testing.T) {
34 | // Init client
35 | client, err := NewAPIClient(setupClient())
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 |
40 | resource.Test(t, resource.TestCase{
41 | PreCheck: func() { testAccPreCheck(t) },
42 | ProviderFactories: testAccProviderFactories,
43 | CheckDestroy: testAccCheckMimirRuleDestroy,
44 | Steps: []resource.TestStep{
45 | {
46 | Config: testAccResourceMimirRules_basic,
47 | Check: resource.ComposeTestCheckFunc(
48 | testAccCheckMimirNamespaceExists("mimir_rules.rules_1", "alert_group_1", client),
49 | testAccCheckMimirNamespaceExists("mimir_rules.rules_1", "record_group_1", client),
50 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "namespace", "namespace_1"),
51 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "groups_count", "2"),
52 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "total_rules", "3"),
53 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "managed_groups.0", "alert_group_1"),
54 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "managed_groups.1", "record_group_1"),
55 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "rule_names.0", "HighCPUUsage"),
56 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "rule_names.1", "HighMemoryUsage"),
57 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "rule_names.2", "instance:cpu:rate5m"),
58 | ),
59 | },
60 | {
61 | Config: testAccResourceMimirRules_basic_update,
62 | Check: resource.ComposeTestCheckFunc(
63 | testAccCheckMimirNamespaceExists("mimir_rules.rules_1", "alert_group_1", client),
64 | testAccCheckMimirNamespaceExists("mimir_rules.rules_1", "record_group_1", client),
65 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "namespace", "namespace_1"),
66 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "groups_count", "2"),
67 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "total_rules", "4"),
68 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "rule_names.2", "LowDiskSpace"),
69 | ),
70 | },
71 | },
72 | })
73 | }
74 |
75 | func TestAccResourceMimirRules_ContentFile(t *testing.T) {
76 | // Init client
77 | client, err := NewAPIClient(setupClient())
78 | if err != nil {
79 | t.Fatal(err)
80 | }
81 |
82 | // Create temporary YAML file
83 | tmpfile, err := os.CreateTemp("", "rules-*.yaml")
84 | if err != nil {
85 | t.Fatal(err)
86 | }
87 | defer os.Remove(tmpfile.Name())
88 |
89 | content := `groups:
90 | - name: file_based_alerts
91 | interval: 1m
92 | rules:
93 | - alert: TestAlert
94 | expr: up == 0
95 | for: 5m
96 | labels:
97 | severity: warning
98 | `
99 | if _, err := tmpfile.Write([]byte(content)); err != nil {
100 | t.Fatal(err)
101 | }
102 | if err := tmpfile.Close(); err != nil {
103 | t.Fatal(err)
104 | }
105 |
106 | resource.Test(t, resource.TestCase{
107 | PreCheck: func() { testAccPreCheck(t) },
108 | ProviderFactories: testAccProviderFactories,
109 | CheckDestroy: testAccCheckMimirRuleDestroy,
110 | Steps: []resource.TestStep{
111 | {
112 | Config: testAccResourceMimirRules_contentFile(tmpfile.Name()),
113 | Check: resource.ComposeTestCheckFunc(
114 | testAccCheckMimirNamespaceExists("mimir_rules.rules_file", "file_based_alerts", client),
115 | resource.TestCheckResourceAttr("mimir_rules.rules_file", "namespace", "namespace_1"),
116 | resource.TestCheckResourceAttr("mimir_rules.rules_file", "groups_count", "1"),
117 | resource.TestCheckResourceAttr("mimir_rules.rules_file", "total_rules", "1"),
118 | resource.TestCheckResourceAttr("mimir_rules.rules_file", "rule_names.0", "TestAlert"),
119 | ),
120 | },
121 | },
122 | })
123 | }
124 |
125 | func TestAccResourceMimirRules_OnlyGroups(t *testing.T) {
126 | // Init client
127 | client, err := NewAPIClient(setupClient())
128 | if err != nil {
129 | t.Fatal(err)
130 | }
131 |
132 | resource.Test(t, resource.TestCase{
133 | PreCheck: func() { testAccPreCheck(t) },
134 | ProviderFactories: testAccProviderFactories,
135 | CheckDestroy: testAccCheckMimirRuleDestroy,
136 | Steps: []resource.TestStep{
137 | {
138 | Config: testAccResourceMimirRules_onlyGroups,
139 | Check: resource.ComposeTestCheckFunc(
140 | testAccCheckMimirNamespaceExists("mimir_rules.rules_filtered", "alert_group_1", client),
141 | resource.TestCheckResourceAttr("mimir_rules.rules_filtered", "namespace", "namespace_1"),
142 | resource.TestCheckResourceAttr("mimir_rules.rules_filtered", "groups_count", "1"),
143 | resource.TestCheckResourceAttr("mimir_rules.rules_filtered", "total_rules", "2"),
144 | resource.TestCheckResourceAttr("mimir_rules.rules_filtered", "managed_groups.0", "alert_group_1"),
145 | ),
146 | },
147 | },
148 | })
149 | }
150 |
151 | func TestAccResourceMimirRules_IgnoreGroups(t *testing.T) {
152 | // Init client
153 | client, err := NewAPIClient(setupClient())
154 | if err != nil {
155 | t.Fatal(err)
156 | }
157 |
158 | resource.Test(t, resource.TestCase{
159 | PreCheck: func() { testAccPreCheck(t) },
160 | ProviderFactories: testAccProviderFactories,
161 | CheckDestroy: testAccCheckMimirRuleDestroy,
162 | Steps: []resource.TestStep{
163 | {
164 | Config: testAccResourceMimirRules_ignoreGroups,
165 | Check: resource.ComposeTestCheckFunc(
166 | testAccCheckMimirNamespaceExists("mimir_rules.rules_ignored", "alert_group_1", client),
167 | resource.TestCheckResourceAttr("mimir_rules.rules_ignored", "namespace", "namespace_1"),
168 | resource.TestCheckResourceAttr("mimir_rules.rules_ignored", "groups_count", "1"),
169 | resource.TestCheckResourceAttr("mimir_rules.rules_ignored", "managed_groups.0", "alert_group_1"),
170 | ),
171 | },
172 | },
173 | })
174 | }
175 |
176 | func TestAccResourceMimirRules_WithOrgID(t *testing.T) {
177 | // Init client
178 | client, err := NewAPIClient(setupClient())
179 | if err != nil {
180 | t.Fatal(err)
181 | }
182 |
183 | resource.Test(t, resource.TestCase{
184 | PreCheck: func() { testAccPreCheck(t) },
185 | ProviderFactories: testAccProviderFactories,
186 | CheckDestroy: testAccCheckMimirRuleDestroy,
187 | Steps: []resource.TestStep{
188 | {
189 | Config: testAccResourceMimirRules_withOrgID,
190 | Check: resource.ComposeTestCheckFunc(
191 | testAccCheckMimirNamespaceExists("mimir_rules.rules_with_org", "alert_group_1", client),
192 | resource.TestCheckResourceAttr("mimir_rules.rules_with_org", "org_id", "another_tenant"),
193 | resource.TestCheckResourceAttr("mimir_rules.rules_with_org", "namespace", "namespace_1"),
194 | resource.TestCheckResourceAttr("mimir_rules.rules_with_org", "groups_count", "1"),
195 | ),
196 | },
197 | },
198 | })
199 | }
200 |
201 | func TestAccResourceMimirRules_Federated(t *testing.T) {
202 | // Init client
203 | client, err := NewAPIClient(setupClient())
204 | if err != nil {
205 | t.Fatal(err)
206 | }
207 |
208 | resource.Test(t, resource.TestCase{
209 | PreCheck: func() { testAccPreCheck(t) },
210 | ProviderFactories: testAccProviderFactories,
211 | CheckDestroy: testAccCheckMimirRuleDestroy,
212 | Steps: []resource.TestStep{
213 | {
214 | Config: testAccResourceMimirRules_federated,
215 | Check: resource.ComposeTestCheckFunc(
216 | testAccCheckMimirNamespaceExists("mimir_rules.rules_federated", "federated_group", client),
217 | resource.TestCheckResourceAttr("mimir_rules.rules_federated", "namespace", "namespace_1"),
218 | resource.TestCheckResourceAttr("mimir_rules.rules_federated", "groups_count", "1"),
219 | ),
220 | },
221 | },
222 | })
223 | }
224 |
225 | func TestAccResourceMimirRules_Update_AddGroup(t *testing.T) {
226 | // Init client
227 | client, err := NewAPIClient(setupClient())
228 | if err != nil {
229 | t.Fatal(err)
230 | }
231 |
232 | resource.Test(t, resource.TestCase{
233 | PreCheck: func() { testAccPreCheck(t) },
234 | ProviderFactories: testAccProviderFactories,
235 | CheckDestroy: testAccCheckMimirRuleDestroy,
236 | Steps: []resource.TestStep{
237 | {
238 | Config: testAccResourceMimirRules_basic,
239 | Check: resource.ComposeTestCheckFunc(
240 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "groups_count", "2"),
241 | ),
242 | },
243 | {
244 | Config: testAccResourceMimirRules_addGroup,
245 | Check: resource.ComposeTestCheckFunc(
246 | testAccCheckMimirNamespaceExists("mimir_rules.rules_1", "alert_group_1", client),
247 | testAccCheckMimirNamespaceExists("mimir_rules.rules_1", "record_group_1", client),
248 | testAccCheckMimirNamespaceExists("mimir_rules.rules_1", "new_group", client),
249 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "groups_count", "3"),
250 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "managed_groups.2", "new_group"),
251 | ),
252 | },
253 | },
254 | })
255 | }
256 |
257 | func TestAccResourceMimirRules_Update_RemoveGroup(t *testing.T) {
258 | // Init client
259 | client, err := NewAPIClient(setupClient())
260 | if err != nil {
261 | t.Fatal(err)
262 | }
263 |
264 | resource.Test(t, resource.TestCase{
265 | PreCheck: func() { testAccPreCheck(t) },
266 | ProviderFactories: testAccProviderFactories,
267 | CheckDestroy: testAccCheckMimirRuleDestroy,
268 | Steps: []resource.TestStep{
269 | {
270 | Config: testAccResourceMimirRules_basic,
271 | Check: resource.ComposeTestCheckFunc(
272 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "groups_count", "2"),
273 | ),
274 | },
275 | {
276 | Config: testAccResourceMimirRules_removeGroup,
277 | Check: resource.ComposeTestCheckFunc(
278 | testAccCheckMimirNamespaceExists("mimir_rules.rules_1", "alert_group_1", client),
279 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "groups_count", "1"),
280 | resource.TestCheckResourceAttr("mimir_rules.rules_1", "managed_groups.0", "alert_group_1"),
281 | ),
282 | },
283 | },
284 | })
285 | }
286 |
287 | // Test configurations
288 |
289 | const testAccResourceMimirRules_basic = `
290 | resource "mimir_rules" "rules_1" {
291 | namespace = "namespace_1"
292 |
293 | content = <<-EOT
294 | groups:
295 | - name: alert_group_1
296 | interval: 30s
297 | rules:
298 | - alert: HighCPUUsage
299 | expr: cpu_usage > 80
300 | for: 5m
301 | labels:
302 | severity: warning
303 | annotations:
304 | summary: "High CPU usage detected"
305 | - alert: HighMemoryUsage
306 | expr: memory_usage > 90
307 | for: 3m
308 | labels:
309 | severity: critical
310 | annotations:
311 | summary: "High memory usage detected"
312 |
313 | - name: record_group_1
314 | interval: 1m
315 | rules:
316 | - record: instance:cpu:rate5m
317 | expr: rate(cpu_total[5m])
318 | labels:
319 | job: monitoring
320 | EOT
321 | }
322 | `
323 |
324 | const testAccResourceMimirRules_basic_update = `
325 | resource "mimir_rules" "rules_1" {
326 | namespace = "namespace_1"
327 |
328 | content = <<-EOT
329 | groups:
330 | - name: alert_group_1
331 | interval: 30s
332 | rules:
333 | - alert: HighCPUUsage
334 | expr: cpu_usage > 80
335 | for: 5m
336 | labels:
337 | severity: warning
338 | annotations:
339 | summary: "High CPU usage detected"
340 | - alert: HighMemoryUsage
341 | expr: memory_usage > 90
342 | for: 3m
343 | labels:
344 | severity: critical
345 | annotations:
346 | summary: "High memory usage detected"
347 | - alert: LowDiskSpace
348 | expr: disk_free < 10
349 | for: 10m
350 | labels:
351 | severity: critical
352 | annotations:
353 | summary: "Low disk space"
354 |
355 | - name: record_group_1
356 | interval: 1m
357 | rules:
358 | - record: instance:cpu:rate5m
359 | expr: rate(cpu_total[5m])
360 | labels:
361 | job: monitoring
362 | EOT
363 | }
364 | `
365 |
366 | func testAccResourceMimirRules_contentFile(filepath string) string {
367 | return fmt.Sprintf(`
368 | resource "mimir_rules" "rules_file" {
369 | namespace = "namespace_1"
370 | content_file = "%s"
371 | }
372 | `, filepath)
373 | }
374 |
375 | const testAccResourceMimirRules_onlyGroups = `
376 | resource "mimir_rules" "rules_filtered" {
377 | namespace = "namespace_1"
378 |
379 | only_groups = ["alert_group_1"]
380 |
381 | content = <<-EOT
382 | groups:
383 | - name: alert_group_1
384 | rules:
385 | - alert: HighCPUUsage
386 | expr: cpu_usage > 80
387 | - alert: HighMemoryUsage
388 | expr: memory_usage > 90
389 |
390 | - name: record_group_1
391 | rules:
392 | - record: instance:cpu:rate5m
393 | expr: rate(cpu_total[5m])
394 | EOT
395 | }
396 | `
397 |
398 | const testAccResourceMimirRules_ignoreGroups = `
399 | resource "mimir_rules" "rules_ignored" {
400 | namespace = "namespace_1"
401 |
402 | ignore_groups = ["record_group_1"]
403 |
404 | content = <<-EOT
405 | groups:
406 | - name: alert_group_1
407 | rules:
408 | - alert: HighCPUUsage
409 | expr: cpu_usage > 80
410 |
411 | - name: record_group_1
412 | rules:
413 | - record: instance:cpu:rate5m
414 | expr: rate(cpu_total[5m])
415 | EOT
416 | }
417 | `
418 |
419 | const testAccResourceMimirRules_withOrgID = `
420 | resource "mimir_rules" "rules_with_org" {
421 | org_id = "another_tenant"
422 | namespace = "namespace_1"
423 |
424 | content = <<-EOT
425 | groups:
426 | - name: alert_group_1
427 | rules:
428 | - alert: TestAlert
429 | expr: up == 0
430 | EOT
431 | }
432 | `
433 |
434 | const testAccResourceMimirRules_federated = `
435 | resource "mimir_rules" "rules_federated" {
436 | namespace = "namespace_1"
437 |
438 | content = <<-EOT
439 | groups:
440 | - name: federated_group
441 | source_tenants: ["tenant-a", "tenant-b"]
442 | rules:
443 | - alert: CrossTenantAlert
444 | expr: sum(metric) > 100
445 | EOT
446 | }
447 | `
448 |
449 | const testAccResourceMimirRules_addGroup = `
450 | resource "mimir_rules" "rules_1" {
451 | namespace = "namespace_1"
452 |
453 | content = <<-EOT
454 | groups:
455 | - name: alert_group_1
456 | interval: 30s
457 | rules:
458 | - alert: HighCPUUsage
459 | expr: cpu_usage > 80
460 | for: 5m
461 | labels:
462 | severity: warning
463 | annotations:
464 | summary: "High CPU usage detected"
465 | - alert: HighMemoryUsage
466 | expr: memory_usage > 90
467 | for: 3m
468 | labels:
469 | severity: critical
470 | annotations:
471 | summary: "High memory usage detected"
472 |
473 | - name: record_group_1
474 | interval: 1m
475 | rules:
476 | - record: instance:cpu:rate5m
477 | expr: rate(cpu_total[5m])
478 | labels:
479 | job: monitoring
480 |
481 | - name: new_group
482 | rules:
483 | - alert: NewAlert
484 | expr: new_metric > 50
485 | EOT
486 | }
487 | `
488 |
489 | const testAccResourceMimirRules_removeGroup = `
490 | resource "mimir_rules" "rules_1" {
491 | namespace = "namespace_1"
492 |
493 | content = <<-EOT
494 | groups:
495 | - name: alert_group_1
496 | interval: 30s
497 | rules:
498 | - alert: HighCPUUsage
499 | expr: cpu_usage > 80
500 | for: 5m
501 | labels:
502 | severity: warning
503 | annotations:
504 | summary: "High CPU usage detected"
505 | - alert: HighMemoryUsage
506 | expr: memory_usage > 90
507 | for: 3m
508 | labels:
509 | severity: critical
510 | annotations:
511 | summary: "High memory usage detected"
512 | EOT
513 | }
514 | `
515 |
--------------------------------------------------------------------------------
/mimir/resource_mimir_rule_group_recording_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "regexp"
7 | "testing"
8 |
9 | "github.com/hashicorp/go-version"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
11 | )
12 |
13 | func TestAccResourceRuleGroupRecording_expectValidationError(t *testing.T) {
14 | resource.Test(t, resource.TestCase{
15 | PreCheck: func() { testAccPreCheck(t) },
16 | ProviderFactories: testAccProviderFactories,
17 | Steps: []resource.TestStep{
18 | {
19 | Config: testAccResourceRuleGroupRecording_expectNameValidationError,
20 | ExpectError: regexp.MustCompile("Invalid Group Rule Name"),
21 | },
22 | {
23 | Config: testAccResourceRuleGroupRecording_expectRuleNameValidationError,
24 | ExpectError: regexp.MustCompile("Invalid Recording Rule Name"),
25 | },
26 | {
27 | Config: testAccResourceRuleGroupRecording_expectPromQLValidationError,
28 | ExpectError: regexp.MustCompile("Invalid PromQL expression"),
29 | },
30 | {
31 | Config: testAccResourceRuleGroupRecording_expectLabelNameValidationError,
32 | ExpectError: regexp.MustCompile("Invalid Label Name"),
33 | },
34 | },
35 | })
36 | }
37 |
38 | const testAccResourceRuleGroupRecording_expectNameValidationError = `
39 | resource "mimir_rule_group_recording" "record_1" {
40 | name = "record_1-@error"
41 | namespace = "namespace_1"
42 | rule {
43 | record = "test1_info"
44 | expr = "test1_metric"
45 | }
46 | }
47 | `
48 | const testAccResourceRuleGroupRecording_expectRuleNameValidationError = `
49 | resource "mimir_rule_group_recording" "record_1" {
50 | name = "record_1"
51 | namespace = "namespace_1"
52 | rule {
53 | record = "test1_info;error"
54 | expr = "test1_metric"
55 | }
56 | }
57 | `
58 |
59 | const testAccResourceRuleGroupRecording_expectPromQLValidationError = `
60 | resource "mimir_rule_group_recording" "record_1" {
61 | name = "record_1"
62 | namespace = "namespace_1"
63 | rule {
64 | record = "test1_info"
65 | expr = "rate(hi)"
66 | }
67 | }
68 | `
69 | const testAccResourceRuleGroupRecording_expectLabelNameValidationError = `
70 | resource "mimir_rule_group_recording" "record_1" {
71 | name = "record_1"
72 | namespace = "namespace_1"
73 | rule {
74 | record = "test1_info"
75 | expr = "test1_metric"
76 | labels = {
77 | ins-tance = "localhost"
78 | }
79 | }
80 | }
81 | `
82 |
83 | func TestAccResourceRuleGroupRecording_Basic(t *testing.T) {
84 | // Init client
85 | client, err := NewAPIClient(setupClient())
86 | if err != nil {
87 | t.Fatal(err)
88 | }
89 |
90 | resource.Test(t, resource.TestCase{
91 | PreCheck: func() { testAccPreCheck(t) },
92 | ProviderFactories: testAccProviderFactories,
93 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
94 | Steps: []resource.TestStep{
95 | {
96 | Config: testAccResourceRuleGroupRecording_basic,
97 | Check: resource.ComposeTestCheckFunc(
98 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1", "record_1", client),
99 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "name", "record_1"),
100 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "namespace", "namespace_1"),
101 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "rule.0.record", "test1_info"),
102 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "rule.0.expr", "test1_metric"),
103 | ),
104 | },
105 | {
106 | Config: testAccResourceRuleGroupRecording_basic_update,
107 | Check: resource.ComposeTestCheckFunc(
108 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1", "record_1", client),
109 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "name", "record_1"),
110 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "namespace", "namespace_1"),
111 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "rule.0.record", "test1_info"),
112 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "rule.0.expr", "test1_metric"),
113 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "rule.1.record", "test2_info"),
114 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "rule.1.expr", "test2_metric"),
115 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1", "rule.1.labels.key1", "val1"),
116 | ),
117 | },
118 | {
119 | Config: testAccResourceRuleGroupRecording_interval,
120 | Check: resource.ComposeTestCheckFunc(
121 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_interval", "record_1_interval", client),
122 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "name", "record_1_interval"),
123 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "namespace", "namespace_1"),
124 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "rule.0.record", "test1_info"),
125 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "rule.0.expr", "test1_metric"),
126 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "interval", "6h"),
127 | ),
128 | },
129 | {
130 | Config: testAccResourceRuleGroupRecording_interval_update,
131 | Check: resource.ComposeTestCheckFunc(
132 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_interval", "record_1_interval", client),
133 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "name", "record_1_interval"),
134 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "namespace", "namespace_1"),
135 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "rule.0.record", "test1_info"),
136 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "rule.0.expr", "test1_metric"),
137 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_interval", "interval", "10m"),
138 | ),
139 | },
140 | },
141 | })
142 | }
143 |
144 | func TestAccResourceRuleGroupRecording_EvaluationDelay(t *testing.T) {
145 | currentVersion, _ := version.NewVersion(os.Getenv("MIMIR_VERSION"))
146 | validVersion, _ := version.NewVersion("2.12.0")
147 |
148 | if !currentVersion.Equal(validVersion) {
149 | fmt.Printf("Skipping ruler evaluation delay tests (current version '%s' is not '%s')\n", currentVersion, validVersion)
150 | return
151 | }
152 |
153 | // Init client
154 | client, err := NewAPIClient(setupClient())
155 | if err != nil {
156 | t.Fatal(err)
157 | }
158 |
159 | resource.Test(t, resource.TestCase{
160 | PreCheck: func() { testAccPreCheck(t) },
161 | ProviderFactories: testAccProviderFactories,
162 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
163 | Steps: []resource.TestStep{
164 | {
165 | Config: testAccResourceRuleGroupRecording_evaluation_delay,
166 | Check: resource.ComposeTestCheckFunc(
167 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_evaluation_delay", "record_1_evaluation_delay", client),
168 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "name", "record_1_evaluation_delay"),
169 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "namespace", "namespace_1"),
170 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "rule.0.record", "test1_info"),
171 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "rule.0.expr", "test1_metric"),
172 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "evaluation_delay", "5m"),
173 | ),
174 | },
175 | {
176 | Config: testAccResourceRuleGroupRecording_evaluation_delay_update,
177 | Check: resource.ComposeTestCheckFunc(
178 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_evaluation_delay", "record_1_evaluation_delay", client),
179 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "name", "record_1_evaluation_delay"),
180 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "namespace", "namespace_1"),
181 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "rule.0.record", "test1_info"),
182 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "rule.0.expr", "test1_metric"),
183 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_evaluation_delay", "evaluation_delay", "1m"),
184 | ),
185 | },
186 | },
187 | })
188 | }
189 |
190 | func TestAccResourceRuleGroupRecording_QueryOffset(t *testing.T) {
191 | currentVersion, _ := version.NewVersion(os.Getenv("MIMIR_VERSION"))
192 | minVersion, _ := version.NewVersion("2.13.0")
193 |
194 | if currentVersion.LessThan(minVersion) {
195 | fmt.Printf("Skipping ruler query offset tests (current version '%s' is less than '%s')\n", currentVersion, minVersion)
196 | return
197 | }
198 |
199 | // Init client
200 | client, err := NewAPIClient(setupClient())
201 | if err != nil {
202 | t.Fatal(err)
203 | }
204 |
205 | resource.Test(t, resource.TestCase{
206 | PreCheck: func() { testAccPreCheck(t) },
207 | ProviderFactories: testAccProviderFactories,
208 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
209 | Steps: []resource.TestStep{
210 | {
211 | Config: testAccResourceRuleGroupRecording_query_offset,
212 | Check: resource.ComposeTestCheckFunc(
213 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_query_offset", "record_1_query_offset", client),
214 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "name", "record_1_query_offset"),
215 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "namespace", "namespace_1"),
216 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "rule.0.record", "test1_info"),
217 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "rule.0.expr", "test1_metric"),
218 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "query_offset", "5m"),
219 | ),
220 | },
221 | {
222 | Config: testAccResourceRuleGroupRecording_query_offset_update,
223 | Check: resource.ComposeTestCheckFunc(
224 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_query_offset", "record_1_query_offset", client),
225 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "name", "record_1_query_offset"),
226 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "namespace", "namespace_1"),
227 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "rule.0.record", "test1_info"),
228 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "rule.0.expr", "test1_metric"),
229 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_query_offset", "query_offset", "1m"),
230 | ),
231 | },
232 | },
233 | })
234 | }
235 |
236 | func TestAccResourceRuleGroupRecording_Federated(t *testing.T) {
237 | // Init client
238 | client, err := NewAPIClient(setupClient())
239 | if err != nil {
240 | t.Fatal(err)
241 | }
242 |
243 | resource.Test(t, resource.TestCase{
244 | PreCheck: func() { testAccPreCheck(t) },
245 | ProviderFactories: testAccProviderFactories,
246 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
247 | Steps: []resource.TestStep{
248 | {
249 | Config: testAccResourceRuleGroupRecording_federated_rule_group,
250 | Check: resource.ComposeTestCheckFunc(
251 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_federated_rule_group", "record_1_federated_rule_group", client),
252 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "name", "record_1_federated_rule_group"),
253 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "namespace", "namespace_1"),
254 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "source_tenants.0", "tenant-a"),
255 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "source_tenants.1", "tenant-b"),
256 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "rule.0.record", "test1_info"),
257 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "rule.0.expr", "test1_metric"),
258 | ),
259 | },
260 | {
261 | Config: testAccResourceRuleGroupRecording_federated_rule_group_tenant_change,
262 | Check: resource.ComposeTestCheckFunc(
263 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_federated_rule_group", "record_1_federated_rule_group", client),
264 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "name", "record_1_federated_rule_group"),
265 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "namespace", "namespace_1"),
266 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "source_tenants.0", "tenant-a"),
267 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "source_tenants.1", "tenant-c"),
268 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "rule.0.record", "test1_info"),
269 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "rule.0.expr", "test1_metric"),
270 | ),
271 | },
272 | {
273 | Config: testAccResourceRuleGroupRecording_federated_rule_group_rule_change,
274 | Check: resource.ComposeTestCheckFunc(
275 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_federated_rule_group", "record_1_federated_rule_group", client),
276 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "name", "record_1_federated_rule_group"),
277 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "namespace", "namespace_1"),
278 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "source_tenants.0", "tenant-a"),
279 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "source_tenants.1", "tenant-c"),
280 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "rule.0.record", "test2_info"),
281 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_federated_rule_group", "rule.0.expr", "test2_metric"),
282 | ),
283 | },
284 | },
285 | })
286 | }
287 |
288 | func TestAccResourceRuleGroupRecording_WithOrgID(t *testing.T) {
289 | // Init client
290 | client, err := NewAPIClient(setupClient())
291 | if err != nil {
292 | t.Fatal(err)
293 | }
294 | resource.Test(t, resource.TestCase{
295 | PreCheck: func() { testAccPreCheck(t) },
296 | ProviderFactories: testAccProviderFactories,
297 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
298 | Steps: []resource.TestStep{
299 | {
300 | Config: testAccResourceRuleGroupRecording_withOrgID,
301 | Check: resource.ComposeTestCheckFunc(
302 | testAccCheckMimirRuleGroupExists("mimir_rule_group_recording.record_1_withOrgID", "record_1_withOrgID", client),
303 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_withOrgID", "org_id", "another_tenant"),
304 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_withOrgID", "name", "record_1_withOrgID"),
305 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_withOrgID", "namespace", "namespace_1"),
306 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_withOrgID", "rule.0.record", "test1_info"),
307 | resource.TestCheckResourceAttr("mimir_rule_group_recording.record_1_withOrgID", "rule.0.expr", "test1_metric"),
308 | ),
309 | },
310 | },
311 | })
312 | }
313 |
314 | const testAccResourceRuleGroupRecording_basic = `
315 | resource "mimir_rule_group_recording" "record_1" {
316 | name = "record_1"
317 | namespace = "namespace_1"
318 | rule {
319 | record = "test1_info"
320 | expr = "test1_metric"
321 | }
322 | }
323 | `
324 |
325 | const testAccResourceRuleGroupRecording_basic_update = `
326 | resource "mimir_rule_group_recording" "record_1" {
327 | name = "record_1"
328 | namespace = "namespace_1"
329 | rule {
330 | record = "test1_info"
331 | expr = "test1_metric"
332 | }
333 | rule {
334 | record = "test2_info"
335 | expr = "test2_metric"
336 | labels = {
337 | key1 = "val1"
338 | }
339 | }
340 | }
341 | `
342 |
343 | const testAccResourceRuleGroupRecording_federated_rule_group = `
344 | resource "mimir_rule_group_recording" "record_1_federated_rule_group" {
345 | name = "record_1_federated_rule_group"
346 | namespace = "namespace_1"
347 | source_tenants = ["tenant-a", "tenant-b"]
348 | rule {
349 | record = "test1_info"
350 | expr = "test1_metric"
351 | }
352 | }
353 | `
354 |
355 | const testAccResourceRuleGroupRecording_federated_rule_group_tenant_change = `
356 | resource "mimir_rule_group_recording" "record_1_federated_rule_group" {
357 | name = "record_1_federated_rule_group"
358 | namespace = "namespace_1"
359 | source_tenants = ["tenant-a", "tenant-c"]
360 | rule {
361 | record = "test1_info"
362 | expr = "test1_metric"
363 | }
364 | }
365 | `
366 |
367 | const testAccResourceRuleGroupRecording_federated_rule_group_rule_change = `
368 | resource "mimir_rule_group_recording" "record_1_federated_rule_group" {
369 | name = "record_1_federated_rule_group"
370 | namespace = "namespace_1"
371 | source_tenants = ["tenant-a", "tenant-c"]
372 | rule {
373 | record = "test2_info"
374 | expr = "test2_metric"
375 | }
376 | }
377 | `
378 |
379 | const testAccResourceRuleGroupRecording_interval = `
380 | resource "mimir_rule_group_recording" "record_1_interval" {
381 | name = "record_1_interval"
382 | namespace = "namespace_1"
383 | interval = "6h"
384 | rule {
385 | record = "test1_info"
386 | expr = "test1_metric"
387 | }
388 | }
389 | `
390 |
391 | const testAccResourceRuleGroupRecording_interval_update = `
392 | resource "mimir_rule_group_recording" "record_1_interval" {
393 | name = "record_1_interval"
394 | namespace = "namespace_1"
395 | interval = "10m"
396 | rule {
397 | record = "test1_info"
398 | expr = "test1_metric"
399 | }
400 | }
401 | `
402 |
403 | const testAccResourceRuleGroupRecording_evaluation_delay = `
404 | resource "mimir_rule_group_recording" "record_1_evaluation_delay" {
405 | name = "record_1_evaluation_delay"
406 | namespace = "namespace_1"
407 | evaluation_delay = "5m"
408 | rule {
409 | record = "test1_info"
410 | expr = "test1_metric"
411 | }
412 | }
413 | `
414 |
415 | const testAccResourceRuleGroupRecording_evaluation_delay_update = `
416 | resource "mimir_rule_group_recording" "record_1_evaluation_delay" {
417 | name = "record_1_evaluation_delay"
418 | namespace = "namespace_1"
419 | evaluation_delay = "1m"
420 | rule {
421 | record = "test1_info"
422 | expr = "test1_metric"
423 | }
424 | }
425 | `
426 |
427 | const testAccResourceRuleGroupRecording_query_offset = `
428 | resource "mimir_rule_group_recording" "record_1_query_offset" {
429 | name = "record_1_query_offset"
430 | namespace = "namespace_1"
431 | query_offset = "5m"
432 | rule {
433 | record = "test1_info"
434 | expr = "test1_metric"
435 | }
436 | }
437 | `
438 |
439 | const testAccResourceRuleGroupRecording_query_offset_update = `
440 | resource "mimir_rule_group_recording" "record_1_query_offset" {
441 | name = "record_1_query_offset"
442 | namespace = "namespace_1"
443 | query_offset = "1m"
444 | rule {
445 | record = "test1_info"
446 | expr = "test1_metric"
447 | }
448 | }
449 | `
450 |
451 | const testAccResourceRuleGroupRecording_withOrgID = `
452 | resource "mimir_rule_group_recording" "record_1_withOrgID" {
453 | org_id = "another_tenant"
454 | name = "record_1_withOrgID"
455 | namespace = "namespace_1"
456 | rule {
457 | record = "test1_info"
458 | expr = "test1_metric"
459 | }
460 | }
461 | `
462 |
--------------------------------------------------------------------------------
/mimir/resource_mimir_rule_group_alerting_test.go:
--------------------------------------------------------------------------------
1 | package mimir
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "regexp"
7 | "testing"
8 |
9 | "github.com/hashicorp/go-version"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
11 | )
12 |
13 | func TestAccResourceRuleGroupAlerting_expectValidationError(t *testing.T) {
14 | resource.Test(t, resource.TestCase{
15 | PreCheck: func() { testAccPreCheck(t) },
16 | ProviderFactories: testAccProviderFactories,
17 | Steps: []resource.TestStep{
18 | {
19 | Config: testAccResourceRuleGroupAlerting_expectNameValidationError,
20 | ExpectError: regexp.MustCompile("Invalid Group Rule Name"),
21 | },
22 | {
23 | Config: testAccResourceRuleGroupAlerting_expectRuleNameValidationError,
24 | ExpectError: regexp.MustCompile("Invalid Alerting Rule Name"),
25 | },
26 | {
27 | Config: testAccResourceRuleGroupAlerting_expectPromQLValidationError,
28 | ExpectError: regexp.MustCompile("Invalid PromQL expression"),
29 | },
30 | {
31 | Config: testAccResourceRuleGroupAlerting_expectDurationValidationError,
32 | ExpectError: regexp.MustCompile("unknown unit"),
33 | },
34 | {
35 | Config: testAccResourceRuleGroupAlerting_expectLabelNameValidationError,
36 | ExpectError: regexp.MustCompile("Invalid Label Name"),
37 | },
38 | {
39 | Config: testAccResourceRuleGroupAlerting_expectAnnotationNameValidationError,
40 | ExpectError: regexp.MustCompile("Invalid Annotation Name"),
41 | },
42 | },
43 | })
44 | }
45 |
46 | const testAccResourceRuleGroupAlerting_expectNameValidationError = `
47 | resource "mimir_rule_group_alerting" "alert_1" {
48 | name = "alert-@error"
49 | namespace = "namespace_1"
50 | rule {
51 | alert = "test1_alert"
52 | expr = "test1_metric"
53 | }
54 | }
55 | `
56 |
57 | const testAccResourceRuleGroupAlerting_expectRuleNameValidationError = `
58 | resource "mimir_rule_group_alerting" "alert_1" {
59 | name = "alert_1"
60 | namespace = "namespace_1"
61 | rule {
62 | alert = "test1 alert"
63 | expr = "test1_metric"
64 | }
65 | }
66 | `
67 |
68 | const testAccResourceRuleGroupAlerting_expectPromQLValidationError = `
69 | resource "mimir_rule_group_alerting" "alert_1" {
70 | name = "alert_1"
71 | namespace = "namespace_1"
72 | rule {
73 | alert = "test1_alert"
74 | expr = "rate(hi)"
75 | }
76 | }
77 | `
78 |
79 | const testAccResourceRuleGroupAlerting_expectDurationValidationError = `
80 | resource "mimir_rule_group_alerting" "alert_1" {
81 | name = "alert_1"
82 | namespace = "namespace_1"
83 | rule {
84 | alert = "test1_alert"
85 | expr = "test1_metric"
86 | for = "3months"
87 | }
88 | }
89 | `
90 |
91 | const testAccResourceRuleGroupAlerting_expectLabelNameValidationError = `
92 | resource "mimir_rule_group_alerting" "alert_1" {
93 | name = "alert_1"
94 | namespace = "namespace_1"
95 | rule {
96 | alert = "test1_alert"
97 | expr = "test1_metric"
98 | labels = {
99 | ins-tance = "localhost"
100 | }
101 | }
102 | }
103 | `
104 |
105 | const testAccResourceRuleGroupAlerting_expectAnnotationNameValidationError = `
106 | resource "mimir_rule_group_alerting" "alert_1" {
107 | name = "alert_1"
108 | namespace = "namespace_1"
109 | rule {
110 | alert = "test1_alert"
111 | expr = "test1_metric"
112 | annotations = {
113 | ins-tance = "localhost"
114 | }
115 | }
116 | }
117 | `
118 |
119 | func TestAccResourceRuleGroupAlerting_Basic(t *testing.T) {
120 | // Init client
121 | client, err := NewAPIClient(setupClient())
122 | if err != nil {
123 | t.Fatal(err)
124 | }
125 |
126 | resource.Test(t, resource.TestCase{
127 | PreCheck: func() { testAccPreCheck(t) },
128 | ProviderFactories: testAccProviderFactories,
129 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
130 | Steps: []resource.TestStep{
131 | {
132 | Config: testAccResourceRuleGroupAlerting_basic,
133 | Check: resource.ComposeTestCheckFunc(
134 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1", "alert_1", client),
135 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "name", "alert_1"),
136 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "namespace", "namespace_1"),
137 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.0.alert", "test1"),
138 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.0.expr", "test1_metric"),
139 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.alert", "test2"),
140 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.expr", "test2_metric"),
141 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.for", ""),
142 | ),
143 | },
144 | {
145 | Config: testAccResourceRuleGroupAlerting_basic_update,
146 | Check: resource.ComposeTestCheckFunc(
147 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1", "alert_1", client),
148 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "name", "alert_1"),
149 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "namespace", "namespace_1"),
150 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.0.alert", "test1"),
151 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.0.expr", "test1_metric"),
152 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.alert", "test2"),
153 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.expr", "test2_metric"),
154 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.for", "1m"),
155 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.labels.severity", "critical"),
156 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.annotations.summary", "test 2 alert summary"),
157 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1", "rule.1.annotations.description", "test 2 alert description"),
158 | ),
159 | },
160 | {
161 | Config: testAccResourceRuleGroupAlerting_interval,
162 | Check: resource.ComposeTestCheckFunc(
163 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1_interval", "alert_1_interval", client),
164 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "name", "alert_1_interval"),
165 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "namespace", "namespace_1"),
166 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "rule.0.alert", "test1_info"),
167 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "rule.0.expr", "test1_metric"),
168 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "interval", "6h"),
169 | ),
170 | },
171 | {
172 | Config: testAccResourceRuleGroupAlerting_interval_update,
173 | Check: resource.ComposeTestCheckFunc(
174 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1_interval", "alert_1_interval", client),
175 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "name", "alert_1_interval"),
176 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "namespace", "namespace_1"),
177 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "rule.0.alert", "test1_info"),
178 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "rule.0.expr", "test1_metric"),
179 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_interval", "interval", "10m"),
180 | ),
181 | },
182 | },
183 | })
184 | }
185 |
186 | func TestAccResourceRuleGroupAlerting_Federated(t *testing.T) {
187 | // Init client
188 | client, err := NewAPIClient(setupClient())
189 | if err != nil {
190 | t.Fatal(err)
191 | }
192 |
193 | resource.Test(t, resource.TestCase{
194 | PreCheck: func() { testAccPreCheck(t) },
195 | ProviderFactories: testAccProviderFactories,
196 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
197 | Steps: []resource.TestStep{
198 | {
199 | Config: testAccResourceRuleGroupAlerting_federated_rule_group,
200 | Check: resource.ComposeTestCheckFunc(
201 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1_federated_rule_group", "alert_1_federated_rule_group", client),
202 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "name", "alert_1_federated_rule_group"),
203 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "namespace", "namespace_1"),
204 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "source_tenants.0", "tenant-a"),
205 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "source_tenants.1", "tenant-b"),
206 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "rule.0.alert", "test1"),
207 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "rule.0.expr", "test1_metric"),
208 | ),
209 | },
210 | {
211 | Config: testAccResourceRuleGroupAlerting_federated_rule_group_tenant_change,
212 | Check: resource.ComposeTestCheckFunc(
213 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1_federated_rule_group", "alert_1_federated_rule_group", client),
214 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "name", "alert_1_federated_rule_group"),
215 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "namespace", "namespace_1"),
216 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "source_tenants.0", "tenant-a"),
217 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "source_tenants.1", "tenant-c"),
218 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "source_tenants.2", "tenant-d"),
219 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "rule.0.alert", "test1"),
220 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "rule.0.expr", "test1_metric"),
221 | ),
222 | },
223 | {
224 | Config: testAccResourceRuleGroupAlerting_federated_rule_group_rule_change,
225 | Check: resource.ComposeTestCheckFunc(
226 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1_federated_rule_group", "alert_1_federated_rule_group", client),
227 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "name", "alert_1_federated_rule_group"),
228 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "namespace", "namespace_1"),
229 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "source_tenants.0", "tenant-a"),
230 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "source_tenants.1", "tenant-c"),
231 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "source_tenants.2", "tenant-d"),
232 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "rule.0.alert", "test2"),
233 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_federated_rule_group", "rule.0.expr", "test2_metric"),
234 | ),
235 | },
236 | },
237 | })
238 | }
239 |
240 | func TestAccResourceRuleGroupAlerting_PromQLValidation_HistogramAvg(t *testing.T) {
241 | /* Skip this test if mimir version is older than 2.12.0
242 |
243 | === RUN TestAccResourceRuleGroupAlerting_PromQLValidation_HistogramAvg
244 | resource_mimir_rule_group_alerting_test.go:245: Step 1/1 error: Error running apply: exit status 1
245 | 2024/07/05 09:20:03 [DEBUG] Using modified User-Agent: Terraform/0.12.31 HashiCorp-terraform-exec/0.18.1
246 |
247 | Error: Cannot create alerting rule group 'alert_1_histogram_avg_rule_group' (namespace: namespace_1) - unexpected response code '400': 4:13: group "alert_1_histogram_avg_rule_group", rule 0, "test_histogram_avg": could not parse expression: 1:1: parse error: unknown function with name "histogram_avg"
248 |
249 |
250 | on terraform_plugin_test.tf line 2, in resource "mimir_rule_group_alerting" "alert_1_histogram_avg_rule_group":
251 | 2: resource "mimir_rule_group_alerting" "alert_1_histogram_avg_rule_group" {
252 |
253 |
254 | --- FAIL: TestAccResourceRuleGroupAlerting_PromQLValidation_HistogramAvg (0.28s)
255 |
256 | */
257 | currentVersion, _ := version.NewVersion(os.Getenv("MIMIR_VERSION"))
258 | minVersion, _ := version.NewVersion("2.12.0")
259 |
260 | if currentVersion.LessThan(minVersion) {
261 | fmt.Printf("Skipping PromQL HistogramAvg tests (current version '%s' is less than '%s')\n", currentVersion, minVersion)
262 | return
263 | }
264 |
265 | // Init client
266 | client, err := NewAPIClient(setupClient())
267 | if err != nil {
268 | t.Fatal(err)
269 | }
270 |
271 | resource.Test(t, resource.TestCase{
272 | PreCheck: func() { testAccPreCheck(t) },
273 | ProviderFactories: testAccProviderFactories,
274 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
275 | Steps: []resource.TestStep{
276 | {
277 | Config: testAccResourceRuleGroupAlerting_promql_validation_histogram_avg,
278 | Check: resource.ComposeTestCheckFunc(
279 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1_histogram_avg_rule_group", "alert_1_histogram_avg_rule_group", client),
280 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_histogram_avg_rule_group", "name", "alert_1_histogram_avg_rule_group"),
281 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_histogram_avg_rule_group", "namespace", "namespace_1"),
282 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_histogram_avg_rule_group", "rule.0.alert", "test_histogram_avg"),
283 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_histogram_avg_rule_group", "rule.0.expr", "histogram_avg(rate(test_metric[5m])) > 1"),
284 | ),
285 | },
286 | },
287 | })
288 | }
289 |
290 | func TestAccResourceRuleGroupAlerting_FormatPromQLExpr(t *testing.T) {
291 | // Init client
292 | client, err := NewAPIClient(setupClient())
293 | if err != nil {
294 | t.Fatal(err)
295 | }
296 | os.Setenv("MIMIR_FORMAT_PROMQL_EXPR", "true")
297 | resource.Test(t, resource.TestCase{
298 | PreCheck: func() { testAccPreCheck(t) },
299 | ProviderFactories: testAccProviderFactories,
300 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
301 | Steps: []resource.TestStep{
302 | {
303 | Config: testAccResourceRuleGroupAlerting_prettify_promql_expr,
304 | Check: resource.ComposeTestCheckFunc(
305 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1_prettify", "alert_1_prettify", client),
306 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_prettify", "name", "alert_1_prettify"),
307 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_prettify", "namespace", "namespace_1"),
308 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_prettify", "rule.0.alert", "checkPrettifyPromQL"),
309 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_prettify", "rule.0.expr", "up == 0\nunless\n my_very_very_long_useless_metric_that_mean_nothing_but_necessary_to_check_prettify_promql > 300"),
310 | ),
311 | },
312 | },
313 | })
314 | os.Setenv("MIMIR_FORMAT_PROMQL_EXPR", "false")
315 | }
316 |
317 | func TestAccResourceRuleGroupAlerting_WithOrgID(t *testing.T) {
318 | // Init client
319 | client, err := NewAPIClient(setupClient())
320 | if err != nil {
321 | t.Fatal(err)
322 | }
323 | resource.Test(t, resource.TestCase{
324 | PreCheck: func() { testAccPreCheck(t) },
325 | ProviderFactories: testAccProviderFactories,
326 | CheckDestroy: testAccCheckMimirRuleGroupDestroy,
327 | Steps: []resource.TestStep{
328 | {
329 | Config: testAccResourceRuleGroupAlerting_withOrgID,
330 | Check: resource.ComposeTestCheckFunc(
331 | testAccCheckMimirRuleGroupExists("mimir_rule_group_alerting.alert_1_withOrgID", "alert_1_withOrgID", client),
332 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_withOrgID", "org_id", "another_tenant"),
333 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_withOrgID", "name", "alert_1_withOrgID"),
334 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_withOrgID", "namespace", "namespace_1"),
335 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_withOrgID", "rule.0.alert", "test1_info"),
336 | resource.TestCheckResourceAttr("mimir_rule_group_alerting.alert_1_withOrgID", "rule.0.expr", "test1_metric"),
337 | ),
338 | },
339 | },
340 | })
341 | }
342 |
343 | const testAccResourceRuleGroupAlerting_basic = `
344 | resource "mimir_rule_group_alerting" "alert_1" {
345 | name = "alert_1"
346 | namespace = "namespace_1"
347 | rule {
348 | alert = "test1"
349 | expr = "test1_metric"
350 | }
351 | rule {
352 | alert = "test2"
353 | expr = "test2_metric"
354 | for = "0s"
355 | }
356 | }
357 | `
358 |
359 | const testAccResourceRuleGroupAlerting_basic_update = `
360 | resource "mimir_rule_group_alerting" "alert_1" {
361 | name = "alert_1"
362 | namespace = "namespace_1"
363 | rule {
364 | alert = "test1"
365 | expr = "test1_metric"
366 | }
367 | rule {
368 | alert = "test2"
369 | expr = "test2_metric"
370 | for = "1m"
371 | labels = {
372 | severity = "critical"
373 | }
374 | annotations = {
375 | summary = "test 2 alert summary"
376 | description = "test 2 alert description"
377 | }
378 | }
379 | }
380 | `
381 |
382 | const testAccResourceRuleGroupAlerting_interval = `
383 | resource "mimir_rule_group_alerting" "alert_1_interval" {
384 | name = "alert_1_interval"
385 | namespace = "namespace_1"
386 | interval = "6h"
387 | rule {
388 | alert = "test1_info"
389 | expr = "test1_metric"
390 | }
391 | }
392 | `
393 |
394 | const testAccResourceRuleGroupAlerting_interval_update = `
395 | resource "mimir_rule_group_alerting" "alert_1_interval" {
396 | name = "alert_1_interval"
397 | namespace = "namespace_1"
398 | interval = "10m"
399 | rule {
400 | alert = "test1_info"
401 | expr = "test1_metric"
402 | }
403 | }
404 | `
405 |
406 | const testAccResourceRuleGroupAlerting_prettify_promql_expr = `
407 | resource "mimir_rule_group_alerting" "alert_1_prettify" {
408 | name = "alert_1_prettify"
409 | namespace = "namespace_1"
410 | rule {
411 | alert = "checkPrettifyPromQL"
412 | expr = "up==0 unless my_very_very_long_useless_metric_that_mean_nothing_but_necessary_to_check_prettify_promql > 300"
413 | }
414 | }
415 | `
416 |
417 | const testAccResourceRuleGroupAlerting_federated_rule_group = `
418 | resource "mimir_rule_group_alerting" "alert_1_federated_rule_group" {
419 | name = "alert_1_federated_rule_group"
420 | source_tenants = ["tenant-a", "tenant-b"]
421 | namespace = "namespace_1"
422 | rule {
423 | alert = "test1"
424 | expr = "test1_metric"
425 | }
426 | }
427 | `
428 |
429 | const testAccResourceRuleGroupAlerting_federated_rule_group_tenant_change = `
430 | resource "mimir_rule_group_alerting" "alert_1_federated_rule_group" {
431 | name = "alert_1_federated_rule_group"
432 | source_tenants = ["tenant-a", "tenant-c", "tenant-d"]
433 | namespace = "namespace_1"
434 | rule {
435 | alert = "test1"
436 | expr = "test1_metric"
437 | }
438 | }
439 | `
440 |
441 | const testAccResourceRuleGroupAlerting_federated_rule_group_rule_change = `
442 | resource "mimir_rule_group_alerting" "alert_1_federated_rule_group" {
443 | name = "alert_1_federated_rule_group"
444 | source_tenants = ["tenant-a", "tenant-c", "tenant-d"]
445 | namespace = "namespace_1"
446 | rule {
447 | alert = "test2"
448 | expr = "test2_metric"
449 | }
450 | }
451 | `
452 |
453 | const testAccResourceRuleGroupAlerting_promql_validation_histogram_avg = `
454 | resource "mimir_rule_group_alerting" "alert_1_histogram_avg_rule_group" {
455 | name = "alert_1_histogram_avg_rule_group"
456 | namespace = "namespace_1"
457 | rule {
458 | alert = "test_histogram_avg"
459 | expr = "histogram_avg(rate(test_metric[5m])) > 1"
460 | }
461 | }
462 | `
463 |
464 | const testAccResourceRuleGroupAlerting_withOrgID = `
465 | resource "mimir_rule_group_alerting" "alert_1_withOrgID" {
466 | org_id = "another_tenant"
467 | name = "alert_1_withOrgID"
468 | namespace = "namespace_1"
469 | rule {
470 | alert = "test1_info"
471 | expr = "test1_metric"
472 | }
473 | }
474 | `
475 |
--------------------------------------------------------------------------------