fix(ops): guard production Influx placeholders
This commit is contained in:
@@ -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> {
|
fn utc_now() -> DateTime<Utc> {
|
||||||
Utc::now()
|
Utc::now()
|
||||||
}
|
}
|
||||||
@@ -930,6 +957,7 @@ fn run(cli: &Cli) -> Result<RunSummary> {
|
|||||||
error: None,
|
error: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
validate_runtime_config(&config)?;
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.timeout(Duration::from_secs(cli.timeout_seconds))
|
.timeout(Duration::from_secs(cli.timeout_seconds))
|
||||||
.no_proxy()
|
.no_proxy()
|
||||||
@@ -1002,6 +1030,37 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use serde_json::json;
|
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]
|
#[test]
|
||||||
fn endpoint_lines_emit_self_test_and_signal() {
|
fn endpoint_lines_emit_self_test_and_signal() {
|
||||||
let events = vec![
|
let events = vec![
|
||||||
|
|||||||
@@ -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> {
|
fn utc_now() -> DateTime<Utc> {
|
||||||
Utc::now()
|
Utc::now()
|
||||||
}
|
}
|
||||||
@@ -879,6 +906,7 @@ fn run(cli: &Cli) -> Result<RunSummary> {
|
|||||||
error: None,
|
error: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
validate_runtime_config(&config)?;
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.timeout(Duration::from_secs(cli.timeout_seconds))
|
.timeout(Duration::from_secs(cli.timeout_seconds))
|
||||||
.no_proxy()
|
.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]
|
#[test]
|
||||||
fn aggregates_daily_and_hourly_active_samples() {
|
fn aggregates_daily_and_hourly_active_samples() {
|
||||||
let config = test_config();
|
let config = test_config();
|
||||||
|
|||||||
@@ -622,6 +622,32 @@
|
|||||||
fail_msg: "aw_dlp_influx_enabled=true, но token пуст и в локальном env, и в текущем /etc/activitywatch/aw-server.env. Exporter будет падать и Grafana не получит DLP-ряды."
|
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
|
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 перед хотфиксами
|
- name: Записать /etc/activitywatch/aw-server.env перед хотфиксами
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: /etc/activitywatch/aw-server.env
|
dest: /etc/activitywatch/aw-server.env
|
||||||
|
|||||||
@@ -36,9 +36,35 @@ Grafana datasource `InfluxDB-AW` читает тот же bucket.
|
|||||||
|
|
||||||
- если `aw_worktime_influx_enabled=true`, `aw_worktime_influx_token` обязан быть непустым;
|
- если `aw_worktime_influx_enabled=true`, `aw_worktime_influx_token` обязан быть непустым;
|
||||||
- если `aw_dlp_influx_enabled=true`, `aw_dlp_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 со старыми рядами.
|
Кроме того, разовый запуск 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
|
## Expected measurements
|
||||||
|
|
||||||
После успешного запуска в `aw_metrics` должны быть свежие ряды:
|
После успешного запуска в `aw_metrics` должны быть свежие ряды:
|
||||||
|
|||||||
Reference in New Issue
Block a user