├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── dependabot.yml ├── workflows │ ├── release.yml │ └── test.yml └── ISSUE_TEMPLATE.md ├── CHANGELOG.md ├── examples ├── resources │ ├── mimirtool_ruler_namespace │ │ ├── import.sh │ │ └── resource.tf │ └── mimirtool_alertmanager │ │ └── resource.tf └── provider │ └── provider.tf ├── terraform-registry-manifest.json ├── tools └── tools.go ├── internal └── provider │ ├── testdata │ ├── example_alertmanager_template.tmpl │ ├── rules-fails-check.yaml │ ├── rules-parse-error.yaml │ ├── example_alertmanager_config.yaml │ ├── rules.yaml │ ├── rules2_spacing.yaml │ ├── rules2.yaml │ └── rules-quoting.yaml │ ├── provider_test.go │ ├── types.go │ ├── common.go │ ├── validators.go │ ├── alertmanager_resource_test.go │ ├── alertmanager_resource.go │ ├── provider.go │ ├── ruler_namespace_resource_test.go │ └── ruler_namespace_resource.go ├── AUTHORS ├── .pre-commit-config.yaml ├── docker-compose.yml ├── CONTRIBUTORS ├── MAINTAINERS ├── .gitignore ├── GNUmakefile ├── config.yaml ├── main.go ├── docs ├── resources │ ├── alertmanager.md │ └── ruler_namespace.md └── index.md ├── .goreleaser.yml ├── .golangci.yml ├── README.md ├── CONTRIBUTING.md ├── go.mod └── LICENSE /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ovh/metrics 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (November 15, 2022) 2 | 3 | Initial version 4 | -------------------------------------------------------------------------------- /examples/resources/mimirtool_ruler_namespace/import.sh: -------------------------------------------------------------------------------- 1 | terraform import mimirtool_ruler_namespace.demo demo 2 | -------------------------------------------------------------------------------- /examples/provider/provider.tf: -------------------------------------------------------------------------------- 1 | provider "mimirtool" { 2 | address = "http://localhost:9009" 3 | tenant_id = "anonymous" 4 | } 5 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["5.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | // document generation 8 | _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" 9 | ) 10 | -------------------------------------------------------------------------------- /internal/provider/testdata/example_alertmanager_template.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__alertmanager" }}AlertManager{{ end }} 2 | {{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver | urlquery }}{{ end }} 3 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. 4 | 5 | Please read the full text at https://www.hashicorp.com/community-guidelines 6 | -------------------------------------------------------------------------------- /internal/provider/testdata/rules-fails-check.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: mimir_api_1 3 | rules: 4 | - expr: histogram_quantile(0.99, sum(rate(cortex_request_duration_seconds_bucket[1m])) 5 | by (le, cluster, job)) 6 | record: cluster_job_cortex_request_duration_seconds_99quantile 7 | -------------------------------------------------------------------------------- /internal/provider/testdata/rules-parse-error.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: mimir_api_1 3 | rules: 4 | - expression: histogram_quantile(0.99, sum(rate(cortex_request_duration_seconds_bucket[1m])) 5 | by (le, cluster, job)) 6 | record: cluster_job_cortex_request_duration_seconds_99quantile 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Puppet Mimir authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files 3 | # and it lists the copyright holders only. 4 | 5 | Julien GIRARD 6 | Nicolas DUPEUX 7 | Wilfried ROSET 8 | 9 | # Please keep the list sorted. 10 | 11 | OVH SAS 12 | -------------------------------------------------------------------------------- /internal/provider/testdata/example_alertmanager_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # See: https://grafana.com/docs/mimir/latest/references/http-api/#alertmanager 3 | global: 4 | smtp_smarthost: 'localhost:25' 5 | smtp_from: 'youraddress@example.org' 6 | templates: 7 | - 'default_template' 8 | route: 9 | receiver: example-email 10 | receivers: 11 | - name: example-email 12 | email_configs: 13 | - to: 'youraddress@example.org' 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.3.0 5 | hooks: 6 | - id: check-executables-have-shebangs 7 | - id: check-json 8 | - id: check-merge-conflict 9 | - id: trailing-whitespace 10 | exclude: ^docs/.* 11 | - repo: https://github.com/dnephin/pre-commit-golang 12 | rev: v0.5.1 13 | hooks: 14 | - id: go-fmt 15 | - id: go-lint 16 | -------------------------------------------------------------------------------- /internal/provider/testdata/rules.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: mimir_api_1 3 | rules: 4 | - expr: histogram_quantile(0.99, sum(rate(cortex_request_duration_seconds_bucket[1m])) 5 | by (le, cluster, job)) 6 | record: cluster_job:cortex_request_duration_seconds:99quantile 7 | - expr: histogram_quantile(0.50, sum(rate(cortex_request_duration_seconds_bucket[1m])) 8 | by (le, cluster, job)) 9 | record: cluster_job:cortex_request_duration_seconds:50quantile -------------------------------------------------------------------------------- /internal/provider/testdata/rules2_spacing.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: mimir_api_1 3 | rules: 4 | - expr: |- 5 | histogram_quantile(0.99, sum(rate(cortex_request_duration_seconds_bucket[1m])) 6 | by (le, cluster, job)) 7 | record: cluster_job:cortex_request_duration_seconds:99quantile 8 | - expr: histogram_quantile(0.50, sum(rate(cortex_request_duration_seconds_bucket[1m])) 9 | by (le, cluster, job)) 10 | record: cluster_job:cortex_request_duration_seconds:50quantile -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | mimir: 4 | image: grafana/mimir:${MIMIR_VERSION} 5 | # We don't want to conflict with possible other container 6 | container_name: mimirtool-mimir-service 7 | ports: 8 | - 8080:8080 9 | volumes: 10 | - ./config.yaml:/etc/mimir/config.yaml 11 | command: 12 | - --config.file=/etc/mimir/config.yaml 13 | healthcheck: 14 | test: ["CMD", "wget", "-O-", "http://localhost:8080/ready"] 15 | timeout: 45s 16 | interval: 30s 17 | retries: 3 18 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of the project maintainers. 2 | # This is mostly useful for contributors that want to push 3 | # significant pull requests or for project management issues. 4 | # 5 | # 6 | # Names should be added to this file like so: 7 | # Individual's name 8 | # Individual's name 9 | # 10 | # Please keep the list sorted. 11 | # 12 | Julien GIRARD 13 | Nicolas DUPEUX 14 | Wilfried ROSET 15 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | # This is the official list of the project maintainers. 2 | # This is mostly useful for contributors that want to push 3 | # significant pull requests or for project management issues. 4 | # 5 | # 6 | # Names should be added to this file like so: 7 | # Individual's name 8 | # Individual's name 9 | # 10 | # Please keep the list sorted. 11 | # 12 | Julien GIRARD 13 | Nicolas DUPEUX 14 | Wilfried ROSET 15 | -------------------------------------------------------------------------------- /examples/resources/mimirtool_ruler_namespace/resource.tf: -------------------------------------------------------------------------------- 1 | resource "mimirtool_ruler_namespace" "demo" { 2 | namespace = "demo" 3 | config_yaml = < 80' 18 | labels: 19 | severity: warning 20 | annotations: 21 | description: |- 22 | CPU load is > 80% 23 | VALUE = {{ $value }} 24 | LABELS = {{ $labels }} 25 | summary: Host high CPU load (instance {{ $labels.instance }}) 26 | -------------------------------------------------------------------------------- /internal/provider/common.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/attr" 8 | "github.com/hashicorp/terraform-plugin-framework/types" 9 | ) 10 | 11 | func hash(s string) string { 12 | sha := sha256.Sum256([]byte(s)) 13 | return hex.EncodeToString(sha[:]) 14 | } 15 | 16 | // mapStringFromTypesMap converts a types.Map to map[string]string for template handling. 17 | func mapStringFromTypesMap(m types.Map) map[string]string { 18 | if m.IsNull() || m.IsUnknown() { 19 | return nil 20 | } 21 | result := make(map[string]string, len(m.Elements())) 22 | for k, v := range m.Elements() { 23 | if strVal, ok := v.(types.String); ok && !strVal.IsNull() && !strVal.IsUnknown() { 24 | result[k] = strVal.ValueString() 25 | } 26 | } 27 | return result 28 | } 29 | 30 | // typeMapFromMapString converts a map[string]string (e.g., Alertmanager templates) 31 | // into a Terraform types.Map value, where each value is a types.StringValue. 32 | // This is useful for storing string maps in Terraform state. 33 | func typeMapFromMapString(templates map[string]string) types.Map { 34 | if templates == nil { 35 | return types.MapNull(types.StringType) 36 | } 37 | 38 | templatesMap := make(map[string]attr.Value, len(templates)) 39 | for k, v := range templates { 40 | templatesMap[k] = types.StringValue(v) 41 | } 42 | 43 | return types.MapValueMust(types.StringType, templatesMap) 44 | } 45 | -------------------------------------------------------------------------------- /docs/resources/alertmanager.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "mimirtool_alertmanager Resource - terraform-provider-mimirtool" 4 | subcategory: "" 5 | description: |- 6 | Official documentation https://grafana.com/docs/mimir/latest/references/http-api/#alertmanager 7 | --- 8 | 9 | # mimirtool_alertmanager (Resource) 10 | 11 | [Official documentation](https://grafana.com/docs/mimir/latest/references/http-api/#alertmanager) 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "mimirtool_alertmanager" "demo" { 17 | config_yaml = < 40 | ## Schema 41 | 42 | ### Required 43 | 44 | - `config_yaml` (String) The Alertmanager configuration to load in Grafana Mimir as YAML. 45 | 46 | ### Optional 47 | 48 | - `templates_config_yaml` (Map of String) The templates to load along with the configuration. 49 | 50 | ### Read-Only 51 | 52 | - `id` (String) The ID of this resource. 53 | 54 | 55 | -------------------------------------------------------------------------------- /docs/resources/ruler_namespace.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "mimirtool_ruler_namespace Resource - terraform-provider-mimirtool" 4 | subcategory: "" 5 | description: |- 6 | Official documentation https://grafana.com/docs/mimir/latest/references/http-api/#ruler 7 | --- 8 | 9 | # mimirtool_ruler_namespace (Resource) 10 | 11 | [Official documentation](https://grafana.com/docs/mimir/latest/references/http-api/#ruler) 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | resource "mimirtool_ruler_namespace" "demo" { 17 | namespace = "demo" 18 | config_yaml = < 33 | ## Schema 34 | 35 | ### Required 36 | 37 | - `config_yaml` (String) The namespace's groups rules definition to create in Grafana Mimir as YAML. 38 | - `namespace` (String) The name of the namespace to create in Grafana Mimir. 39 | 40 | ### Optional 41 | 42 | - `recording_rule_check` (Boolean) Controls whether to run recording rule checks entirely. 43 | - `strict_recording_rule_check` (Boolean) Fails rules checks that do not match best practices exactly. See: https://prometheus.io/docs/practices/rules/ 44 | 45 | ### Read-Only 46 | 47 | - `id` (String) The ID of this resource. 48 | 49 | ## Import 50 | 51 | Import is supported using the following syntax: 52 | 53 | ```shell 54 | terraform import mimirtool_ruler_namespace.demo demo 55 | ``` 56 | -------------------------------------------------------------------------------- /.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 | - name: Checkout 24 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.5.4 25 | with: 26 | fetch-depth: 0 27 | - name: Set up Go 28 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 29 | with: 30 | go-version-file: "go.mod" 31 | cache: true 32 | - name: Import GPG key 33 | uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 34 | id: import_gpg 35 | with: 36 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 37 | passphrase: ${{ secrets.PASSPHRASE }} 38 | - name: Run GoReleaser 39 | uses: goreleaser/goreleaser-action@v6 40 | with: 41 | distribution: goreleaser 42 | version: "~> v2" 43 | args: release --clean 44 | env: 45 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 46 | # GitHub sets this automatically 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Hi there, 2 | 3 | Thank you for opening an issue. Please note that we try to keep the Terraform issue tracker reserved for bug reports and feature requests. For general usage questions, please see: https://www.terraform.io/community.html. 4 | 5 | ### Terraform Version 6 | Run `terraform -v` to show the version. If you are not running the latest version of Terraform, please upgrade because your issue may have already been fixed. 7 | 8 | ### Affected Resource(s) 9 | Please list the resources as a list, for example: 10 | - opc_instance 11 | - opc_storage_volume 12 | 13 | If this issue appears to affect multiple resources, it may be an issue with Terraform's core, so please mention this. 14 | 15 | ### Terraform Configuration Files 16 | ```hcl 17 | # Copy-paste your Terraform configurations here - for large Terraform configs, 18 | # please use a service like Dropbox and share a link to the ZIP file. For 19 | # security, you can also encrypt the files using our GPG public key. 20 | ``` 21 | 22 | ### Debug Output 23 | Please provider a link to a GitHub Gist containing the complete debug output: https://www.terraform.io/docs/internals/debugging.html. Please do NOT paste the debug output in the issue; just paste a link to the Gist. 24 | 25 | ### Panic Output 26 | If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`. 27 | 28 | ### Expected Behavior 29 | What should have happened? 30 | 31 | ### Actual Behavior 32 | What actually happened? 33 | 34 | ### Steps to Reproduce 35 | Please list the steps required to reproduce the issue, for example: 36 | 1. `terraform apply` 37 | 38 | ### Important Factoids 39 | Are there anything atypical about your accounts that we should know? For example: Running in EC2 Classic? Custom version of OpenStack? Tight ACLs? 40 | 41 | ### References 42 | Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example: 43 | - GH-1234 44 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | before: 4 | hooks: 5 | # this is just an example and not a requirement for provider building/publishing 6 | - go mod tidy 7 | builds: 8 | - env: 9 | # goreleaser does not work with CGO, it could also complicate 10 | # usage by users in CI/CD systems like Terraform Cloud where 11 | # they are unable to install libraries. 12 | - CGO_ENABLED=0 13 | mod_timestamp: "{{ .CommitTimestamp }}" 14 | flags: 15 | - -trimpath 16 | ldflags: 17 | - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}" 18 | goos: 19 | - freebsd 20 | - windows 21 | - linux 22 | - darwin 23 | goarch: 24 | - amd64 25 | - arm64 26 | binary: "{{ .ProjectName }}_v{{ .Version }}" 27 | archives: 28 | - format: zip 29 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 30 | checksum: 31 | extra_files: 32 | - glob: "terraform-registry-manifest.json" 33 | name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" 34 | name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" 35 | algorithm: sha256 36 | signs: 37 | - artifacts: checksum 38 | args: 39 | # if you are using this in a GitHub action or some other automated pipeline, you 40 | # need to pass the batch flag to indicate its not interactive. 41 | - "--batch" 42 | - "--local-user" 43 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 44 | - "--output" 45 | - "${signature}" 46 | - "--detach-sign" 47 | - "${artifact}" 48 | release: 49 | extra_files: 50 | - glob: "terraform-registry-manifest.json" 51 | name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" 52 | # If you want to manually examine the release before its live, uncomment this line: 53 | draft: true 54 | changelog: 55 | disable: false 56 | use: github 57 | -------------------------------------------------------------------------------- /internal/provider/validators.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | // Validator for valid namespace YAML 12 | // Ensures the YAML is a valid single namespace definition 13 | // Returns a diagnostic error if not 14 | 15 | type namespaceYAMLValidator struct{} 16 | 17 | func (v namespaceYAMLValidator) Description(_ context.Context) string { 18 | return "Validates that the YAML is a valid single namespace definition" 19 | } 20 | 21 | func (v namespaceYAMLValidator) MarkdownDescription(ctx context.Context) string { 22 | return v.Description(ctx) 23 | } 24 | 25 | func (v namespaceYAMLValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { 26 | if req.ConfigValue.IsNull() || req.ConfigValue.ValueString() == "" { 27 | // Let the non-empty validator handle this case 28 | return 29 | } 30 | _, err := getRuleNamespaceFromYAML(ctx, req.ConfigValue.ValueString()) 31 | if err != nil { 32 | resp.Diagnostics.AddAttributeError( 33 | req.Path, 34 | "Invalid namespace YAML", 35 | fmt.Sprintf("Namespace definition is not valid: %s", err.Error()), 36 | ) 37 | } 38 | } 39 | 40 | // yamlSyntaxValidator checks that a string is valid YAML 41 | 42 | type yamlSyntaxValidator struct{} 43 | 44 | func (v yamlSyntaxValidator) Description(_ context.Context) string { 45 | return "Ensures the string is valid YAML syntax" 46 | } 47 | 48 | func (v yamlSyntaxValidator) MarkdownDescription(ctx context.Context) string { 49 | return v.Description(ctx) 50 | } 51 | 52 | func (v yamlSyntaxValidator) ValidateString(_ context.Context, req validator.StringRequest, resp *validator.StringResponse) { 53 | if req.ConfigValue.IsNull() || req.ConfigValue.ValueString() == "" { 54 | return 55 | } 56 | var temp interface{} 57 | err := yaml.Unmarshal([]byte(req.ConfigValue.ValueString()), &temp) 58 | if err != nil { 59 | resp.Diagnostics.AddAttributeError( 60 | req.Path, 61 | "Invalid YAML syntax", 62 | err.Error(), 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "mimirtool Provider" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # mimirtool Provider 10 | 11 | 12 | 13 | ## Example Usage 14 | 15 | ```terraform 16 | provider "mimirtool" { 17 | address = "http://localhost:9009" 18 | tenant_id = "anonymous" 19 | } 20 | ``` 21 | 22 | 23 | ## Schema 24 | 25 | ### Required 26 | 27 | - `address` (String) Address to use when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_ADDRESS` or `MIMIR_ADDRESS` environment variable. 28 | 29 | ### Optional 30 | 31 | - `alertmanager_http_prefix` (String) Path prefix to use for alertmanager. May alternatively be set via the `MIMIRTOOL_ALERTMANAGER_HTTP_PREFIX` or `MIMIR_ALERTMANAGER_HTTP_PREFIX` environment variable. 32 | - `api_key` (String, Sensitive) API key to use when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_API_KEY` or `MIMIR_API_KEY` environment variable. 33 | - `api_user` (String) API user to use when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_API_USER` or `MIMIR_API_USER` environment variable. 34 | - `auth_token` (String, Sensitive) Authentication token for bearer token or JWT auth when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_AUTH_TOKEN` or `MIMIR_AUTH_TOKEN` environment variable. 35 | - `insecure_skip_verify` (Boolean) Skip TLS certificate verification. May alternatively be set via the `MIMIRTOOL_INSECURE_SKIP_VERIFY` or `MIMIR_INSECURE_SKIP_VERIFY` environment variable. 36 | - `prometheus_http_prefix` (String) Path prefix to use for rules. May alternatively be set via the `MIMIRTOOL_PROMETHEUS_HTTP_PREFIX` or `MIMIR_PROMETHEUS_HTTP_PREFIX` environment variable. 37 | - `tenant_id` (String) Tenant ID to use when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_TENANT_ID` or `MIMIR_TENANT_ID` environment variable. 38 | - `tls_ca_path` (String) Certificate CA bundle to use to verify the MIMIR server's certificate. May alternatively be set via the `MIMIRTOOL_TLS_CA_PATH` or `MIMIR_TLS_CA_PATH` environment variable. 39 | - `tls_cert_path` (String) Client TLS certificate file to use to authenticate to the MIMIR server. May alternatively be set via the `MIMIRTOOL_TLS_CERT_PATH` or `MIMIR_TLS_CERT_PATH` environment variable. 40 | - `tls_key_path` (String) Client TLS key file to use to authenticate to the MIMIR server. May alternatively be set via the `MIMIRTOOL_TLS_KEY_PATH` or `MIMIR_TLS_KEY_PATH` environment variable. 41 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | build-tags: 4 | - netgo 5 | - requires_docker 6 | - requires_libpcap 7 | output: 8 | formats: 9 | text: 10 | path: stdout 11 | colors: false 12 | linters: 13 | default: none 14 | enable: 15 | # Checks whether HTTP response body is closed successfully. 16 | - bodyclose 17 | # Checks whether Rows.Err of rows is checked successfully. 18 | # - rowserrcheck # Does not support generics yet (see https://github.com/golangci/golangci-lint/issues/2649) 19 | # Detects places where loop variables are copied. 20 | - copyloopvar 21 | # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()). 22 | - dogsled 23 | # Tool for code clone detection. 24 | - dupl 25 | # Checking for unchecked errors in Go code. 26 | # These unchecked errors can be critical bugs in some cases. 27 | - errcheck 28 | # Find code that will cause problems with the error wrapping scheme introduced in Go 1.13. 29 | - errorlint 30 | - fatcontext 31 | # Finds repeated strings that could be replaced by a constant. 32 | - goconst 33 | # Checks that printf-like functions are named with f at the end. 34 | - goprintffuncname 35 | # Inspects source code for security problems. 36 | - gosec 37 | # Vet examines Go source code and reports suspicious constructs. 38 | - govet 39 | # Detects when assignments to existing variables are not used. 40 | - ineffassign 41 | # Finds commonly misspelled English words. 42 | - misspell 43 | # Checks that functions with naked returns are not longer than a maximum size (can be zero). 44 | - nakedret 45 | # It's a set of rules from staticcheck. 46 | # Reports ill-formed or insufficient nolint directives. 47 | - nolintlint 48 | # Fast, configurable, extensible, flexible, and beautiful linter for Go. 49 | - revive 50 | - staticcheck 51 | # Remove unnecessary type conversions. 52 | - unconvert 53 | # Reports unused function parameters. 54 | - unparam 55 | # Reports unused function parameters. 56 | - unused 57 | settings: 58 | errorlint: 59 | errorf: false 60 | asserts: false 61 | comparison: true 62 | exclusions: 63 | generated: lax 64 | presets: 65 | - comments 66 | - common-false-positives 67 | - legacy 68 | - std-error-handling 69 | rules: 70 | - linters: 71 | - revive 72 | text: if-return 73 | paths: 74 | - third_party$ 75 | - builtin$ 76 | - examples$ 77 | formatters: 78 | enable: 79 | # Check whether code was gofmt-ed. 80 | - gofmt 81 | # Check import statements are formatted according to the 'goimport' command. 82 | - goimports 83 | exclusions: 84 | generated: lax 85 | paths: 86 | - third_party$ 87 | - builtin$ 88 | - examples$ 89 | -------------------------------------------------------------------------------- /internal/provider/alertmanager_resource_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 8 | ) 9 | 10 | func TestAccResourceAlertmanager(t *testing.T) { 11 | resource.Test(t, resource.TestCase{ 12 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 13 | Steps: []resource.TestStep{ 14 | { 15 | Config: testAccResourceAlertmanager, 16 | Check: resource.ComposeTestCheckFunc( 17 | resource.TestCheckResourceAttr( 18 | "mimirtool_alertmanager.demo", "config_yaml", testAccResourceAlertmanagerYaml), 19 | resource.TestCheckResourceAttr( 20 | "mimirtool_alertmanager.demo", "templates_config_yaml.default_template", testAccResourceAlertmanagerTemplate), 21 | ), 22 | }, 23 | }, 24 | }) 25 | } 26 | 27 | func TestAccResourceAlertmanagerParseError(t *testing.T) { 28 | resource.Test(t, resource.TestCase{ 29 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 30 | Steps: []resource.TestStep{ 31 | { 32 | Config: testAccResourceAlertmanagerParseError, 33 | ExpectError: regexp.MustCompile(`(?i)invalid yaml syntax|yaml`), 34 | }, 35 | }, 36 | }) 37 | } 38 | 39 | func TestAccResourceAlertmanagerImport(t *testing.T) { 40 | resource.Test(t, resource.TestCase{ 41 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 42 | Steps: []resource.TestStep{ 43 | { 44 | Config: testAccResourceAlertmanager, 45 | }, 46 | { 47 | ResourceName: "mimirtool_alertmanager.demo", 48 | ImportState: true, 49 | ImportStateVerify: true, 50 | }, 51 | }, 52 | }) 53 | } 54 | 55 | const testAccResourceAlertmanager = ` 56 | provider "mimirtool" { 57 | address = "http://localhost:8080" 58 | } 59 | 60 | resource "mimirtool_alertmanager" "demo" { 61 | config_yaml = file("testdata/example_alertmanager_config.yaml") 62 | templates_config_yaml = { 63 | default_template = file("testdata/example_alertmanager_template.tmpl") 64 | } 65 | } 66 | ` 67 | 68 | const testAccResourceAlertmanagerYaml = `--- 69 | # See: https://grafana.com/docs/mimir/latest/references/http-api/#alertmanager 70 | global: 71 | smtp_smarthost: 'localhost:25' 72 | smtp_from: 'youraddress@example.org' 73 | templates: 74 | - 'default_template' 75 | route: 76 | receiver: example-email 77 | receivers: 78 | - name: example-email 79 | email_configs: 80 | - to: 'youraddress@example.org' 81 | ` 82 | const testAccResourceAlertmanagerTemplate = `{{ define "__alertmanager" }}AlertManager{{ end }} 83 | {{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver | urlquery }}{{ end }} 84 | ` 85 | 86 | const testAccResourceAlertmanagerParseError = ` 87 | provider "mimirtool" { 88 | address = "http://localhost:8080" 89 | } 90 | 91 | resource "mimirtool_alertmanager" "demo" { 92 | config_yaml = <= 1.1.6 8 | - [Go](https://golang.org/doc/install) >= 1.20 9 | 10 | ## Building The Provider 11 | 12 | 1. Clone the repository 13 | 1. Enter the repository directory 14 | 1. Build the provider using the Go `install` command: 15 | ```sh 16 | $ go install 17 | ``` 18 | 19 | To learn more about how to overrides the provider built locally have a look at [the developper documentation](https://developer.hashicorp.com/terraform/cli/config/config-file#development-overrides-for-provider-developers) 20 | 21 | ## Developing the Provider 22 | 23 | If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (see [Requirements](#requirements) above). 24 | 25 | To compile the provider, run `go install`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. 26 | 27 | To generate or update documentation, run `go generate`. 28 | 29 | In order to run the full suite of Acceptance tests, run `make testacc`. 30 | 31 | *Note:* Acceptance tests create real resources, and often cost money to run. 32 | 33 | ```sh 34 | $ make testacc 35 | ``` 36 | 37 | ### Adding Dependencies 38 | 39 | This provider uses [Go modules](https://github.com/golang/go/wiki/Modules). 40 | Please see the Go documentation for the most up to date information about using Go modules. 41 | 42 | To add a new dependency `github.com/author/dependency` to your Terraform provider: 43 | 44 | ``` 45 | go get github.com/author/dependency 46 | go mod tidy 47 | ``` 48 | 49 | Then commit the changes to `go.mod` and `go.sum`. 50 | 51 | ## Documentation 52 | 53 | Documentation is generated with 54 | [tfplugindocs](https://github.com/hashicorp/terraform-plugin-docs). Generated 55 | files are in `docs/` and should not be updated manually. They are derived from: 56 | 57 | - Schema `Description` fields in the provider Go code. 58 | - [examples/](./examples) 59 | - [templates/](./templates) 60 | 61 | Use `go generate` to update generated docs. 62 | 63 | ## Releasing 64 | 65 | Builds and releases are automated with GitHub Actions and 66 | [GoReleaser](https://github.com/goreleaser/goreleaser/). 67 | 68 | Currently there are a few manual steps to this: 69 | 70 | 1. Kick off the release: 71 | 72 | ```sh 73 | RELEASE_VERSION=v... \ 74 | make release 75 | ``` 76 | 77 | 2. Publish release: 78 | 79 | The Action creates the release, but leaves it in "draft" state. Open it up in 80 | a [browser](https://github.com/grafana/terraform-provider-grafana/releases) 81 | and if all looks well, click the `Auto-generate release notes` button and mash the publish button. 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to terraform-provider-mixtool 2 | 3 | This project accepts contributions. In order to contribute, you should 4 | pay attention to a few things: 5 | 6 | 1. your code must follow the golang coding style rules (go fmt) 7 | 2. your code must be added to the unit tests. Documentation is excluded 8 | from this task 9 | 3. your code must be documented (README.md) 10 | 4. your work must be signed (see below) 11 | 5. you may contribute through GitHub Pull Requests which must pass all tests 12 | 13 | # Submitting Modifications 14 | 15 | The contributions should be submitted through Github Pull Requests 16 | and follow the DCO which is defined below. 17 | 18 | # Licensing for new files 19 | 20 | terraform-provider-mixtool is licensed under the Apache License 2.0. Anything 21 | contributed to terraform-provider-mixtool must be released under this license. 22 | 23 | When introducing a new file into the project, please make sure it has a 24 | copyright header making clear under which license it's being released. 25 | 26 | # Developer Certificate of Origin (DCO) 27 | 28 | To improve tracking of contributions to this project we will use a 29 | process modeled on the modified DCO 1.1 and use a "sign-off" procedure 30 | on patches that are being emailed around or contributed in any other 31 | way. 32 | 33 | The sign-off is a simple line at the end of the explanation for the 34 | patch, which certifies that you wrote it or otherwise have the right 35 | to pass it on as an open-source patch. The rules are pretty simple, 36 | if you can certify the below: 37 | 38 | By making a contribution to this project, I certify that: 39 | 40 | (a) The contribution was created in whole or in part by me and I have 41 | the right to submit it under the open source license indicated in 42 | the file; or 43 | 44 | (b) The contribution is based upon previous work that, to the best of 45 | my knowledge, is covered under an appropriate open source License 46 | and I have the right under that license to submit that work with 47 | modifications, whether created in whole or in part by me, under 48 | the same open source license (unless I am permitted to submit 49 | under a different license), as indicated in the file; or 50 | 51 | (c) The contribution was provided directly to me by some other person 52 | who certified (a), (b) or (c) and I have not modified it. 53 | 54 | (d) The contribution is made free of any other party's intellectual 55 | property claims or rights. 56 | 57 | (e) I understand and agree that this project and the contribution are 58 | public and that a record of the contribution (including all 59 | personal information I submit with it, including my sign-off) is 60 | maintained indefinitely and may be redistributed consistent with 61 | this project or the open source license(s) involved. 62 | 63 | 64 | then you just add a line saying 65 | 66 | Signed-off-by: Random J Developer 67 | 68 | using your real name (sorry, no pseudonyms or anonymous contributions.) 69 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - "README.md" 6 | push: 7 | paths-ignore: 8 | - "README.md" 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | steps: 15 | - name: Check out code into the Go module directory 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version-file: "go.mod" 22 | cache: true 23 | id: go 24 | 25 | - name: Get dependencies 26 | run: | 27 | go mod download 28 | 29 | - name: Build 30 | run: | 31 | go build -v . 32 | 33 | generate: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: actions/setup-go@v3 38 | with: 39 | go-version-file: "go.mod" 40 | cache: true 41 | - uses: hashicorp/setup-terraform@v3 42 | - run: go generate ./... 43 | - name: git diff 44 | run: | 45 | git diff --compact-summary --exit-code || \ 46 | (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1) 47 | 48 | # run acceptance tests in a matrix with Terraform core versions 49 | test: 50 | name: Matrix Test 51 | needs: build 52 | runs-on: ubuntu-latest 53 | timeout-minutes: 15 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | # list whatever Terraform versions here you would like to support 58 | # As a best practice, HashiCorp expects customers to stay current 59 | # within two (2) releases from the latest major release in order to 60 | # receive optimal support. Release updates for Customers are provided 61 | # regularly on HashiCorp product pages for each HashiCorp product. 62 | # See: https://support.hashicorp.com/hc/en-us/articles/360021185113 63 | terraform: 64 | - "1.8.*" 65 | - "1.9.*" 66 | - "1.10.*" 67 | # We will support the 3 last minor versions as Grafana honor 2 versions 68 | # before dropping a deprecated flag 69 | mimir: 70 | - "2.13.0" 71 | - "2.14.0" 72 | - "2.15.0" 73 | 74 | steps: 75 | - name: Check out code into the Go module directory 76 | uses: actions/checkout@v3 77 | 78 | - name: Set up Go 79 | uses: actions/setup-go@v3 80 | with: 81 | go-version-file: "go.mod" 82 | cache: true 83 | id: go 84 | 85 | - uses: hashicorp/setup-terraform@v2 86 | with: 87 | terraform_version: ${{ matrix.terraform }} 88 | terraform_wrapper: false 89 | 90 | - name: Get dependencies 91 | run: | 92 | go mod download 93 | 94 | # Not using `services` as they do not allow us to configure mimir and we 95 | # don't want to build/maintain our own image just for that. 96 | # The makefile will take care of spawning a mimir instance. 97 | - name: TF acceptance tests 98 | timeout-minutes: 10 99 | env: 100 | TF_ACC: "1" 101 | MIMIRTOOL_ADDRESS: "http://localhost:8080" 102 | run: | 103 | MIMIR_VERSION=${{ matrix.mimir }} make testacc 104 | -------------------------------------------------------------------------------- /internal/provider/alertmanager_resource.go: -------------------------------------------------------------------------------- 1 | // This file implements the Terraform resource for managing the Alertmanager configuration in Grafana Mimir. 2 | // It supports create, read, update, and delete operations, and handles templates as well as config YAML. 3 | // See: https://grafana.com/docs/mimir/latest/references/http-api/#alertmanager 4 | 5 | package provider 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | 11 | "errors" 12 | 13 | "github.com/grafana/mimir/pkg/mimirtool/client" 14 | "github.com/hashicorp/terraform-plugin-framework/path" 15 | "github.com/hashicorp/terraform-plugin-framework/resource" 16 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 17 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 18 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 19 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 20 | "github.com/hashicorp/terraform-plugin-framework/types" 21 | "github.com/hashicorp/terraform-plugin-log/tflog" 22 | ) 23 | 24 | // Ensure provider defined types fully satisfy framework interfaces. 25 | var ( 26 | _ resource.Resource = &AlertmanagerResource{} 27 | _ resource.ResourceWithImportState = &AlertmanagerResource{} 28 | ) 29 | 30 | func NewAlertmanagerResource() resource.Resource { 31 | return &AlertmanagerResource{} 32 | } 33 | 34 | // AlertmanagerResource defines the resource implementation. 35 | type AlertmanagerResource struct { 36 | client mimirClientInterface 37 | } 38 | 39 | func (r *AlertmanagerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 40 | resp.TypeName = req.ProviderTypeName + "_alertmanager" 41 | } 42 | 43 | func (r *AlertmanagerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 44 | resp.Schema = schema.Schema{ 45 | MarkdownDescription: "Manages the Alertmanager configuration in Grafana Mimir. [Official documentation](https://grafana.com/docs/mimir/latest/references/http-api/#alertmanager)", 46 | Attributes: map[string]schema.Attribute{ 47 | "id": schema.StringAttribute{ 48 | Computed: true, 49 | MarkdownDescription: "ID for the Alertmanager resource (always 'alertmanager'). This is a singleton resource per tenant.", 50 | PlanModifiers: []planmodifier.String{ 51 | stringplanmodifier.UseStateForUnknown(), 52 | }, 53 | }, 54 | "config_yaml": schema.StringAttribute{ 55 | MarkdownDescription: "The Alertmanager configuration to load in Grafana Mimir as YAML. This should be a valid Alertmanager YAML config.", 56 | Required: true, 57 | Validators: []validator.String{ 58 | yamlSyntaxValidator{}, 59 | }, 60 | }, 61 | "templates_config_yaml": schema.MapAttribute{ 62 | MarkdownDescription: "A map of template names to template YAML content to load along with the Alertmanager configuration.", 63 | ElementType: types.StringType, 64 | Optional: true, 65 | }, 66 | }, 67 | } 68 | } 69 | 70 | func (r *AlertmanagerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 71 | if req.ProviderData == nil { 72 | return 73 | } 74 | client, ok := req.ProviderData.(mimirClientInterface) 75 | if !ok { 76 | resp.Diagnostics.AddError( 77 | "Unexpected Resource Configure Type", 78 | fmt.Sprintf("Expected mimirClientInterface, got: %T. Please report this issue to the provider developers.", req.ProviderData), 79 | ) 80 | return 81 | } 82 | r.client = client 83 | } 84 | 85 | type AlertmanagerResourceModel struct { 86 | ID types.String `tfsdk:"id"` 87 | ConfigYAML types.String `tfsdk:"config_yaml"` 88 | TemplatesConfigYAML types.Map `tfsdk:"templates_config_yaml"` 89 | } 90 | 91 | func (r *AlertmanagerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 92 | var plan AlertmanagerResourceModel 93 | resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) 94 | if resp.Diagnostics.HasError() { 95 | return 96 | } 97 | 98 | alertmanagerConfig := plan.ConfigYAML.ValueString() 99 | templates := mapStringFromTypesMap(plan.TemplatesConfigYAML) 100 | 101 | err := r.client.CreateAlertmanagerConfig(ctx, alertmanagerConfig, templates) 102 | if err != nil { 103 | tflog.Error(ctx, "Failed to create Alertmanager config via POST", map[string]interface{}{"error": err}) 104 | resp.Diagnostics.AddError( 105 | "Error creating Alertmanager config", 106 | fmt.Sprintf("Failed to create Alertmanager config via POST: %s", err), 107 | ) 108 | return 109 | } 110 | 111 | plan.ID = types.StringValue("alertmanager") 112 | resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) 113 | } 114 | 115 | func (r *AlertmanagerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 116 | var state AlertmanagerResourceModel 117 | resp.Diagnostics.Append(req.State.Get(ctx, &state)...) 118 | if resp.Diagnostics.HasError() { 119 | return 120 | } 121 | 122 | alertmanagerConfig, templates, err := r.client.GetAlertmanagerConfig(ctx) 123 | if err != nil { 124 | if errors.Is(err, client.ErrResourceNotFound) { 125 | tflog.Info(ctx, "No alertmanager config found in backend; removing from state") 126 | resp.State.RemoveResource(ctx) 127 | return 128 | } 129 | tflog.Error(ctx, "Failed to read Alertmanager config", map[string]interface{}{"error": err}) 130 | resp.Diagnostics.AddError( 131 | "Error reading Alertmanager config", 132 | fmt.Sprintf("Failed to read Alertmanager config: %s", err), 133 | ) 134 | return 135 | } 136 | 137 | state.ConfigYAML = types.StringValue(alertmanagerConfig) 138 | state.TemplatesConfigYAML = typeMapFromMapString(templates) 139 | 140 | resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) 141 | } 142 | 143 | // The backend API does not support PUT for Alertmanager config updates. 144 | // Therefore, Update uses the same logic as Create (POST) to replace the configuration. 145 | func (r *AlertmanagerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 146 | var plan AlertmanagerResourceModel 147 | resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) 148 | if resp.Diagnostics.HasError() { 149 | return 150 | } 151 | 152 | alertmanagerConfig := plan.ConfigYAML.ValueString() 153 | templates := mapStringFromTypesMap(plan.TemplatesConfigYAML) 154 | 155 | err := r.client.CreateAlertmanagerConfig(ctx, alertmanagerConfig, templates) 156 | if err != nil { 157 | tflog.Error(ctx, "Failed to update Alertmanager config via POST", map[string]interface{}{"error": err}) 158 | resp.Diagnostics.AddError( 159 | "Error updating Alertmanager config", 160 | fmt.Sprintf("Failed to update Alertmanager config via POST: %s", err), 161 | ) 162 | return 163 | } 164 | 165 | plan.ID = types.StringValue("alertmanager") 166 | resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) 167 | } 168 | 169 | func (r *AlertmanagerResource) Delete(ctx context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { 170 | err := r.client.DeleteAlermanagerConfig(ctx) 171 | if err != nil { 172 | tflog.Error(ctx, "Failed to delete Alertmanager config", map[string]interface{}{"error": err}) 173 | resp.Diagnostics.AddError( 174 | "Error deleting Alertmanager config", 175 | fmt.Sprintf("Failed to delete Alertmanager config: %s", err), 176 | ) 177 | return 178 | } 179 | resp.State.RemoveResource(ctx) 180 | } 181 | 182 | func (r *AlertmanagerResource) ImportState(ctx context.Context, _ resource.ImportStateRequest, resp *resource.ImportStateResponse) { 183 | resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), "alertmanager")...) 184 | } 185 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ovh/terraform-provider-mimirtool 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/grafana/mimir v0.0.0-20240722104006-e8e4dc777899 7 | github.com/hashicorp/terraform-plugin-framework v1.15.0 8 | github.com/hashicorp/terraform-plugin-go v0.28.0 9 | github.com/hashicorp/terraform-plugin-log v0.9.0 10 | github.com/hashicorp/terraform-plugin-testing v1.13.2 11 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 12 | ) 13 | 14 | require ( 15 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect 16 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 // indirect 17 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect 18 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect 19 | github.com/BurntSushi/toml v1.2.1 // indirect 20 | github.com/DmitriyVTitov/size v1.5.0 // indirect 21 | github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect 22 | github.com/Masterminds/goutils v1.1.1 // indirect 23 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 24 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 25 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 26 | github.com/agext/levenshtein v1.2.2 // indirect 27 | github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect 28 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 29 | github.com/armon/go-metrics v0.4.1 // indirect 30 | github.com/armon/go-radix v1.0.0 // indirect 31 | github.com/aws/aws-sdk-go v1.53.16 // indirect 32 | github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/bgentry/speakeasy v0.1.0 // indirect 35 | github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect 36 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 37 | github.com/cloudflare/circl v1.6.1 // indirect 38 | github.com/coreos/go-semver v0.3.0 // indirect 39 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 40 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 41 | github.com/dennwc/varint v1.0.0 // indirect 42 | github.com/dgraph-io/ristretto v0.1.1 // indirect 43 | github.com/dustin/go-humanize v1.0.1 // indirect 44 | github.com/edsrzf/mmap-go v1.1.0 // indirect 45 | github.com/efficientgo/core v1.0.0-rc.0.0.20221201130417-ba593f67d2a4 // indirect 46 | github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect 47 | github.com/fatih/color v1.16.0 // indirect 48 | github.com/go-kit/log v0.2.1 // indirect 49 | github.com/go-logfmt/logfmt v0.6.0 // indirect 50 | github.com/go-logr/logr v1.4.2 // indirect 51 | github.com/go-logr/stdr v1.2.2 // indirect 52 | github.com/gogo/googleapis v1.4.1 // indirect 53 | github.com/gogo/protobuf v1.3.2 // indirect 54 | github.com/gogo/status v1.1.1 // indirect 55 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 56 | github.com/golang/glog v1.2.4 // indirect 57 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 58 | github.com/golang/protobuf v1.5.4 // indirect 59 | github.com/golang/snappy v0.0.4 // indirect 60 | github.com/google/btree v1.1.2 // indirect 61 | github.com/google/go-cmp v0.7.0 // indirect 62 | github.com/google/uuid v1.6.0 // indirect 63 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect 64 | github.com/hashicorp/cli v1.1.7 // indirect 65 | github.com/hashicorp/consul/api v1.29.1 // indirect 66 | github.com/hashicorp/errwrap v1.1.0 // indirect 67 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 68 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 69 | github.com/hashicorp/go-cty v1.5.0 // indirect 70 | github.com/hashicorp/go-hclog v1.6.3 // indirect 71 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 72 | github.com/hashicorp/go-msgpack v1.1.5 // indirect 73 | github.com/hashicorp/go-multierror v1.1.1 // indirect 74 | github.com/hashicorp/go-plugin v1.6.3 // indirect 75 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 76 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 77 | github.com/hashicorp/go-sockaddr v1.0.6 // indirect 78 | github.com/hashicorp/go-uuid v1.0.3 // indirect 79 | github.com/hashicorp/go-version v1.7.0 // indirect 80 | github.com/hashicorp/golang-lru v1.0.2 // indirect 81 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 82 | github.com/hashicorp/hc-install v0.9.2 // indirect 83 | github.com/hashicorp/hcl/v2 v2.23.0 // indirect 84 | github.com/hashicorp/logutils v1.0.0 // indirect 85 | github.com/hashicorp/memberlist v0.5.0 // indirect 86 | github.com/hashicorp/serf v0.10.1 // indirect 87 | github.com/hashicorp/terraform-exec v0.24.0 // indirect 88 | github.com/hashicorp/terraform-json v0.27.2 // indirect 89 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 // indirect 90 | github.com/hashicorp/terraform-registry-address v0.2.5 // indirect 91 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 92 | github.com/hashicorp/yamux v0.1.1 // indirect 93 | github.com/huandu/xstrings v1.3.3 // indirect 94 | github.com/imdario/mergo v0.3.16 // indirect 95 | github.com/jmespath/go-jmespath v0.4.0 // indirect 96 | github.com/jpillora/backoff v1.0.0 // indirect 97 | github.com/klauspost/compress v1.17.9 // indirect 98 | github.com/kylelemons/godebug v1.1.0 // indirect 99 | github.com/mattn/go-colorable v0.1.14 // indirect 100 | github.com/mattn/go-isatty v0.0.20 // indirect 101 | github.com/mattn/go-runewidth v0.0.9 // indirect 102 | github.com/miekg/dns v1.1.59 // indirect 103 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 104 | github.com/mitchellh/copystructure v1.2.0 // indirect 105 | github.com/mitchellh/go-homedir v1.1.0 // indirect 106 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 107 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 108 | github.com/mitchellh/mapstructure v1.5.0 // indirect 109 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 110 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 111 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 112 | github.com/oklog/run v1.1.0 // indirect 113 | github.com/oklog/ulid v1.3.1 // indirect 114 | github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect 115 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 116 | github.com/pkg/errors v0.9.1 // indirect 117 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 118 | github.com/posener/complete v1.2.3 // indirect 119 | github.com/prometheus/client_golang v1.19.1 // indirect 120 | github.com/prometheus/client_model v0.6.1 // indirect 121 | github.com/prometheus/common v0.54.1-0.20240615204547-04635d2962f9 // indirect 122 | github.com/prometheus/common/sigv4 v0.1.0 // indirect 123 | github.com/prometheus/procfs v0.15.1 // indirect 124 | github.com/prometheus/prometheus v1.99.0 // indirect 125 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect 126 | github.com/shopspring/decimal v1.3.1 // indirect 127 | github.com/sirupsen/logrus v1.9.3 // indirect 128 | github.com/spf13/cast v1.5.0 // indirect 129 | github.com/stretchr/testify v1.10.0 // indirect 130 | github.com/thanos-io/objstore v0.0.0-20240622095743-1afe5d4bc3cd // indirect 131 | github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect 132 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 133 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 134 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 135 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 136 | github.com/xlab/treeprint v1.2.0 // indirect 137 | github.com/yuin/goldmark v1.7.7 // indirect 138 | github.com/yuin/goldmark-meta v1.1.0 // indirect 139 | github.com/zclconf/go-cty v1.17.0 // indirect 140 | go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect 141 | go.etcd.io/etcd/api/v3 v3.5.4 // indirect 142 | go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect 143 | go.etcd.io/etcd/client/v3 v3.5.4 // indirect 144 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 145 | go.opentelemetry.io/otel v1.34.0 // indirect 146 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 147 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 148 | go.uber.org/atomic v1.11.0 // indirect 149 | go.uber.org/goleak v1.3.0 // indirect 150 | go.uber.org/multierr v1.11.0 // indirect 151 | go.uber.org/zap v1.21.0 // indirect 152 | golang.org/x/crypto v0.45.0 // indirect 153 | golang.org/x/mod v0.29.0 // indirect 154 | golang.org/x/net v0.47.0 // indirect 155 | golang.org/x/oauth2 v0.26.0 // indirect 156 | golang.org/x/sync v0.18.0 // indirect 157 | golang.org/x/sys v0.38.0 // indirect 158 | golang.org/x/text v0.31.0 // indirect 159 | golang.org/x/time v0.5.0 // indirect 160 | golang.org/x/tools v0.38.0 // indirect 161 | google.golang.org/appengine v1.6.8 // indirect 162 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect 163 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 164 | google.golang.org/grpc v1.72.1 // indirect 165 | google.golang.org/protobuf v1.36.6 // indirect 166 | gopkg.in/yaml.v2 v2.4.0 // indirect 167 | k8s.io/apimachinery v0.29.3 // indirect 168 | k8s.io/client-go v0.29.3 // indirect 169 | k8s.io/klog/v2 v2.120.1 // indirect 170 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 171 | ) 172 | 173 | require ( 174 | github.com/grafana/dskit v0.0.0-20240719153732-6e8a03e781de 175 | github.com/hashicorp/terraform-plugin-docs v0.24.0 176 | gopkg.in/yaml.v3 v3.0.1 177 | ) 178 | 179 | // Replace memberlist with our fork which includes some fixes that haven't been 180 | // merged upstream yet: 181 | // - https://github.com/hashicorp/memberlist/pull/260 182 | // - https://github.com/grafana/memberlist/pull/3 183 | // - https://github.com/hashicorp/memberlist/pull/263 184 | replace github.com/hashicorp/memberlist => github.com/grafana/memberlist v0.3.1-0.20220714140823-09ffed8adbbe 185 | 186 | // gopkg.in/yaml.v3 187 | // + https://github.com/go-yaml/yaml/pull/691 188 | // + https://github.com/go-yaml/yaml/pull/876 189 | replace gopkg.in/yaml.v3 => github.com/colega/go-yaml-yaml v0.0.0-20220720105220-255a8d16d094 190 | 191 | // We are using our modified version of the upstream GO regexp (branch remotes/origin/speedup) 192 | replace github.com/grafana/regexp => github.com/grafana/regexp v0.0.0-20240531075221-3685f1377d7b 193 | 194 | // Replace goautoneg with a fork until https://github.com/munnerz/goautoneg/pull/6 is merged 195 | replace github.com/munnerz/goautoneg => github.com/grafana/goautoneg v0.0.0-20240607115440-f335c04c58ce 196 | 197 | // Using a fork of Prometheus with Mimir-specific changes. 198 | replace github.com/prometheus/prometheus => github.com/grafana/mimir-prometheus v0.0.0-20240711155029-3af4160b0afb 199 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 OVHcloud 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | A copy of the license terms follows: 16 | 17 | Apache License 18 | Version 2.0, January 2004 19 | http://www.apache.org/licenses/ 20 | 21 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 22 | 23 | 1. Definitions. 24 | 25 | "License" shall mean the terms and conditions for use, reproduction, 26 | and distribution as defined by Sections 1 through 9 of this document. 27 | 28 | "Licensor" shall mean the copyright owner or entity authorized by 29 | the copyright owner that is granting the License. 30 | 31 | "Legal Entity" shall mean the union of the acting entity and all 32 | other entities that control, are controlled by, or are under common 33 | control with that entity. For the purposes of this definition, 34 | "control" means (i) the power, direct or indirect, to cause the 35 | direction or management of such entity, whether by contract or 36 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 37 | outstanding shares, or (iii) beneficial ownership of such entity. 38 | 39 | "You" (or "Your") shall mean an individual or Legal Entity 40 | exercising permissions granted by this License. 41 | 42 | "Source" form shall mean the preferred form for making modifications, 43 | including but not limited to software source code, documentation 44 | source, and configuration files. 45 | 46 | "Object" form shall mean any form resulting from mechanical 47 | transformation or translation of a Source form, including but 48 | not limited to compiled object code, generated documentation, 49 | and conversions to other media types. 50 | 51 | "Work" shall mean the work of authorship, whether in Source or 52 | Object form, made available under the License, as indicated by a 53 | copyright notice that is included in or attached to the work 54 | (an example is provided in the Appendix below). 55 | 56 | "Derivative Works" shall mean any work, whether in Source or Object 57 | form, that is based on (or derived from) the Work and for which the 58 | editorial revisions, annotations, elaborations, or other modifications 59 | represent, as a whole, an original work of authorship. For the purposes 60 | of this License, Derivative Works shall not include works that remain 61 | separable from, or merely link (or bind by name) to the interfaces of, 62 | the Work and Derivative Works thereof. 63 | 64 | "Contribution" shall mean any work of authorship, including 65 | the original version of the Work and any modifications or additions 66 | to that Work or Derivative Works thereof, that is intentionally 67 | submitted to Licensor for inclusion in the Work by the copyright owner 68 | or by an individual or Legal Entity authorized to submit on behalf of 69 | the copyright owner. For the purposes of this definition, "submitted" 70 | means any form of electronic, verbal, or written communication sent 71 | to the Licensor or its representatives, including but not limited to 72 | communication on electronic mailing lists, source code control systems, 73 | and issue tracking systems that are managed by, or on behalf of, the 74 | Licensor for the purpose of discussing and improving the Work, but 75 | excluding communication that is conspicuously marked or otherwise 76 | designated in writing by the copyright owner as "Not a Contribution." 77 | 78 | "Contributor" shall mean Licensor and any individual or Legal Entity 79 | on behalf of whom a Contribution has been received by Licensor and 80 | subsequently incorporated within the Work. 81 | 82 | 2. Grant of Copyright License. Subject to the terms and conditions of 83 | this License, each Contributor hereby grants to You a perpetual, 84 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 85 | copyright license to reproduce, prepare Derivative Works of, 86 | publicly display, publicly perform, sublicense, and distribute the 87 | Work and such Derivative Works in Source or Object form. 88 | 89 | 3. Grant of Patent License. Subject to the terms and conditions of 90 | this License, each Contributor hereby grants to You a perpetual, 91 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 92 | (except as stated in this section) patent license to make, have made, 93 | use, offer to sell, sell, import, and otherwise transfer the Work, 94 | where such license applies only to those patent claims licensable 95 | by such Contributor that are necessarily infringed by their 96 | Contribution(s) alone or by combination of their Contribution(s) 97 | with the Work to which such Contribution(s) was submitted. If You 98 | institute patent litigation against any entity (including a 99 | cross-claim or counterclaim in a lawsuit) alleging that the Work 100 | or a Contribution incorporated within the Work constitutes direct 101 | or contributory patent infringement, then any patent licenses 102 | granted to You under this License for that Work shall terminate 103 | as of the date such litigation is filed. 104 | 105 | 4. Redistribution. You may reproduce and distribute copies of the 106 | Work or Derivative Works thereof in any medium, with or without 107 | modifications, and in Source or Object form, provided that You 108 | meet the following conditions: 109 | 110 | (a) You must give any other recipients of the Work or 111 | Derivative Works a copy of this License; and 112 | 113 | (b) You must cause any modified files to carry prominent notices 114 | stating that You changed the files; and 115 | 116 | (c) You must retain, in the Source form of any Derivative Works 117 | that You distribute, all copyright, patent, trademark, and 118 | attribution notices from the Source form of the Work, 119 | excluding those notices that do not pertain to any part of 120 | the Derivative Works; and 121 | 122 | (d) If the Work includes a "NOTICE" text file as part of its 123 | distribution, then any Derivative Works that You distribute must 124 | include a readable copy of the attribution notices contained 125 | within such NOTICE file, excluding those notices that do not 126 | pertain to any part of the Derivative Works, in at least one 127 | of the following places: within a NOTICE text file distributed 128 | as part of the Derivative Works; within the Source form or 129 | documentation, if provided along with the Derivative Works; or, 130 | within a display generated by the Derivative Works, if and 131 | wherever such third-party notices normally appear. The contents 132 | of the NOTICE file are for informational purposes only and 133 | do not modify the License. You may add Your own attribution 134 | notices within Derivative Works that You distribute, alongside 135 | or as an addendum to the NOTICE text from the Work, provided 136 | that such additional attribution notices cannot be construed 137 | as modifying the License. 138 | 139 | You may add Your own copyright statement to Your modifications and 140 | may provide additional or different license terms and conditions 141 | for use, reproduction, or distribution of Your modifications, or 142 | for any such Derivative Works as a whole, provided Your use, 143 | reproduction, and distribution of the Work otherwise complies with 144 | the conditions stated in this License. 145 | 146 | 5. Submission of Contributions. Unless You explicitly state otherwise, 147 | any Contribution intentionally submitted for inclusion in the Work 148 | by You to the Licensor shall be under the terms and conditions of 149 | this License, without any additional terms or conditions. 150 | Notwithstanding the above, nothing herein shall supersede or modify 151 | the terms of any separate license agreement you may have executed 152 | with Licensor regarding such Contributions. 153 | 154 | 6. Trademarks. This License does not grant permission to use the trade 155 | names, trademarks, service marks, or product names of the Licensor, 156 | except as required for reasonable and customary use in describing the 157 | origin of the Work and reproducing the content of the NOTICE file. 158 | 159 | 7. Disclaimer of Warranty. Unless required by applicable law or 160 | agreed to in writing, Licensor provides the Work (and each 161 | Contributor provides its Contributions) on an "AS IS" BASIS, 162 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 163 | implied, including, without limitation, any warranties or conditions 164 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 165 | PARTICULAR PURPOSE. You are solely responsible for determining the 166 | appropriateness of using or redistributing the Work and assume any 167 | risks associated with Your exercise of permissions under this License. 168 | 169 | 8. Limitation of Liability. In no event and under no legal theory, 170 | whether in tort (including negligence), contract, or otherwise, 171 | unless required by applicable law (such as deliberate and grossly 172 | negligent acts) or agreed to in writing, shall any Contributor be 173 | liable to You for damages, including any direct, indirect, special, 174 | incidental, or consequential damages of any character arising as a 175 | result of this License or out of the use or inability to use the 176 | Work (including but not limited to damages for loss of goodwill, 177 | work stoppage, computer failure or malfunction, or any and all 178 | other commercial damages or losses), even if such Contributor 179 | has been advised of the possibility of such damages. 180 | 181 | 9. Accepting Warranty or Additional Liability. While redistributing 182 | the Work or Derivative Works thereof, You may choose to offer, 183 | and charge a fee for, acceptance of support, warranty, indemnity, 184 | or other liability obligations and/or rights consistent with this 185 | License. However, in accepting such obligations, You may act only 186 | on Your own behalf and on Your sole responsibility, not on behalf 187 | of any other Contributor, and only if You agree to indemnify, 188 | defend, and hold each Contributor harmless for any liability 189 | incurred by, or claims asserted against, such Contributor by reason 190 | of your accepting any such warranty or additional liability. 191 | 192 | END OF TERMS AND CONDITIONS 193 | -------------------------------------------------------------------------------- /internal/provider/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package provider 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/grafana/dskit/crypto/tls" 12 | mimirtool "github.com/grafana/mimir/pkg/mimirtool/client" 13 | mimirVersion "github.com/grafana/mimir/pkg/util/version" 14 | "github.com/hashicorp/terraform-plugin-framework/datasource" 15 | "github.com/hashicorp/terraform-plugin-framework/provider" 16 | "github.com/hashicorp/terraform-plugin-framework/provider/schema" 17 | "github.com/hashicorp/terraform-plugin-framework/resource" 18 | "github.com/hashicorp/terraform-plugin-framework/types" 19 | "github.com/hashicorp/terraform-plugin-log/tflog" 20 | ) 21 | 22 | // Ensure MimirtoolProvider satisfies various provider interfaces. 23 | var _ provider.Provider = &MimirtoolProvider{} 24 | 25 | // MimirtoolProvider defines the provider implementation. 26 | type MimirtoolProvider struct { 27 | // version is set to the provider version on release, "dev" when the 28 | // provider is built and ran locally, and "test" when running acceptance 29 | // testing. 30 | version string 31 | } 32 | 33 | // MimirClientConfig holds the configuration for the Mimir client 34 | type MimirClientConfig struct { 35 | Address string 36 | TenantID string 37 | APIUser string 38 | APIKey string 39 | AuthToken string 40 | TLSKeyPath string 41 | TLSCertPath string 42 | TLSCAPath string 43 | InsecureSkipVerify bool 44 | PrometheusHTTPPrefix string 45 | AlertmanagerHTTPPrefix string 46 | } 47 | 48 | // MimirtoolProviderModel describes the provider data model. 49 | type MimirtoolProviderModel struct { 50 | Address types.String `tfsdk:"address"` 51 | TenantID types.String `tfsdk:"tenant_id"` 52 | APIUser types.String `tfsdk:"api_user"` 53 | APIKey types.String `tfsdk:"api_key"` 54 | AuthToken types.String `tfsdk:"auth_token"` 55 | TLSKeyPath types.String `tfsdk:"tls_key_path"` 56 | TLSCertPath types.String `tfsdk:"tls_cert_path"` 57 | TLSCAPath types.String `tfsdk:"tls_ca_path"` 58 | InsecureSkipVerify types.Bool `tfsdk:"insecure_skip_verify"` 59 | PrometheusHTTPPrefix types.String `tfsdk:"prometheus_http_prefix"` 60 | AlertmanagerHTTPPrefix types.String `tfsdk:"alertmanager_http_prefix"` 61 | } 62 | 63 | func (p *MimirtoolProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { 64 | resp.TypeName = "mimirtool" 65 | resp.Version = p.version 66 | } 67 | 68 | func (p *MimirtoolProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { 69 | resp.Schema = schema.Schema{ 70 | MarkdownDescription: "The Mimirtool provider allows you to manage Grafana Mimir resources using Terraform.", 71 | Attributes: map[string]schema.Attribute{ 72 | "address": schema.StringAttribute{ 73 | MarkdownDescription: "Address to use when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_ADDRESS` or `MIMIR_ADDRESS` environment variable.", 74 | Optional: true, 75 | }, 76 | "tenant_id": schema.StringAttribute{ 77 | MarkdownDescription: "Tenant ID to use when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_TENANT_ID` or `MIMIR_TENANT_ID` environment variable.", 78 | Optional: true, 79 | }, 80 | "api_user": schema.StringAttribute{ 81 | MarkdownDescription: "API user to use when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_API_USER` or `MIMIR_API_USER` environment variable.", 82 | Optional: true, 83 | }, 84 | "api_key": schema.StringAttribute{ 85 | MarkdownDescription: "API key to use when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_API_KEY` or `MIMIR_API_KEY` environment variable.", 86 | Optional: true, 87 | Sensitive: true, 88 | }, 89 | "auth_token": schema.StringAttribute{ 90 | MarkdownDescription: "Authentication token for bearer token or JWT auth when contacting Grafana Mimir. May alternatively be set via the `MIMIRTOOL_AUTH_TOKEN` or `MIMIR_AUTH_TOKEN` environment variable.", 91 | Optional: true, 92 | Sensitive: true, 93 | }, 94 | "tls_key_path": schema.StringAttribute{ 95 | MarkdownDescription: "Client TLS key file to use to authenticate to the MIMIR server. May alternatively be set via the `MIMIRTOOL_TLS_KEY_PATH` or `MIMIR_TLS_KEY_PATH` environment variable.", 96 | Optional: true, 97 | }, 98 | "tls_cert_path": schema.StringAttribute{ 99 | MarkdownDescription: "Client TLS certificate file to use to authenticate to the MIMIR server. May alternatively be set via the `MIMIRTOOL_TLS_CERT_PATH` or `MIMIR_TLS_CERT_PATH` environment variable.", 100 | Optional: true, 101 | }, 102 | "tls_ca_path": schema.StringAttribute{ 103 | MarkdownDescription: "Certificate CA bundle to use to verify the MIMIR server's certificate. May alternatively be set via the `MIMIRTOOL_TLS_CA_PATH` or `MIMIR_TLS_CA_PATH` environment variable.", 104 | Optional: true, 105 | }, 106 | "insecure_skip_verify": schema.BoolAttribute{ 107 | MarkdownDescription: "Skip TLS certificate verification. May alternatively be set via the `MIMIRTOOL_INSECURE_SKIP_VERIFY` or `MIMIR_INSECURE_SKIP_VERIFY` environment variable.", 108 | Optional: true, 109 | }, 110 | "prometheus_http_prefix": schema.StringAttribute{ 111 | MarkdownDescription: "Path prefix to use for rules. May alternatively be set via the `MIMIRTOOL_PROMETHEUS_HTTP_PREFIX` or `MIMIR_PROMETHEUS_HTTP_PREFIX` environment variable.", 112 | Optional: true, 113 | }, 114 | "alertmanager_http_prefix": schema.StringAttribute{ 115 | MarkdownDescription: "Path prefix to use for alertmanager. May alternatively be set via the `MIMIRTOOL_ALERTMANAGER_HTTP_PREFIX` or `MIMIR_ALERTMANAGER_HTTP_PREFIX` environment variable.", 116 | Optional: true, 117 | }, 118 | }, 119 | } 120 | } 121 | 122 | func (p *MimirtoolProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { 123 | var data MimirtoolProviderModel 124 | 125 | resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) 126 | 127 | if resp.Diagnostics.HasError() { 128 | return 129 | } 130 | 131 | // Get values from config or environment variables 132 | clientConfig := MimirClientConfig{ 133 | Address: getStringValue(data.Address, "MIMIRTOOL_ADDRESS", "MIMIR_ADDRESS", ""), 134 | TenantID: getStringValue(data.TenantID, "MIMIRTOOL_TENANT_ID", "MIMIR_TENANT_ID", ""), 135 | APIUser: getStringValue(data.APIUser, "MIMIRTOOL_API_USER", "MIMIR_API_USER", ""), 136 | APIKey: getStringValue(data.APIKey, "MIMIRTOOL_API_KEY", "MIMIR_API_KEY", ""), 137 | AuthToken: getStringValue(data.AuthToken, "MIMIRTOOL_AUTH_TOKEN", "MIMIR_AUTH_TOKEN", ""), 138 | TLSKeyPath: getStringValue(data.TLSKeyPath, "MIMIRTOOL_TLS_KEY_PATH", "MIMIR_TLS_KEY_PATH", ""), 139 | TLSCertPath: getStringValue(data.TLSCertPath, "MIMIRTOOL_TLS_CERT_PATH", "MIMIR_TLS_CERT_PATH", ""), 140 | TLSCAPath: getStringValue(data.TLSCAPath, "MIMIRTOOL_TLS_CA_PATH", "MIMIR_TLS_CA_PATH", ""), 141 | InsecureSkipVerify: getBoolValue(data.InsecureSkipVerify, "MIMIRTOOL_INSECURE_SKIP_VERIFY", "MIMIR_INSECURE_SKIP_VERIFY", false), 142 | PrometheusHTTPPrefix: getStringValue(data.PrometheusHTTPPrefix, "MIMIRTOOL_PROMETHEUS_HTTP_PREFIX", "MIMIR_PROMETHEUS_HTTP_PREFIX", "/prometheus"), 143 | AlertmanagerHTTPPrefix: getStringValue(data.AlertmanagerHTTPPrefix, "MIMIRTOOL_ALERTMANAGER_HTTP_PREFIX", "MIMIR_ALERTMANAGER_HTTP_PREFIX", "/alertmanager"), 144 | } 145 | 146 | tflog.Info(ctx, "Configured Mimirtool provider", map[string]interface{}{ 147 | "address": clientConfig.Address, 148 | "tenant_id": clientConfig.TenantID, 149 | "prometheus_http_prefix": clientConfig.PrometheusHTTPPrefix, 150 | "alertmanager_http_prefix": clientConfig.AlertmanagerHTTPPrefix, 151 | }) 152 | 153 | // Validate required fields 154 | if clientConfig.Address == "" { 155 | resp.Diagnostics.AddError( 156 | "Missing Required Configuration", 157 | "The provider cannot create the Mimir client as there is a missing or empty value for the \"address\" configuration. "+ 158 | "Set the address value in the configuration or use the MIMIRTOOL_ADDRESS or MIMIR_ADDRESS environment variable. "+ 159 | "If either is already set, ensure the value is not empty.", 160 | ) 161 | return 162 | } 163 | 164 | // Create a new Mimirtool client using the configuration values 165 | var err error 166 | c := &myClient{} 167 | c.cli, err = getDefaultMimirClient(clientConfig, p.version) 168 | if err != nil { 169 | resp.Diagnostics.AddError( 170 | "Unable to Create Mimirtool API Client", 171 | "An unexpected error occurred when creating the Mimirtool API client. "+ 172 | "If the error is not clear, please contact the provider developers.\n\n"+ 173 | "Mimirtool Client Error: "+err.Error(), 174 | ) 175 | return 176 | } 177 | 178 | resp.DataSourceData = c.cli 179 | resp.ResourceData = c.cli 180 | } 181 | 182 | func getDefaultMimirClient(cfg MimirClientConfig, version string) (mimirClientInterface, error) { 183 | mimirVersion.Version = fmt.Sprintf("terraform-provider-mimirtool-%s", version) 184 | return mimirtool.New(mimirtool.Config{ 185 | AuthToken: cfg.AuthToken, 186 | User: cfg.APIUser, 187 | Key: cfg.APIKey, 188 | Address: cfg.Address, 189 | ID: cfg.TenantID, 190 | TLS: tls.ClientConfig{ 191 | CAPath: cfg.TLSCAPath, 192 | CertPath: cfg.TLSCertPath, 193 | KeyPath: cfg.TLSKeyPath, 194 | InsecureSkipVerify: cfg.InsecureSkipVerify, 195 | }, 196 | }) 197 | } 198 | 199 | func (p *MimirtoolProvider) Resources(_ context.Context) []func() resource.Resource { 200 | return []func() resource.Resource{ 201 | NewRulerNamespaceResource, 202 | NewAlertmanagerResource, 203 | } 204 | } 205 | 206 | func (p *MimirtoolProvider) DataSources(_ context.Context) []func() datasource.DataSource { 207 | return []func() datasource.DataSource{} 208 | } 209 | 210 | func New(version string) func() provider.Provider { 211 | return func() provider.Provider { 212 | return &MimirtoolProvider{ 213 | version: version, 214 | } 215 | } 216 | } 217 | 218 | // Helper functions to get values from config or environment variables 219 | func getStringValue(configValue types.String, envVar1, envVar2, defaultValue string) string { 220 | if !configValue.IsNull() && !configValue.IsUnknown() { 221 | return configValue.ValueString() 222 | } 223 | 224 | // Try first environment variable 225 | if value := os.Getenv(envVar1); value != "" { 226 | return value 227 | } 228 | 229 | // Try second environment variable 230 | if value := os.Getenv(envVar2); value != "" { 231 | return value 232 | } 233 | 234 | return defaultValue 235 | } 236 | 237 | func getBoolValue(configValue types.Bool, envVar1, envVar2 string, defaultValue bool) bool { 238 | if !configValue.IsNull() && !configValue.IsUnknown() { 239 | return configValue.ValueBool() 240 | } 241 | 242 | // Try first environment variable 243 | if value := os.Getenv(envVar1); value != "" { 244 | return value == "true" || value == "1" 245 | } 246 | 247 | // Try second environment variable 248 | if value := os.Getenv(envVar2); value != "" { 249 | return value == "true" || value == "1" 250 | } 251 | 252 | return defaultValue 253 | } 254 | -------------------------------------------------------------------------------- /internal/provider/ruler_namespace_resource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package provider 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "regexp" 10 | "testing" 11 | 12 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" 13 | "github.com/hashicorp/terraform-plugin-testing/knownvalue" 14 | "github.com/hashicorp/terraform-plugin-testing/statecheck" 15 | "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" 16 | "gopkg.in/yaml.v3" 17 | ) 18 | 19 | // Structs for semantic YAML comparison 20 | // These should match the structure of your rule YAML 21 | // Add fields as needed for your use case 22 | 23 | type Rule struct { 24 | Record string `yaml:"record,omitempty"` 25 | Expr string `yaml:"expr,omitempty"` 26 | } 27 | type RuleGroup struct { 28 | Name string `yaml:"name"` 29 | Rules []Rule `yaml:"rules"` 30 | } 31 | type RuleNamespace struct { 32 | Groups []RuleGroup `yaml:"groups"` 33 | } 34 | 35 | // Semantic YAML matcher for knownvalue.StringFunc 36 | func SemanticYAMLMatcher(expected string) func(string) error { 37 | return func(actual string) error { 38 | var expectedObj, actualObj RuleNamespace 39 | if err := yaml.Unmarshal([]byte(expected), &expectedObj); err != nil { 40 | return fmt.Errorf("Failed to parse expected YAML: %s", err) 41 | } 42 | if err := yaml.Unmarshal([]byte(actual), &actualObj); err != nil { 43 | return fmt.Errorf("Failed to parse actual YAML: %s", err) 44 | } 45 | if !reflect.DeepEqual(expectedObj, actualObj) { 46 | return fmt.Errorf("YAML semantic mismatch\nExpected: %#v\nActual: %#v", expectedObj, actualObj) 47 | } 48 | return nil 49 | } 50 | } 51 | 52 | // SemanticYAMLStateCheck returns a statecheck.StateCheck for semantic YAML comparison on a given resource and attribute. 53 | func SemanticYAMLStateCheck(resourceName, attr, expected string) statecheck.StateCheck { 54 | return statecheck.ExpectKnownValue( 55 | resourceName, 56 | tfjsonpath.New(attr), 57 | knownvalue.StringFunc(SemanticYAMLMatcher(expected)), 58 | ) 59 | } 60 | 61 | func TestAccResourceNamespace(t *testing.T) { 62 | resource.Test(t, resource.TestCase{ 63 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 64 | Steps: []resource.TestStep{ 65 | { 66 | Config: testAccResourceNamespace, 67 | Check: resource.ComposeAggregateTestCheckFunc( 68 | resource.TestCheckResourceAttr("mimirtool_ruler_namespace.demo", "namespace", "demo"), 69 | ), 70 | ConfigStateChecks: []statecheck.StateCheck{ 71 | SemanticYAMLStateCheck("mimirtool_ruler_namespace.demo", "remote_config_yaml", testAccResourceNamespaceYaml), 72 | }, 73 | }, 74 | { 75 | Config: testAccResourceNamespaceAfterUpdate, 76 | ConfigStateChecks: []statecheck.StateCheck{ 77 | statecheck.ExpectKnownValue( 78 | "mimirtool_ruler_namespace.demo", 79 | tfjsonpath.New("namespace"), 80 | knownvalue.StringExact("demo"), 81 | ), 82 | SemanticYAMLStateCheck("mimirtool_ruler_namespace.demo", "remote_config_yaml", testAccResourceNamespaceYamlAfterUpdate), 83 | }, 84 | }, 85 | { 86 | ResourceName: "mimirtool_ruler_namespace.demo", 87 | ImportStateId: "demo", 88 | ImportState: true, 89 | ImportStateVerify: true, 90 | // These fields can't be retrieved from mimir ruler 91 | ImportStateVerifyIgnore: []string{"recording_rule_check", "strict_recording_rule_check", "config_yaml"}, 92 | }, 93 | }, 94 | }) 95 | } 96 | 97 | func TestAccResourceNamespaceRename(t *testing.T) { 98 | resource.Test(t, resource.TestCase{ 99 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 100 | Steps: []resource.TestStep{ 101 | { 102 | Config: testAccResourceNamespaceRename, 103 | ConfigStateChecks: []statecheck.StateCheck{ 104 | statecheck.ExpectKnownValue( 105 | "mimirtool_ruler_namespace.alerts", 106 | tfjsonpath.New("namespace"), 107 | knownvalue.StringExact("alerts_infra"), 108 | ), 109 | }, 110 | }, 111 | { 112 | Config: testAccResourceNamespaceRenameAfterUpdate, 113 | ConfigStateChecks: []statecheck.StateCheck{ 114 | statecheck.ExpectKnownValue( 115 | "mimirtool_ruler_namespace.alerts", 116 | tfjsonpath.New("namespace"), 117 | knownvalue.StringExact("infra"), 118 | ), 119 | }, 120 | }, 121 | }, 122 | }) 123 | } 124 | 125 | func TestAccResourceNamespaceDiffSuppress(t *testing.T) { 126 | resource.Test(t, resource.TestCase{ 127 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 128 | Steps: []resource.TestStep{ 129 | { 130 | Config: testAccResourceNamespaceWhitespaceDiff, 131 | ConfigStateChecks: []statecheck.StateCheck{ 132 | statecheck.ExpectKnownValue( 133 | "mimirtool_ruler_namespace.demo", 134 | tfjsonpath.New("namespace"), 135 | knownvalue.StringExact("demo"), 136 | ), 137 | SemanticYAMLStateCheck("mimirtool_ruler_namespace.demo", "remote_config_yaml", testAccResourceNamespaceYamlWhitespace), 138 | }, 139 | }, 140 | }, 141 | }) 142 | } 143 | 144 | func TestAccResourceNamespaceQuoting(t *testing.T) { 145 | resource.Test(t, resource.TestCase{ 146 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 147 | Steps: []resource.TestStep{ 148 | { 149 | Config: testAccResourceNamespaceQuoting, 150 | ConfigStateChecks: []statecheck.StateCheck{ 151 | statecheck.ExpectKnownValue( 152 | "mimirtool_ruler_namespace.demo", 153 | tfjsonpath.New("namespace"), 154 | knownvalue.StringExact("demo"), 155 | ), 156 | SemanticYAMLStateCheck("mimirtool_ruler_namespace.demo", "remote_config_yaml", testAccResourceNamespaceQuotingExpected), 157 | }, 158 | }, 159 | }, 160 | }) 161 | } 162 | 163 | func TestAccResourceNamespaceCheckRules(t *testing.T) { 164 | resource.Test(t, resource.TestCase{ 165 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 166 | Steps: []resource.TestStep{ 167 | { 168 | Config: testAccResourceNamespaceFailsCheck, 169 | ExpectError: regexp.MustCompile("namespace contains 1 rules that don't match the requirements"), 170 | }, 171 | }, 172 | }) 173 | } 174 | 175 | func TestAccResourceNamespaceNoCheck(t *testing.T) { 176 | resource.Test(t, resource.TestCase{ 177 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 178 | Steps: []resource.TestStep{ 179 | { 180 | Config: testAccResourceNamespaceNoCheck, 181 | ConfigStateChecks: []statecheck.StateCheck{ 182 | statecheck.ExpectKnownValue( 183 | "mimirtool_ruler_namespace.demo", 184 | tfjsonpath.New("namespace"), 185 | knownvalue.StringExact("demo"), 186 | ), 187 | statecheck.ExpectKnownValue( 188 | "mimirtool_ruler_namespace.demo", 189 | tfjsonpath.New("recording_rule_check"), 190 | knownvalue.Bool(false), 191 | ), 192 | SemanticYAMLStateCheck("mimirtool_ruler_namespace.demo", "remote_config_yaml", testAccResourceNamespaceNoCheckExpected), 193 | }, 194 | }, 195 | }, 196 | }) 197 | } 198 | 199 | func TestAccResourceNamespaceParseRules(t *testing.T) { 200 | resource.Test(t, resource.TestCase{ 201 | ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, 202 | Steps: []resource.TestStep{ 203 | { 204 | Config: testAccResourceNamespaceParseError, 205 | ExpectError: regexp.MustCompile("field expression not found"), 206 | }, 207 | }, 208 | }) 209 | } 210 | 211 | const testAccResourceNamespaceRename = ` 212 | provider "mimirtool" { 213 | address = "http://localhost:8080" 214 | } 215 | 216 | resource "mimirtool_ruler_namespace" "alerts" { 217 | namespace = "alerts_infra" 218 | config_yaml = file("testdata/rules.yaml") 219 | } 220 | ` 221 | 222 | const testAccResourceNamespaceRenameAfterUpdate = ` 223 | provider "mimirtool" { 224 | address = "http://localhost:8080" 225 | } 226 | 227 | resource "mimirtool_ruler_namespace" "alerts" { 228 | namespace = "infra" 229 | config_yaml = file("testdata/rules.yaml") 230 | } 231 | ` 232 | 233 | const testAccResourceNamespace = ` 234 | provider "mimirtool" { 235 | address = "http://localhost:8080" 236 | } 237 | 238 | resource "mimirtool_ruler_namespace" "demo" { 239 | namespace = "demo" 240 | config_yaml = file("testdata/rules.yaml") 241 | } 242 | ` 243 | const testAccResourceNamespaceYaml = `groups: 244 | - name: mimir_api_1 245 | rules: 246 | - record: cluster_job:cortex_request_duration_seconds:99quantile 247 | expr: histogram_quantile(0.99, sum by (le, cluster, job) (rate(cortex_request_duration_seconds_bucket[1m]))) 248 | - record: cluster_job:cortex_request_duration_seconds:50quantile 249 | expr: histogram_quantile(0.5, sum by (le, cluster, job) (rate(cortex_request_duration_seconds_bucket[1m])))` 250 | 251 | const testAccResourceNamespaceYamlWhitespace = `groups: 252 | - name: mimir_api_1 253 | rules: 254 | - record: cluster_job:cortex_request_duration_seconds:99quantile 255 | expr: |- 256 | histogram_quantile(0.99, sum by (le, cluster, job) (rate(cortex_request_duration_seconds_bucket[1m]))) 257 | - record: cluster_job:cortex_request_duration_seconds:50quantile 258 | expr: histogram_quantile(0.5, sum by (le, cluster, job) (rate(cortex_request_duration_seconds_bucket[1m]))) 259 | ` 260 | 261 | const testAccResourceNamespaceAfterUpdate = ` 262 | provider "mimirtool" { 263 | address = "http://localhost:8080" 264 | } 265 | 266 | resource "mimirtool_ruler_namespace" "demo" { 267 | namespace = "demo" 268 | config_yaml = file("testdata/rules2.yaml") 269 | } 270 | ` 271 | 272 | const testAccResourceNamespaceYamlAfterUpdate = `groups: 273 | - name: mimir_api_1 274 | rules: 275 | - record: cluster_job:cortex_request_duration_seconds:99quantile 276 | expr: histogram_quantile(0.99, sum by (le, cluster, job) (rate(cortex_request_duration_seconds_bucket[1m]))) 277 | - record: cluster_job:cortex_request_duration_seconds:50quantile 278 | expr: histogram_quantile(0.5, sum by (le, cluster, job) (rate(cortex_request_duration_seconds_bucket[1m]))) 279 | - name: mimir_api_2 280 | rules: 281 | - record: cluster_job_route:cortex_request_duration_seconds:99quantile 282 | expr: histogram_quantile(0.99, sum by (le, cluster, job, route) (rate(cortex_request_duration_seconds_bucket[1m]))) 283 | ` 284 | 285 | const testAccResourceNamespaceWhitespaceDiff = `provider "mimirtool" { 286 | address = "http://localhost:8080" 287 | } 288 | 289 | resource "mimirtool_ruler_namespace" "demo" { 290 | namespace = "demo" 291 | config_yaml = file("testdata/rules2_spacing.yaml") 292 | } 293 | ` 294 | const testAccResourceNamespaceFailsCheck = ` 295 | provider "mimirtool" { 296 | address = "http://localhost:8080" 297 | } 298 | 299 | resource "mimirtool_ruler_namespace" "demo" { 300 | namespace = "demo" 301 | config_yaml = file("testdata/rules-fails-check.yaml") 302 | } 303 | ` 304 | 305 | const testAccResourceNamespaceNoCheck = ` 306 | provider "mimirtool" { 307 | address = "http://localhost:8080" 308 | } 309 | 310 | resource "mimirtool_ruler_namespace" "demo" { 311 | namespace = "demo" 312 | config_yaml = file("testdata/rules-fails-check.yaml") 313 | recording_rule_check = false 314 | } 315 | ` 316 | 317 | const testAccResourceNamespaceNoCheckExpected = `groups: 318 | - name: mimir_api_1 319 | rules: 320 | - record: cluster_job_cortex_request_duration_seconds_99quantile 321 | expr: histogram_quantile(0.99, sum by (le, cluster, job) (rate(cortex_request_duration_seconds_bucket[1m]))) 322 | ` 323 | const testAccResourceNamespaceParseError = ` 324 | provider "mimirtool" { 325 | address = "http://localhost:8080" 326 | } 327 | 328 | resource "mimirtool_ruler_namespace" "demo" { 329 | namespace = "demo" 330 | config_yaml = file("testdata/rules-parse-error.yaml") 331 | } 332 | ` 333 | const testAccResourceNamespaceQuoting = ` 334 | provider "mimirtool" { 335 | address = "http://localhost:8080" 336 | } 337 | 338 | resource "mimirtool_ruler_namespace" "demo" { 339 | namespace = "demo" 340 | config_yaml = file("testdata/rules-quoting.yaml") 341 | } 342 | ` 343 | const testAccResourceNamespaceQuotingExpected = `groups: 344 | - name: NodeExporter 345 | rules: 346 | - alert: HostDiskWillFillIn24Hours 347 | expr: (node_filesystem_avail_bytes * 100) / node_filesystem_size_bytes < 10 and on (instance, device, mountpoint) predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs"}[1h], 24 * 3600) < 0 and on (instance, device, mountpoint) node_filesystem_readonly == 0 348 | for: 2m 349 | labels: 350 | severity: warning 351 | annotations: 352 | description: |- 353 | Filesystem is predicted to run out of space within the next 24 hours at current write rate 354 | VALUE = {{ $value }} 355 | LABELS = {{ $labels }} 356 | summary: Host disk will fill in 24 hours (instance {{ $labels.instance }}) 357 | - alert: HostHighCpuLoad 358 | expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[2m])) * 100) > 80 359 | labels: 360 | severity: warning 361 | annotations: 362 | description: |- 363 | CPU load is > 80% 364 | VALUE = {{ $value }} 365 | LABELS = {{ $labels }} 366 | summary: Host high CPU load (instance {{ $labels.instance }}) 367 | ` 368 | -------------------------------------------------------------------------------- /internal/provider/ruler_namespace_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/grafana/mimir/pkg/mimirtool/client" 9 | "github.com/grafana/mimir/pkg/mimirtool/rules" 10 | "github.com/grafana/mimir/pkg/mimirtool/rules/rwrulefmt" 11 | "github.com/hashicorp/terraform-plugin-framework/diag" 12 | "github.com/hashicorp/terraform-plugin-framework/resource" 13 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 14 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" 15 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 16 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 17 | "github.com/hashicorp/terraform-plugin-framework/schema/validator" 18 | "github.com/hashicorp/terraform-plugin-framework/types" 19 | "github.com/hashicorp/terraform-plugin-log/tflog" 20 | "gopkg.in/yaml.v3" 21 | ) 22 | 23 | // Ensure provider defined types fully satisfy framework interfaces. 24 | var ( 25 | _ resource.Resource = &RulerNamespaceResource{} 26 | _ resource.ResourceWithImportState = &RulerNamespaceResource{} 27 | ) 28 | 29 | func NewRulerNamespaceResource() resource.Resource { 30 | return &RulerNamespaceResource{} 31 | } 32 | 33 | // RulerNamespaceResource defines the resource implementation. 34 | type RulerNamespaceResource struct { 35 | client *client.MimirClient 36 | } 37 | 38 | // RulerNamespaceResourceModel describes the resource data model. 39 | type RulerNamespaceResourceModel struct { 40 | ID types.String `tfsdk:"id"` 41 | Namespace types.String `tfsdk:"namespace"` 42 | ConfigYAML types.String `tfsdk:"config_yaml"` 43 | RemoteConfigYAML types.String `tfsdk:"remote_config_yaml"` 44 | StrictRecordingRuleCheck types.Bool `tfsdk:"strict_recording_rule_check"` 45 | RecordingRuleCheck types.Bool `tfsdk:"recording_rule_check"` 46 | } 47 | 48 | func (r *RulerNamespaceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 49 | resp.TypeName = req.ProviderTypeName + "_ruler_namespace" 50 | } 51 | 52 | func (r *RulerNamespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { 53 | tflog.Debug(ctx, "SCHEMA - init") 54 | resp.Schema = schema.Schema{ 55 | // This description is used by the documentation generator and the language server. 56 | MarkdownDescription: "[Official documentation](https://grafana.com/docs/mimir/latest/references/http-api/#ruler)", 57 | 58 | Attributes: map[string]schema.Attribute{ 59 | "id": schema.StringAttribute{ 60 | Computed: true, 61 | Description: "hash", 62 | MarkdownDescription: "hash", 63 | PlanModifiers: []planmodifier.String{ 64 | stringplanmodifier.UseStateForUnknown(), 65 | }, 66 | }, 67 | "namespace": schema.StringAttribute{ 68 | MarkdownDescription: "The name of the namespace to create in Grafana Mimir.", 69 | Required: true, 70 | // Ensures that Terraform destroys and recreates the resource when the namespace changes 71 | // as the hash(namespace) will change 72 | PlanModifiers: []planmodifier.String{ 73 | stringplanmodifier.RequiresReplace(), 74 | }, 75 | }, 76 | "config_yaml": schema.StringAttribute{ 77 | MarkdownDescription: "User supplied namespace's groups rules definition to create in Grafana Mimir as YAML.", 78 | Required: true, 79 | Validators: []validator.String{ 80 | namespaceYAMLValidator{}, 81 | }, 82 | }, 83 | "remote_config_yaml": schema.StringAttribute{ 84 | MarkdownDescription: "The namespace's groups rules definition stored in Grafana Mimir as YAML.", 85 | Optional: true, 86 | Computed: true, 87 | }, 88 | "strict_recording_rule_check": schema.BoolAttribute{ 89 | MarkdownDescription: "Fails rules checks that do not match best practices exactly. See: https://prometheus.io/docs/practices/rules/", 90 | Optional: true, 91 | Default: booldefault.StaticBool(false), 92 | Computed: true, // https://discuss.hashicorp.com/t/why-default-attribute-must-also-be-computed/70107/2 93 | }, 94 | "recording_rule_check": schema.BoolAttribute{ 95 | MarkdownDescription: "Controls whether to run recording rule checks entirely.", 96 | Optional: true, 97 | Default: booldefault.StaticBool(true), 98 | Computed: true, // see above 99 | }, 100 | }, 101 | } 102 | } 103 | 104 | func (r *RulerNamespaceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 105 | tflog.Debug(ctx, "CONFIGURE - init") 106 | // Prevent panic if the provider has not been configured. 107 | if req.ProviderData == nil { 108 | return 109 | } 110 | 111 | tflog.Debug(ctx, "CONFIGURE - debug", map[string]interface{}{ 112 | "provider_data": req.ProviderData, 113 | }) 114 | 115 | client, ok := req.ProviderData.(*client.MimirClient) 116 | 117 | if !ok { 118 | resp.Diagnostics.AddError( 119 | "Unexpected Resource Configure Type", 120 | fmt.Sprintf("Expected *client.MimirClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), 121 | ) 122 | 123 | return 124 | } 125 | 126 | r.client = client 127 | } 128 | 129 | func (r *RulerNamespaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 130 | tflog.Debug(ctx, "CREATE - init") 131 | var plan RulerNamespaceResourceModel 132 | 133 | // Read Terraform plan data into the model 134 | resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) 135 | 136 | if resp.Diagnostics.HasError() { 137 | return 138 | } 139 | 140 | // Extract values from the plan 141 | namespace := plan.Namespace.ValueString() 142 | ruleGroup := plan.ConfigYAML.ValueString() 143 | strictRecordingRuleCheck := plan.StrictRecordingRuleCheck.ValueBool() 144 | recordingRuleCheck := plan.RecordingRuleCheck.ValueBool() 145 | 146 | tflog.Debug(ctx, "CREATE - values from plan", map[string]interface{}{ 147 | "namespace": namespace, 148 | "ruleGroup": ruleGroup, 149 | "strictRecordingRuleCheck": strictRecordingRuleCheck, 150 | "recordingRuleCheck": recordingRuleCheck, 151 | }) 152 | 153 | // Parse YAML 154 | ruleNamespace, err := getRuleNamespaceFromYAML(ctx, ruleGroup) 155 | if err != nil { 156 | resp.Diagnostics.AddError( 157 | "Failed to parse rule group YAML", 158 | err.Error(), 159 | ) 160 | return 161 | } 162 | 163 | if recordingRuleCheck { 164 | err = checkRecordingRules(ruleNamespace, strictRecordingRuleCheck) 165 | if err != nil { 166 | // TODO: add a more explicit error message 167 | resp.Diagnostics.AddError( 168 | "Failed to check recording rule group", 169 | err.Error(), 170 | ) 171 | return 172 | } 173 | } 174 | 175 | // Create rule groups in Mimir 176 | if err := createAllRuleGroups(ctx, r.client, namespace, ruleNamespace.Groups); err != nil { 177 | resp.Diagnostics.AddError( 178 | "Failed to create rule groups", 179 | err.Error(), 180 | ) 181 | return 182 | } 183 | 184 | // Set ID 185 | plan.ID = types.StringValue(hash(namespace)) 186 | 187 | // Always fetch canonical YAML from backend and store in state 188 | normalized, ok := fetchAndNormalizeRemoteConfigYAML(ctx, r.client, namespace, "CREATE", &resp.Diagnostics) 189 | if !ok { 190 | return 191 | } 192 | plan.RemoteConfigYAML = types.StringValue(normalized) 193 | 194 | // Save the state 195 | resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) 196 | } 197 | 198 | func (r *RulerNamespaceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 199 | tflog.Debug(ctx, "READ - init") 200 | var state RulerNamespaceResourceModel 201 | resp.Diagnostics.Append(req.State.Get(ctx, &state)...) 202 | if resp.Diagnostics.HasError() { 203 | return 204 | } 205 | 206 | namespace := state.Namespace.ValueString() 207 | 208 | // Use the same helper as Create/Update for fetching and normalizing YAML 209 | normalized, ok := fetchAndNormalizeRemoteConfigYAML(ctx, r.client, namespace, "READ", &resp.Diagnostics) 210 | if !ok { 211 | return 212 | } 213 | state.RemoteConfigYAML = types.StringValue(normalized) 214 | state.ID = types.StringValue(hash(namespace)) 215 | tflog.Debug(ctx, "Read: setting state.ID", map[string]interface{}{"id": state.ID.ValueString()}) 216 | resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) 217 | } 218 | 219 | func (r *RulerNamespaceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 220 | tflog.Debug(ctx, "DELETE - init") 221 | var state RulerNamespaceResourceModel 222 | 223 | // Read Terraform prior state data into the model 224 | resp.Diagnostics.Append(req.State.Get(ctx, &state)...) 225 | 226 | // Extract namespace from state 227 | namespace := state.Namespace.ValueString() 228 | 229 | tflog.Debug(ctx, "DELETE - values from state", map[string]interface{}{ 230 | "state_config_yaml": state.ConfigYAML.ValueString(), 231 | }) 232 | 233 | err := r.client.DeleteNamespace(ctx, namespace) 234 | 235 | if err != nil && !strings.Contains(err.Error(), "not found") { 236 | resp.Diagnostics.AddError( 237 | "Unable to Delete Resource", 238 | "An unexpected error occurred while attempting to delete the resource. "+ 239 | "Please retry the operation or report this issue to the provider developers.\n\n"+ 240 | "HTTP Error: "+err.Error(), 241 | ) 242 | 243 | return 244 | } 245 | 246 | // If the logic reaches here, it implicitly succeeded and will remove 247 | // the resource from state if there are no other errors. 248 | } 249 | 250 | func (r *RulerNamespaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { 251 | tflog.Debug(ctx, "IMPORT STATE - init") 252 | // The import ID is the namespace name 253 | namespace := req.ID 254 | 255 | // Create a state with the namespace set 256 | var state RulerNamespaceResourceModel 257 | state.Namespace = types.StringValue(namespace) 258 | state.ID = types.StringValue(hash(namespace)) 259 | 260 | // Fetch backend rules to update the state 261 | normalized, ok := fetchAndNormalizeRemoteConfigYAML(ctx, r.client, namespace, "IMPORT", &resp.Diagnostics) 262 | if !ok { 263 | return 264 | } 265 | state.RemoteConfigYAML = types.StringValue(normalized) 266 | // state.ConfigYAML = types.StringValue(normalized) // Set config_yaml to the same value for import 267 | 268 | // Set the state 269 | resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) 270 | } 271 | 272 | func getRuleNamespaceFromYAML(_ context.Context, configYAML string) (rules.RuleNamespace, error) { 273 | var ruleNamespace rules.RuleNamespace 274 | // We pass only one ruleGroup while ParseBytes return an array, we only need the first element 275 | ruleNamespaces, err := rules.ParseBytes([]byte(configYAML)) 276 | if err != nil { 277 | return ruleNamespace, fmt.Errorf("failed to parse namespace definition:\n%s", err) 278 | } 279 | 280 | if len(ruleNamespaces) > 1 { 281 | return ruleNamespace, fmt.Errorf("namespace definition contains more than one namespace which is not supported") 282 | } 283 | if len(ruleNamespaces) == 1 { 284 | return ruleNamespaces[0], nil 285 | } 286 | return ruleNamespace, fmt.Errorf("no namespace definition found") 287 | } 288 | 289 | func checkRecordingRules(ruleNamespace rules.RuleNamespace, strict bool) error { 290 | invalidRulesCount := ruleNamespace.CheckRecordingRules(strict) 291 | if invalidRulesCount > 0 { 292 | return fmt.Errorf("namespace contains %d rules that don't match the requirements", invalidRulesCount) 293 | } 294 | return nil 295 | } 296 | 297 | // Borrowed from https://github.com/grafana/terraform-provider-grafana/blob/main/internal/resources/grafana/resource_dashboard.go 298 | func normalizeNamespaceYAML(config any) (string, int, int, error) { 299 | configYAML := config.(string) 300 | var ruleNamespace rules.RuleNamespace 301 | 302 | err := yaml.Unmarshal([]byte(configYAML), &ruleNamespace) 303 | if err != nil { 304 | return "", 0, 0, fmt.Errorf("failed to unmarshal YAML config") 305 | } 306 | count, mod, _ := ruleNamespace.LintExpressions(rules.MimirBackend) 307 | 308 | namespaceBytes, _ := yaml.Marshal(ruleNamespace) 309 | return string(namespaceBytes), count, mod, err 310 | } 311 | 312 | func (r *RulerNamespaceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 313 | tflog.Debug(ctx, "UPDATE - init") 314 | var plan RulerNamespaceResourceModel 315 | var state RulerNamespaceResourceModel 316 | 317 | resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) 318 | resp.Diagnostics.Append(req.State.Get(ctx, &state)...) 319 | 320 | if resp.Diagnostics.HasError() { 321 | return 322 | } 323 | 324 | namespace := plan.Namespace.ValueString() 325 | ruleGroup := plan.ConfigYAML.ValueString() 326 | strictRecordingRuleCheck := plan.StrictRecordingRuleCheck.ValueBool() 327 | recordingRuleCheck := plan.RecordingRuleCheck.ValueBool() 328 | 329 | // Delete the current namespace to replace it 330 | err := r.client.DeleteNamespace(ctx, namespace) 331 | if err != nil { 332 | resp.Diagnostics.AddError( 333 | "Failed to delete existing namespace", 334 | err.Error(), 335 | ) 336 | return 337 | } 338 | 339 | ruleNamespace, err := getRuleNamespaceFromYAML(ctx, ruleGroup) 340 | if err != nil { 341 | resp.Diagnostics.AddError( 342 | "Failed to parse rule group YAML", 343 | err.Error(), 344 | ) 345 | return 346 | } 347 | 348 | if recordingRuleCheck { 349 | err = checkRecordingRules(ruleNamespace, strictRecordingRuleCheck) 350 | if err != nil { 351 | resp.Diagnostics.AddError( 352 | "Failed to check recording rule group", 353 | err.Error(), 354 | ) 355 | return 356 | } 357 | } 358 | 359 | // Create all rule groups for the namespace 360 | if err := createAllRuleGroups(ctx, r.client, namespace, ruleNamespace.Groups); err != nil { 361 | resp.Diagnostics.AddError( 362 | "Failed to create rule groups", 363 | err.Error(), 364 | ) 365 | return 366 | } 367 | 368 | // Set the ID 369 | plan.ID = types.StringValue(hash(namespace)) 370 | 371 | // Fetch backend rules 372 | normalized, ok := fetchAndNormalizeRemoteConfigYAML(ctx, r.client, namespace, "UPDATE", &resp.Diagnostics) 373 | if !ok { 374 | return 375 | } 376 | plan.RemoteConfigYAML = types.StringValue(normalized) 377 | 378 | resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) 379 | } 380 | 381 | // Create rule groups in Mimir 382 | func createAllRuleGroups(ctx context.Context, client *client.MimirClient, namespace string, groups []rwrulefmt.RuleGroup) error { 383 | for _, group := range groups { 384 | if err := client.CreateRuleGroup(ctx, namespace, group); err != nil { 385 | return err 386 | } 387 | } 388 | return nil 389 | } 390 | 391 | // Helper function for fetching and normalizing the remote config YAML 392 | func fetchAndNormalizeRemoteConfigYAML( 393 | ctx context.Context, 394 | client *client.MimirClient, 395 | namespace string, 396 | op string, 397 | diagnostics *diag.Diagnostics, 398 | ) (string, bool) { 399 | remoteNamespaceRuleGroup, err := client.ListRules(ctx, namespace) 400 | if err != nil { 401 | diagnostics.AddError( 402 | fmt.Sprintf("Error Reading Mimir RuleGroup after %s", op), 403 | fmt.Sprintf("Could not read Mimir rulegroup for namespace %q: %s", namespace, err.Error()), 404 | ) 405 | return "", false 406 | } 407 | 408 | tflog.Trace(ctx, op+": raw value for remoteNamespaceRuleGroup", map[string]interface{}{"remoteNamespaceRuleGroup": remoteNamespaceRuleGroup}) 409 | 410 | // Mimir top level key is the namespace name while in the YAML definition the top level key is groups 411 | // Let's rename the key to be able to have a nice difference 412 | // TODO: might not be needed anymore since we have introduced the remote_config_yaml attribute 413 | remoteNamespaceRuleGroup["groups"] = remoteNamespaceRuleGroup[namespace] 414 | delete(remoteNamespaceRuleGroup, namespace) 415 | 416 | tflog.Debug(ctx, op+": after removing the namespace key", map[string]interface{}{"remoteNamespaceRuleGroupAfterRemovingNamespaceKey": remoteNamespaceRuleGroup}) 417 | 418 | remoteConfigYAML, err := yaml.Marshal(remoteNamespaceRuleGroup) 419 | if err != nil { 420 | diagnostics.AddError( 421 | fmt.Sprintf("Error marshaling rule group YAML after %s", op), 422 | err.Error(), 423 | ) 424 | return "", false 425 | } 426 | tflog.Debug(ctx, op+": YAML to be set in state", map[string]interface{}{"remote_config_yaml": remoteConfigYAML}) 427 | normalized, count, mod, err := normalizeNamespaceYAML(string(remoteConfigYAML)) 428 | if err != nil { 429 | diagnostics.AddError( 430 | fmt.Sprintf("Error while normalizing namespace YAML after %s", op), 431 | err.Error(), 432 | ) 433 | return "", false 434 | } 435 | tflog.Debug(ctx, op+": results from normalizeNamespaceYAML", map[string]interface{}{"count": count, "mod": mod, "raw": normalized}) 436 | 437 | return normalized, true 438 | } 439 | --------------------------------------------------------------------------------