Add dashboard UID auto-generation and Gitea dev workflow

This commit is contained in:
Alexandr
2026-03-25 06:38:06 +03:00
commit 345c5786b3
11 changed files with 802 additions and 0 deletions

View File

@ -0,0 +1,91 @@
name: terraform-dev
on:
pull_request:
paths:
- "environments/dev/Seahorse/**"
- "environments/modules/**"
- ".gitea/workflows/terraform-dev.yml"
push:
branches:
- main
paths:
- "environments/dev/Seahorse/**"
- "environments/modules/**"
- ".gitea/workflows/terraform-dev.yml"
workflow_dispatch:
inputs:
run_apply:
description: "Run terraform apply (true/false)"
required: true
default: "false"
env:
TF_IN_AUTOMATION: "true"
TF_INPUT: "false"
TF_CLI_ARGS_init: "-backend=false"
WORKDIR: "environments/dev/Seahorse"
jobs:
validate:
runs-on: [ubuntu-latest]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Terraform version
run: terraform version
- name: Terraform fmt check
run: terraform fmt -check -recursive
- name: Terraform init (no backend)
working-directory: ${{ env.WORKDIR }}
env:
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: terraform init
- name: Terraform validate
working-directory: ${{ env.WORKDIR }}
env:
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: terraform validate
plan:
needs: validate
runs-on: [ubuntu-latest]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Terraform init (no backend)
working-directory: ${{ env.WORKDIR }}
env:
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: terraform init
- name: Terraform plan
working-directory: ${{ env.WORKDIR }}
env:
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: terraform plan -refresh=false -lock=false -out=tfplan
apply:
if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_apply == 'true'
needs: plan
runs-on: [ubuntu-latest]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Terraform init (no backend)
working-directory: ${{ env.WORKDIR }}
env:
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: terraform init
- name: Terraform apply (manual trigger)
working-directory: ${{ env.WORKDIR }}
env:
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: terraform apply -refresh=false -lock=false -auto-approve

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
**/.terraform/
**/.terraform.lock.hcl
*.tfstate
*.tfstate.*
crash.log
.terraform.tfstate.lock.info

View File

@ -0,0 +1,87 @@
# Module for managing datasources
module "grafana_datasource01" {
source = "../../../modules/grafana_datasource"
datasources = var.datasources
org_id = var.org_id
providers = {
grafana = grafana.grafana01
}
}
# Module for managing folders
module "grafana_dashboard_folder01" {
source = "../../../modules/grafana_dashboard_folder"
groups = var.groups
org_id = var.org_id
providers = {
grafana = grafana.grafana01
}
}
# Module for managing dashboards
module "grafana_dashboard01" {
source = "../../../modules/grafana_dashboard"
groups = var.groups
org_id = var.org_id
folder_ids = module.grafana_dashboard_folder01.folder_ids
dashboard_uid_max_length = var.dashboard_uid_max_length
depends_on = [module.grafana_dashboard_folder01]
providers = {
grafana = grafana.grafana01
}
}
# Module for managing contact points
module "grafana_contact_points01" {
source = "../../../modules/grafana_contact_points"
org_id = var.org_id
env = var.env
grafana_url = "https://grafana-dev.hhmon.ru/"
contact_points = local.contact_points
providers = {
grafana = grafana.grafana01
}
}
# Module for managing notification policies
module "grafana_notification_policies01" {
source = "../../../modules/grafana_notification_policies"
org_id = var.org_id
contact_points = local.contact_points
notification_policies = var.notification_policies
depends_on = [module.grafana_contact_points01]
providers = {
grafana = grafana.grafana01
}
}
# Module for managing rule group
module "grafana_rule_group01" {
source = "../../../modules/grafana_rule_group"
org_id = var.org_id
groups = var.groups
folder_uids = module.grafana_dashboard_folder01.folder_uids
datasources = var.datasources
providers = {
grafana = grafana.grafana01
}
# Time-related parameters
interval_seconds = var.interval_seconds
default_interval_ms = var.default_interval_ms
default_alert_duration = var.default_alert_duration
default_evaluation_interval = var.default_evaluation_interval
default_time_range_from = var.default_time_range_from
default_processing_range = var.default_processing_range
default_max_data_points = var.default_max_data_points
default_no_data_state = var.default_no_data_state
default_exec_err_state = var.default_exec_err_state
depends_on = [
module.grafana_datasource01,
module.grafana_notification_policies01,
module.grafana_dashboard_folder01,
module.grafana_contact_points01
]
}

View File

@ -0,0 +1,154 @@
env = "dev"
# Maximum dashboard UID length after auto-generation from json uid + file path
dashboard_uid_max_length = 40
# Controls the ability to manually edit resources in Grafana.
#
# disable_provenance = true:
# - Removes provisioning tags and locks for alerting components.
# - Allows manual changes through the Grafana UI for the following resources:
# - Alert Rules
# - Contact Points
# - Mute Timings
# - Notification Templates
# - Notification Policies
#
# disable_provenance = false:
# - Preserves provisioning tags and locks for the above components.
# - Prevents manual changes in the Grafana UI from conflicting with Terraform-managed alerting resources.
# - This setting ensures that any changes made directly in the Grafana UI will not persist for these resources.
disable_provenance = true
# Grafana organization settings as an array of objects
organizations = [
{
create_new_organization = false
organization_name = "Seahorse"
keep_manual_changes = true
prevent_destroy_on_recreate = true
}
]
# Current organization for deploying
org_id = "2"
# Alert groups configuration
groups = [
{
dashboard_alert_group_name = "System Alerts"
folder_uid = "system"
alert_definitions_path = "alerts/system"
dashboard_path_if_exist = "dashboards/system"
keep_manual_changes = false
prevent_destroy_on_recreate = false
alerts_on_datasources_uid = ["prometheus"]
},
{
dashboard_alert_group_name = "Self monitoring"
folder_uid = "self-monitoring"
alert_definitions_path = "alerts/self-monitoring"
dashboard_path_if_exist = "dashboards/self-monitoring"
keep_manual_changes = false
prevent_destroy_on_recreate = false
alerts_on_datasources_uid = ["prometheus-local-1"]
}
]
# Data sources configuration
datasources = [
{
name = "prometheus"
uid = "prometheus"
type = "prometheus"
url = "http://localhost:8481/select/0/prometheus"
access_mode = "proxy"
is_default = true
basic_auth = false
json_data = {
timeInterval = "15s"
}
keep_manual_changes = false
prevent_destroy_on_recreate = false
}
]
# Notification policies configuration
notification_policies = [
{
contact_point = "infra-alerts-critical"
continue = true
matchers = [
{
label = "severity"
match = "="
value = "disaster"
},
{
label = "team"
match = "="
value = "infra"
}
]
},
{
contact_point = "infra-alerts-critical"
continue = true
matchers = [
{
label = "severity"
match = "="
value = "critical"
},
{
label = "team"
match = "="
value = "infra"
}
]
},
{
contact_point = "infra-alerts-informational"
continue = true
matchers = [
{
label = "severity"
match = "="
value = "warning"
},
{
label = "team"
match = "="
value = "infra"
}
]
},
{
contact_point = "infra-alerts-informational"
continue = true
matchers = [
{
label = "severity"
match = "="
value = "perfomance"
},
{
label = "team"
match = "="
value = "infra"
}
]
},
{
contact_point = "infra-alerts-test"
continue = true
matchers = [
{
label = "status"
match = "="
value = "test"
}
]
}
]

View File

@ -0,0 +1,19 @@
variable "groups" {
description = "List of alert groups with their definitions and data sources"
type = list(object({
dashboard_alert_group_name = string
folder_uid = string
alert_definitions_path = optional(string, null)
dashboard_path_if_exist = optional(string, null)
keep_manual_changes = optional(bool, false)
prevent_destroy_on_recreate = optional(bool, false)
alerts_on_datasources_uid = list(string)
}))
}
variable "dashboard_uid_max_length" {
description = "Maximum generated length of dashboard UID"
type = number
default = 40
}

View File

@ -0,0 +1,87 @@
# Module for managing datasources
module "grafana_datasource01" {
source = "../../../modules/grafana_datasource"
datasources = var.datasources
org_id = var.org_id
providers = {
grafana = grafana.grafana01
}
}
# Module for managing folders
module "grafana_dashboard_folder01" {
source = "../../../modules/grafana_dashboard_folder"
groups = var.groups
org_id = var.org_id
providers = {
grafana = grafana.grafana01
}
}
# Module for managing dashboards
module "grafana_dashboard01" {
source = "../../../modules/grafana_dashboard"
groups = var.groups
org_id = var.org_id
folder_ids = module.grafana_dashboard_folder01.folder_ids
dashboard_uid_max_length = var.dashboard_uid_max_length
depends_on = [module.grafana_dashboard_folder01]
providers = {
grafana = grafana.grafana01
}
}
# Module for managing contact points
module "grafana_contact_points01" {
source = "../../../modules/grafana_contact_points"
org_id = var.org_id
env = var.env
grafana_url = "https://grafana-dev.hhmon.ru/"
contact_points = local.contact_points
providers = {
grafana = grafana.grafana01
}
}
# Module for managing notification policies
module "grafana_notification_policies01" {
source = "../../../modules/grafana_notification_policies"
org_id = var.org_id
contact_points = local.contact_points
notification_policies = var.notification_policies
depends_on = [module.grafana_contact_points01]
providers = {
grafana = grafana.grafana01
}
}
# Module for managing rule group
module "grafana_rule_group01" {
source = "../../../modules/grafana_rule_group"
org_id = var.org_id
groups = var.groups
folder_uids = module.grafana_dashboard_folder01.folder_uids
datasources = var.datasources
providers = {
grafana = grafana.grafana01
}
# Time-related parameters
interval_seconds = var.interval_seconds
default_interval_ms = var.default_interval_ms
default_alert_duration = var.default_alert_duration
default_evaluation_interval = var.default_evaluation_interval
default_time_range_from = var.default_time_range_from
default_processing_range = var.default_processing_range
default_max_data_points = var.default_max_data_points
default_no_data_state = var.default_no_data_state
default_exec_err_state = var.default_exec_err_state
depends_on = [
module.grafana_datasource01,
module.grafana_notification_policies01,
module.grafana_dashboard_folder01,
module.grafana_contact_points01
]
}

View File

@ -0,0 +1,154 @@
env = "dev"
# Maximum dashboard UID length after auto-generation from json uid + file path
dashboard_uid_max_length = 40
# Controls the ability to manually edit resources in Grafana.
#
# disable_provenance = true:
# - Removes provisioning tags and locks for alerting components.
# - Allows manual changes through the Grafana UI for the following resources:
# - Alert Rules
# - Contact Points
# - Mute Timings
# - Notification Templates
# - Notification Policies
#
# disable_provenance = false:
# - Preserves provisioning tags and locks for the above components.
# - Prevents manual changes in the Grafana UI from conflicting with Terraform-managed alerting resources.
# - This setting ensures that any changes made directly in the Grafana UI will not persist for these resources.
disable_provenance = true
# Grafana organization settings as an array of objects
organizations = [
{
create_new_organization = false
organization_name = "adibrov"
keep_manual_changes = true
prevent_destroy_on_recreate = true
}
]
# Current organization for deploying
org_id = "35"
# Alert groups configuration
groups = [
{
dashboard_alert_group_name = "System Alerts"
folder_uid = "system"
alert_definitions_path = "alerts/system"
dashboard_path_if_exist = "dashboards/system"
keep_manual_changes = false
prevent_destroy_on_recreate = false
alerts_on_datasources_uid = ["prometheus"]
},
{
dashboard_alert_group_name = "Self monitoring"
folder_uid = "self-monitoring"
alert_definitions_path = "alerts/self-monitoring"
dashboard_path_if_exist = "dashboards/self-monitoring"
keep_manual_changes = false
prevent_destroy_on_recreate = false
alerts_on_datasources_uid = ["prometheus-local-1"]
}
]
# Data sources configuration
datasources = [
{
name = "prometheus"
uid = "prometheus"
type = "prometheus"
url = "http://localhost:8481/select/0/prometheus"
access_mode = "proxy"
is_default = true
basic_auth = false
json_data = {
timeInterval = "15s"
}
keep_manual_changes = false
prevent_destroy_on_recreate = false
}
]
# Notification policies configuration
notification_policies = [
{
contact_point = "infra-alerts-critical"
continue = true
matchers = [
{
label = "severity"
match = "="
value = "disaster"
},
{
label = "team"
match = "="
value = "infra"
}
]
},
{
contact_point = "infra-alerts-critical"
continue = true
matchers = [
{
label = "severity"
match = "="
value = "critical"
},
{
label = "team"
match = "="
value = "infra"
}
]
},
{
contact_point = "infra-alerts-informational"
continue = true
matchers = [
{
label = "severity"
match = "="
value = "warning"
},
{
label = "team"
match = "="
value = "infra"
}
]
},
{
contact_point = "infra-alerts-informational"
continue = true
matchers = [
{
label = "severity"
match = "="
value = "perfomance"
},
{
label = "team"
match = "="
value = "infra"
}
]
},
{
contact_point = "infra-alerts-test"
continue = true
matchers = [
{
label = "status"
match = "="
value = "test"
}
]
}
]

View File

@ -0,0 +1,19 @@
variable "groups" {
description = "List of alert groups with their definitions and data sources"
type = list(object({
dashboard_alert_group_name = string
folder_uid = string
alert_definitions_path = optional(string, null)
dashboard_path_if_exist = optional(string, null)
keep_manual_changes = optional(bool, false)
prevent_destroy_on_recreate = optional(bool, false)
alerts_on_datasources_uid = list(string)
}))
}
variable "dashboard_uid_max_length" {
description = "Maximum generated length of dashboard UID"
type = number
default = 40
}

View File

@ -0,0 +1,101 @@
locals {
# Dashboards with both manual changes allowed and destroy protection
dashboards_ignore_and_protect = flatten([
for group in var.groups : [
for file in(group.dashboard_path_if_exist != null ? fileset(group.dashboard_path_if_exist, "**/*.json") : []) : {
group_name = group.dashboard_alert_group_name
file_path = "${group.dashboard_path_if_exist}/${file}"
dashboard_json = jsondecode(file("${group.dashboard_path_if_exist}/${file}"))
dashboard_uid = substr(
regexreplace(
lower(
"${try(jsondecode(file("${group.dashboard_path_if_exist}/${file}")).uid, trimsuffix(basename(file), ".json"))}_${group.dashboard_path_if_exist}/${file}"
),
"[^a-z0-9_-]",
"_"
),
0,
var.dashboard_uid_max_length
)
folder_id = lookup(var.folder_ids, group.dashboard_alert_group_name, null)
keep_manual_changes = lookup(group, "keep_manual_changes", false)
prevent_destroy_on_recreate = lookup(group, "prevent_destroy_on_recreate", false)
}
] if lookup(group, "keep_manual_changes", false) && lookup(group, "prevent_destroy_on_recreate", false)
])
# Dashboards with only manual changes allowed
dashboards_ignore_only = flatten([
for group in var.groups : [
for file in(group.dashboard_path_if_exist != null ? fileset(group.dashboard_path_if_exist, "**/*.json") : []) : {
group_name = group.dashboard_alert_group_name
file_path = "${group.dashboard_path_if_exist}/${file}"
dashboard_json = jsondecode(file("${group.dashboard_path_if_exist}/${file}"))
dashboard_uid = substr(
regexreplace(
lower(
"${try(jsondecode(file("${group.dashboard_path_if_exist}/${file}")).uid, trimsuffix(basename(file), ".json"))}_${group.dashboard_path_if_exist}/${file}"
),
"[^a-z0-9_-]",
"_"
),
0,
var.dashboard_uid_max_length
)
folder_id = lookup(var.folder_ids, group.dashboard_alert_group_name, null)
keep_manual_changes = lookup(group, "keep_manual_changes", false)
prevent_destroy_on_recreate = lookup(group, "prevent_destroy_on_recreate", false)
}
] if lookup(group, "keep_manual_changes", false) && !lookup(group, "prevent_destroy_on_recreate", false)
])
# Dashboards with only destroy protection
dashboards_protect_only = flatten([
for group in var.groups : [
for file in(group.dashboard_path_if_exist != null ? fileset(group.dashboard_path_if_exist, "**/*.json") : []) : {
group_name = group.dashboard_alert_group_name
file_path = "${group.dashboard_path_if_exist}/${file}"
dashboard_json = jsondecode(file("${group.dashboard_path_if_exist}/${file}"))
dashboard_uid = substr(
regexreplace(
lower(
"${try(jsondecode(file("${group.dashboard_path_if_exist}/${file}")).uid, trimsuffix(basename(file), ".json"))}_${group.dashboard_path_if_exist}/${file}"
),
"[^a-z0-9_-]",
"_"
),
0,
var.dashboard_uid_max_length
)
folder_id = lookup(var.folder_ids, group.dashboard_alert_group_name, null)
keep_manual_changes = lookup(group, "keep_manual_changes", false)
prevent_destroy_on_recreate = lookup(group, "prevent_destroy_on_recreate", false)
}
] if !lookup(group, "keep_manual_changes", false) && lookup(group, "prevent_destroy_on_recreate", false)
])
# Standard dashboards without any special lifecycle management
dashboards_standard = flatten([
for group in var.groups : [
for file in(group.dashboard_path_if_exist != null ? fileset(group.dashboard_path_if_exist, "**/*.json") : []) : {
group_name = group.dashboard_alert_group_name
file_path = "${group.dashboard_path_if_exist}/${file}"
dashboard_json = jsondecode(file("${group.dashboard_path_if_exist}/${file}"))
dashboard_uid = substr(
regexreplace(
lower(
"${try(jsondecode(file("${group.dashboard_path_if_exist}/${file}")).uid, trimsuffix(basename(file), ".json"))}_${group.dashboard_path_if_exist}/${file}"
),
"[^a-z0-9_-]",
"_"
),
0,
var.dashboard_uid_max_length
)
folder_id = lookup(var.folder_ids, group.dashboard_alert_group_name, null)
keep_manual_changes = lookup(group, "keep_manual_changes", false)
prevent_destroy_on_recreate = lookup(group, "prevent_destroy_on_recreate", false)
}
] if !lookup(group, "keep_manual_changes", false) && !lookup(group, "prevent_destroy_on_recreate", false)
])
}

View File

@ -0,0 +1,52 @@
# Dashboards with both manual changes allowed and destroy protection
resource "grafana_dashboard" "dashboards_ignore_and_protect" {
for_each = { for d in local.dashboards_ignore_and_protect : d.file_path => d }
config_json = jsonencode(merge(each.value.dashboard_json, { uid = each.value.dashboard_uid }))
folder = each.value.folder_id
org_id = var.org_id
overwrite = true
lifecycle {
prevent_destroy = true
ignore_changes = [config_json]
}
}
# Dashboards with only manual changes allowed
resource "grafana_dashboard" "dashboards_ignore_only" {
for_each = { for d in local.dashboards_ignore_only : d.file_path => d }
config_json = jsonencode(merge(each.value.dashboard_json, { uid = each.value.dashboard_uid }))
folder = each.value.folder_id
org_id = var.org_id
overwrite = true
lifecycle {
ignore_changes = [config_json]
}
}
# Dashboards with only destroy protection
resource "grafana_dashboard" "dashboards_protect_only" {
for_each = { for d in local.dashboards_protect_only : d.file_path => d }
config_json = jsonencode(merge(each.value.dashboard_json, { uid = each.value.dashboard_uid }))
folder = each.value.folder_id
org_id = var.org_id
overwrite = true
lifecycle {
prevent_destroy = true
}
}
# Standard dashboards without any special lifecycle management
resource "grafana_dashboard" "dashboards_standard" {
for_each = { for d in local.dashboards_standard : d.file_path => d }
config_json = jsonencode(merge(each.value.dashboard_json, { uid = each.value.dashboard_uid }))
folder = each.value.folder_id
org_id = var.org_id
overwrite = true
}

View File

@ -0,0 +1,32 @@
variable "org_id" {
description = "ID of the organization for dashboards"
type = string
}
variable "groups" {
description = "List of alert groups with their definitions and data sources"
type = list(object({
dashboard_alert_group_name = string
alert_definitions_path = string
dashboard_path_if_exist = optional(string, null)
keep_manual_changes = optional(bool, false)
prevent_destroy_on_recreate = optional(bool, false)
alerts_on_datasources_uid = list(string)
}))
}
variable "folder_ids" {
description = "Mapping of folder IDs for each alert group"
type = map(string)
}
variable "dashboard_uid_max_length" {
description = "Maximum length of generated dashboard UID"
type = number
default = 40
validation {
condition = var.dashboard_uid_max_length > 0 && var.dashboard_uid_max_length <= 40
error_message = "dashboard_uid_max_length must be between 1 and 40."
}
}