Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ deslicer inventory validate --environment acme-prod
deslicer inventory validate --environment acme-prod --log-format json
```

Checks (fail closed): YAML shape and required `inventory_group`; no duplicate groups; no duplicate `source_path`+`dest_dir`; `source_path` exists on disk unless `state: absent`; thin `dest_dir` authoring allowlist (`apps`, `deployment-apps`, `manager-apps`, `shcluster/apps`, `peer-apps`, `slave-apps`, `users` — not `master-apps`); live host-group allowlist via auth → Observer `GET /api/v1/groups` (not a hardcoded list).
Checks (fail closed): YAML shape and required `inventory_group`; no duplicate groups; no duplicate `source_path`+`dest_dir`; `source_path` exists on disk unless `state: absent`; thin `dest_dir` authoring allowlist (`apps`, `deployment-apps`, `manager-apps`, `shcluster/apps` — not `master-apps`, `peer-apps`, `slave-apps`, or `users`); live host-group allowlist via auth → Observer `GET /api/v1/groups` (not a hardcoded list).

Role-based `dest_dir` defaults (when YAML omits `dest_dir`) and identity alias `master-apps` → `manager-apps` are owned by DAP / Observer at compile time — the CLI does not invent defaults.
Role-based `dest_dir` defaults (when YAML omits `dest_dir`) and identity alias `master-apps` → `manager-apps` are owned by DAP / Observer at compile time — the CLI does not invent defaults. `peer-apps` / `slave-apps` / `users` remain Observer observation scopes, not env YAML deploy targets.

`--force` on `init` overwrites workflow templates only — it does not wipe operator `apps:` lists.

Expand Down
23 changes: 23 additions & 0 deletions src/environment_yaml/validate/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,29 @@ fn validate_common_fields(map: &serde_yml::Mapping, obj_path: &str, ctx: &mut Va
)),
}
}

// Phase 1b: thin authoring check for optional target_host override.
// DAP enforces that the host is a member of the destination inventory_group.
if let Some(target_host) = map.get("target_host") {
match scalar_string(Some(target_host)) {
Some(value) if !value.trim().is_empty() => {}
Some(_) => ctx.issues.push(issue(
ctx.file_label,
&format!("{obj_path}.target_host"),
Severity::Error,
"target_host must be a non-empty string".into(),
"Set target_host to a hostname that is a member of this destination's \
inventory_group (DAP verifies membership at compile)",
)),
None => ctx.issues.push(issue(
ctx.file_label,
&format!("{obj_path}.target_host"),
Severity::Error,
"target_host must be a string".into(),
"Set target_host to a hostname string, or omit the field",
)),
}
}
}

fn warn_if_not_splunk_app_root(base: &str, ctx: &mut ValidationCtx<'_>) {
Expand Down
20 changes: 6 additions & 14 deletions src/environment_yaml/validate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,13 @@ pub const VALID_STATES: &[&str] = &["present", "absent"];

/// Thin authoring allowlist for env YAML `dest_dir` (fail-fast in GH Actions / local validate).
///
/// DAP / Observer owns the full scope set, role-based defaults when `dest_dir` is
/// omitted, and identity alias `master-apps` → `manager-apps`. Do not put that
/// logic in the CLI.
/// Deploy targets only (D4 role defaults): DS → `deployment-apps`, CM → `manager-apps`,
/// SH deployer → `shcluster/apps`, else `apps` (on-disk `etc/apps`).
///
/// `apps` maps on-disk to `etc/apps`. Legacy `master-apps` is rejected here —
/// authors must use `manager-apps`.
pub const VALID_DEST_DIRS: &[&str] = &[
"apps",
"deployment-apps",
"manager-apps",
"shcluster/apps",
"peer-apps",
"slave-apps",
"users",
];
/// DAP / Observer still **observes/parses** `peer-apps`, `slave-apps`, `users`, and
/// legacy `master-apps` — those are not GitOps deploy destinations for env YAML.
/// Legacy `master-apps` is rejected here; authors must use `manager-apps`.
pub const VALID_DEST_DIRS: &[&str] = &["apps", "deployment-apps", "manager-apps", "shcluster/apps"];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
Expand Down
52 changes: 50 additions & 2 deletions src/environment_yaml/validate/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,25 +131,73 @@ fn rejects_legacy_master_apps_dest_dir() {
}

#[test]
fn accepts_peer_apps_dest_dir() {
fn rejects_peer_apps_dest_dir() {
let dir = tempdir().unwrap();
write_app(dir.path(), "apps/ta_nix");
let yaml = "destinations:\n\
\x20\x20- inventory_group: indexers\n\
\x20\x20\x20\x20apps:\n\
\x20\x20\x20\x20\x20\x20- source_path: apps/ta_nix\n\
\x20\x20\x20\x20\x20\x20\x20\x20dest_dir: peer-apps\n";
let report =
validate_environment_yaml(yaml, "prod.yml", dir.path(), Some(&known(&["indexers"])));
assert!(report
.errors()
.any(|issue| issue.message.contains("invalid dest_dir")));
}

#[test]
fn rejects_slave_apps_dest_dir() {
let dir = tempdir().unwrap();
write_app(dir.path(), "apps/ta_nix");
let yaml = "destinations:\n\
\x20\x20- inventory_group: indexers\n\
\x20\x20\x20\x20apps:\n\
\x20\x20\x20\x20\x20\x20- source_path: apps/ta_nix\n\
\x20\x20\x20\x20\x20\x20\x20\x20dest_dir: slave-apps\n";
let report =
validate_environment_yaml(yaml, "prod.yml", dir.path(), Some(&known(&["indexers"])));
assert!(report
.errors()
.any(|issue| issue.message.contains("invalid dest_dir")));
}

#[test]
fn accepts_nonempty_target_host() {
let dir = tempdir().unwrap();
write_app(dir.path(), "apps/ta_nix");
let yaml = "destinations:\n\
\x20\x20- inventory_group: indexers\n\
\x20\x20\x20\x20apps:\n\
\x20\x20\x20\x20\x20\x20- source_path: apps/ta_nix\n\
\x20\x20\x20\x20\x20\x20\x20\x20target_host: splunk-idx-01.example.com\n";
let report =
validate_environment_yaml(yaml, "prod.yml", dir.path(), Some(&known(&["indexers"])));
assert!(
report
.errors()
.all(|issue| !issue.message.contains("dest_dir")),
.all(|issue| !issue.message.contains("target_host")),
"{:?}",
report.issues
);
}

#[test]
fn rejects_empty_target_host() {
let dir = tempdir().unwrap();
write_app(dir.path(), "apps/ta_nix");
let yaml = "destinations:\n\
\x20\x20- inventory_group: indexers\n\
\x20\x20\x20\x20apps:\n\
\x20\x20\x20\x20\x20\x20- source_path: apps/ta_nix\n\
\x20\x20\x20\x20\x20\x20\x20\x20target_host: ' '\n";
let report =
validate_environment_yaml(yaml, "prod.yml", dir.path(), Some(&known(&["indexers"])));
assert!(report.errors().any(|issue| issue
.message
.contains("target_host must be a non-empty string")));
}

#[test]
fn rejects_unknown_live_group() {
let dir = tempdir().unwrap();
Expand Down
Loading