├── 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 | --------------------------------------------------------------------------------