From 2e64f9b26c25780ea5e1624c0bf88164ce5b3abc Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Wed, 3 Jun 2026 19:32:31 +0300 Subject: [PATCH] fix(ops): guard production Influx placeholders --- .../crates/dlp-influx-exporter/src/main.rs | 59 +++++++++++++++++++ .../worktime-influx-exporter/src/main.rs | 43 ++++++++++++++ ansible/deploy_aw_server.yml | 26 ++++++++ ...Grafana-and-Prometheus-Monitoring-Stack.md | 26 ++++++++ 4 files changed, 154 insertions(+) diff --git a/adk-rust/crates/dlp-influx-exporter/src/main.rs b/adk-rust/crates/dlp-influx-exporter/src/main.rs index 5c5c72a..16afe9e 100644 --- a/adk-rust/crates/dlp-influx-exporter/src/main.rs +++ b/adk-rust/crates/dlp-influx-exporter/src/main.rs @@ -120,6 +120,33 @@ fn load_config() -> Config { } } +fn is_runtime_placeholder(value: &str) -> bool { + let normalized = value.trim().to_ascii_uppercase(); + normalized.is_empty() + || normalized.contains("HOST-EXAMPLE") + || normalized.contains("WINDOWS_USER_EXAMPLE") + || normalized.contains("192.0.2.") + || normalized.contains("198.51.100.") + || normalized.contains("203.0.113.") +} + +fn validate_runtime_config(config: &Config) -> Result<()> { + if !config.influx_enabled { + return Ok(()); + } + if is_runtime_placeholder(&config.influx_url) { + bail!( + "AW_DLP_INFLUX_URL contains an empty/example/TEST-NET value while AW_DLP_INFLUX_ENABLED=true" + ); + } + if config.hosts.is_empty() || config.hosts.iter().any(|host| is_runtime_placeholder(host)) { + bail!( + "AW_DLP_INFLUX_HOSTS contains an empty/example value while AW_DLP_INFLUX_ENABLED=true" + ); + } + Ok(()) +} + fn utc_now() -> DateTime { Utc::now() } @@ -930,6 +957,7 @@ fn run(cli: &Cli) -> Result { error: None, }); } + validate_runtime_config(&config)?; let client = Client::builder() .timeout(Duration::from_secs(cli.timeout_seconds)) .no_proxy() @@ -1002,6 +1030,37 @@ mod tests { use super::*; use serde_json::json; + fn test_config() -> Config { + Config { + aw_api_base: DEFAULT_AW_API_BASE.to_string(), + case_api_base: DEFAULT_CASE_API_BASE.to_string(), + influx_url: String::new(), + influx_org: DEFAULT_INFLUX_ORG.to_string(), + influx_bucket: DEFAULT_INFLUX_BUCKET.to_string(), + influx_token: String::new(), + influx_enabled: false, + hosts: vec![DEFAULT_HOSTS.to_string()], + lookback_days: 30, + event_limit: 2000, + case_limit: 500, + } + } + + #[test] + fn runtime_validation_rejects_placeholder_influx_destination() { + let mut config = test_config(); + config.influx_enabled = true; + config.influx_url = "http://192.0.2.10:8086".to_string(); + config.hosts = vec!["HOST-EXAMPLE".to_string()]; + + let err = validate_runtime_config(&config).unwrap_err().to_string(); + assert!(err.contains("AW_DLP_INFLUX_URL")); + + config.influx_url = "http://influxdb.example.internal:8086".to_string(); + let err = validate_runtime_config(&config).unwrap_err().to_string(); + assert!(err.contains("AW_DLP_INFLUX_HOSTS")); + } + #[test] fn endpoint_lines_emit_self_test_and_signal() { let events = vec![ diff --git a/adk-rust/crates/worktime-influx-exporter/src/main.rs b/adk-rust/crates/worktime-influx-exporter/src/main.rs index dbf4ef6..b94e6e7 100644 --- a/adk-rust/crates/worktime-influx-exporter/src/main.rs +++ b/adk-rust/crates/worktime-influx-exporter/src/main.rs @@ -204,6 +204,33 @@ fn load_config() -> Config { } } +fn is_runtime_placeholder(value: &str) -> bool { + let normalized = value.trim().to_ascii_uppercase(); + normalized.is_empty() + || normalized.contains("HOST-EXAMPLE") + || normalized.contains("WINDOWS_USER_EXAMPLE") + || normalized.contains("192.0.2.") + || normalized.contains("198.51.100.") + || normalized.contains("203.0.113.") +} + +fn validate_runtime_config(config: &Config) -> Result<()> { + if !config.influx_enabled { + return Ok(()); + } + if is_runtime_placeholder(&config.influx_url) { + bail!( + "AW_WORKTIME_INFLUX_URL contains an empty/example/TEST-NET value while AW_WORKTIME_INFLUX_ENABLED=true" + ); + } + if config.hosts.is_empty() || config.hosts.iter().any(|host| is_runtime_placeholder(host)) { + bail!( + "AW_WORKTIME_INFLUX_HOSTS contains an empty/example value while AW_WORKTIME_INFLUX_ENABLED=true" + ); + } + Ok(()) +} + fn utc_now() -> DateTime { Utc::now() } @@ -879,6 +906,7 @@ fn run(cli: &Cli) -> Result { error: None, }); } + validate_runtime_config(&config)?; let client = Client::builder() .timeout(Duration::from_secs(cli.timeout_seconds)) .no_proxy() @@ -1004,6 +1032,21 @@ mod tests { ); } + #[test] + fn runtime_validation_rejects_placeholder_influx_destination() { + let mut config = test_config(); + config.influx_enabled = true; + config.influx_url = "http://192.0.2.10:8086".to_string(); + config.hosts = vec!["HOST-EXAMPLE".to_string()]; + + let err = validate_runtime_config(&config).unwrap_err().to_string(); + assert!(err.contains("AW_WORKTIME_INFLUX_URL")); + + config.influx_url = "http://influxdb.example.internal:8086".to_string(); + let err = validate_runtime_config(&config).unwrap_err().to_string(); + assert!(err.contains("AW_WORKTIME_INFLUX_HOSTS")); + } + #[test] fn aggregates_daily_and_hourly_active_samples() { let config = test_config(); diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index 008b135..2e89600 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -622,6 +622,32 @@ fail_msg: "aw_dlp_influx_enabled=true, но token пуст и в локальном env, и в текущем /etc/activitywatch/aw-server.env. Exporter будет падать и Grafana не получит DLP-ряды." when: aw_dlp_influx_enabled | default(false) | bool + - name: Проверить destination для AW worktime Influx exporter + ansible.builtin.assert: + that: + - (aw_worktime_influx_url | default('') | string | length) > 0 + - "'192.0.2.' not in (aw_worktime_influx_url | default('') | string)" + - "'198.51.100.' not in (aw_worktime_influx_url | default('') | string)" + - "'203.0.113.' not in (aw_worktime_influx_url | default('') | string)" + - (aw_worktime_influx_hosts | default('') | string | length) > 0 + - "'HOST-EXAMPLE' not in (aw_worktime_influx_hosts | default('') | string)" + - "'WINDOWS_USER_EXAMPLE' not in (aw_worktime_influx_hosts | default('') | string)" + fail_msg: "aw_worktime_influx_enabled=true, но URL/hosts похожи на public example/TEST-NET значения. Задайте live значения в private inventory/env, не в public repo." + when: aw_worktime_influx_enabled | default(false) | bool + + - name: Проверить destination для AW DLP Influx exporter + ansible.builtin.assert: + that: + - (aw_dlp_influx_url | default('') | string | length) > 0 + - "'192.0.2.' not in (aw_dlp_influx_url | default('') | string)" + - "'198.51.100.' not in (aw_dlp_influx_url | default('') | string)" + - "'203.0.113.' not in (aw_dlp_influx_url | default('') | string)" + - (aw_dlp_influx_hosts | default('') | string | length) > 0 + - "'HOST-EXAMPLE' not in (aw_dlp_influx_hosts | default('') | string)" + - "'WINDOWS_USER_EXAMPLE' not in (aw_dlp_influx_hosts | default('') | string)" + fail_msg: "aw_dlp_influx_enabled=true, но URL/hosts похожи на public example/TEST-NET значения. Задайте live значения в private inventory/env, не в public repo." + when: aw_dlp_influx_enabled | default(false) | bool + - name: Записать /etc/activitywatch/aw-server.env перед хотфиксами ansible.builtin.copy: dest: /etc/activitywatch/aw-server.env diff --git a/docs/wiki/Grafana-and-Prometheus-Monitoring-Stack.md b/docs/wiki/Grafana-and-Prometheus-Monitoring-Stack.md index cd1a8a3..6f6468c 100644 --- a/docs/wiki/Grafana-and-Prometheus-Monitoring-Stack.md +++ b/docs/wiki/Grafana-and-Prometheus-Monitoring-Stack.md @@ -36,9 +36,35 @@ Grafana datasource `InfluxDB-AW` читает тот же bucket. - если `aw_worktime_influx_enabled=true`, `aw_worktime_influx_token` обязан быть непустым; - если `aw_dlp_influx_enabled=true`, `aw_dlp_influx_token` обязан быть непустым. +- если exporter включен, Influx URL и список hosts не могут быть пустыми, + `HOST-EXAMPLE`, `WINDOWS_USER_EXAMPLE` или TEST-NET адресами. Кроме того, разовый запуск exporters больше не маскируется `failed_when: false`. Если запись в Influx сломана, playbook должен явно упасть, а не оставлять Grafana со старыми рядами. +## Public defaults vs production runtime + +В публичном репозитории Influx defaults должны оставаться обезличенными: + +```yaml +aw_worktime_influx_url: "http://192.0.2.10:8086" +aw_worktime_influx_hosts: "HOST-EXAMPLE" +aw_dlp_influx_url: "http://192.0.2.10:8086" +aw_dlp_influx_hosts: "HOST-EXAMPLE" +``` + +Это нормальный public-safe вид для expert/release materials. В production эти +значения обязательно переопределяются через private inventory, private env или +уже существующий server-side `/etc/activitywatch/aw-server.env`. + +Защита от повторения ошибки: + +- Ansible не даст записать `/etc/activitywatch/aw-server.env`, если включенный + exporter получил public placeholder вместо live destination; +- Rust exporter дополнительно завершится с понятной ошибкой по переменной + `AW_*_INFLUX_URL` или `AW_*_INFLUX_HOSTS`, если такой env все же попал в + runtime; +- живые URL, hosts и tokens не переносятся в tracked files. + ## Expected measurements После успешного запуска в `aw_metrics` должны быть свежие ряды: