Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ destinations:
apps:
- source_path: apps/ta_nix
- inventory_group: forwarders
apps:
apps: []
```

Each `inventory_group` is an Observer host group (`GET /api/v1/groups`). Apps are repo-relative `source_path` entries. The filename stem (`acme-prod` for `acme-prod.yml`) is the `--environment` value and the GitHub Environment name. A repo-level `DESLICER_ENVIRONMENT` variable is only the name pointer so `pull_request` can select that Environment.
Expand Down
1 change: 1 addition & 0 deletions src/commands/catalog/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::Ctx;

#[derive(ClapArgs)]
pub struct Args {
/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/catalog/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct Args {
#[arg(long)]
pub name: Option<String>,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/catalog/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub struct Args {
/// Catalog blueprint id (from `catalog list`)
pub id: String,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,
}
Expand Down
3 changes: 3 additions & 0 deletions src/commands/enroll/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ pub struct Args {
#[arg(long, value_parser = ["insights", "bootstrap"])]
pub purpose: String,

/// Label stored with the enrollment token.
#[arg(long)]
pub name: Option<String>,

/// Maximum hosts this token can enroll (1-10000). Default 500.
#[arg(long)]
pub max_hosts: Option<u32>,

/// Token lifetime in days (1-365). Default 30.
#[arg(long)]
pub expires_days: Option<u32>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/groups/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::Ctx;

#[derive(ClapArgs)]
pub struct Args {
/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub struct Args {
#[arg(long, default_value = "auto")]
pub provider: String,

/// Tenant slug; resolves to the environment file stem and GitHub Environment name.
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/inventory/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct Args {
#[arg(long)]
pub group: Option<String>,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/inventory/import_seed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Args {
#[arg(long, value_name = "FILE")]
pub file: PathBuf,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/inventory/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::Ctx;

#[derive(ClapArgs)]
pub struct Args {
/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/inventory/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::Ctx;

#[derive(ClapArgs)]
pub struct Args {
/// Environment filename stem (GitHub Environment / tenant slug).
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/license/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct Args {
#[arg(long)]
pub key: String,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/license/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::Ctx;

#[derive(ClapArgs)]
pub struct Args {
/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,
}
Expand Down
4 changes: 4 additions & 0 deletions src/commands/remediate/analyse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ use crate::Ctx;

#[derive(ClapArgs)]
pub struct Args {
/// Host to analyse (hostname or host UUID).
#[arg(long)]
pub host: Option<String>,

/// App name to include in the analysis.
#[arg(long)]
pub app: Option<String>,

/// Inventory group to analyse instead of one host.
#[arg(long)]
pub group: Option<String>,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,
}
Expand Down
2 changes: 2 additions & 0 deletions src/commands/remediate/draft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ pub struct Args {
#[arg(long, required_unless_present = "host")]
pub group: Option<String>,

/// Optional name for the draft remediation plan.
#[arg(long)]
pub name: Option<String>,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/upgrade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub struct RollingArgs {
#[arg(long = "role-group", num_args = 1, value_parser = clap::builder::PossibleValuesParser::new(UPGRADE_ROLE_GROUPS))]
pub role_groups: Vec<String>,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,

Expand Down
1 change: 1 addition & 0 deletions src/commands/upgrade/readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct Args {
#[arg(long, required = true)]
pub target: String,

/// Workspace environment (tenant slug) used to scope Observer auth.
#[arg(long)]
pub environment: Option<String>,

Expand Down
13 changes: 9 additions & 4 deletions src/environment_yaml/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ pub fn generate_environment_yaml(

/// Merge Observer groups with an existing file.
///
/// * New groups are appended with an empty `apps:` list.
/// * Existing `apps:` blocks for surviving groups are preserved.
/// * New groups, and groups whose `apps:` value is null, are written as `apps: []`.
/// * Existing `apps:` blocks that list entries or an inline value are preserved.
/// * Removed groups with no `source_path` apps are dropped.
/// * Removed groups that still list apps are kept and reported as blocked.
pub fn merge_environment_yaml(
Expand Down Expand Up @@ -163,10 +163,15 @@ fn emit_blocked_groups(
fn emit_destination(lines: &mut Vec<String>, name: &str, existing: Option<&ExistingAppsBlock>) {
lines.push(format!(" - inventory_group: {}", format_scalar(name)));
match existing {
Some(block) => {
Some(block) if apps_block_has_content(block) => {
lines.push(format!(" apps:{}", block.inline_suffix));
lines.extend(block.body_lines.iter().cloned());
}
None => lines.push(" apps:".to_string()),
// A bare `apps:` is YAML null. `inventory validate` requires a list.
Some(_) | None => lines.push(" apps: []".to_string()),
}
}

fn apps_block_has_content(block: &ExistingAppsBlock) -> bool {
!block.inline_suffix.trim().is_empty() || !block.body_lines.is_empty()
}
29 changes: 26 additions & 3 deletions src/environment_yaml/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub use validate::{

#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::path::Path;

use super::*;

fn groups(names: &[&str]) -> Vec<String> {
Expand Down Expand Up @@ -63,10 +66,30 @@ mod tests {
\n\
destinations:\n\
\x20\x20- inventory_group: indexers\n\
\x20\x20\x20\x20apps:\n";
\x20\x20\x20\x20apps: []\n";
assert_eq!(result.content, expected);
}

#[test]
fn generated_scaffold_passes_environment_validator() {
let result = generate_environment_yaml("dap-102", "DAP 102", &groups(&["Workers"]));
assert!(
result
.content
.contains("inventory_group: Workers\n apps: []\n"),
"{}",
result.content
);
let known: HashSet<String> = ["Workers".to_string()].into_iter().collect();
let report =
validate_environment_yaml(&result.content, &result.path, Path::new("."), Some(&known));
assert!(
report.is_ok(),
"scaffold must pass inventory validate: {:?}",
report.issues
);
}

#[test]
fn dedupes_repeated_names_and_preserves_order() {
let result = generate_environment_yaml(
Expand Down Expand Up @@ -124,10 +147,10 @@ destinations:\n\
));
assert!(result
.content
.contains(" - inventory_group: forwarders\n apps:\n"));
.contains(" - inventory_group: forwarders\n apps: []\n"));
assert!(result
.content
.contains(" - inventory_group: search_heads\n apps:\n"));
.contains(" - inventory_group: search_heads\n apps: []\n"));
assert_eq!(result.added, vec!["search_heads".to_string()]);
assert!(result.removed_empty.is_empty());
assert!(result.blocked.is_empty());
Expand Down
73 changes: 73 additions & 0 deletions tests/cli_help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,76 @@ fn change_status_help_documents_timeout() {
);
assert!(help.contains("Examples:"));
}

/// Flags called out as blank in the help sweep. Keep this list aligned with that set.
const DOCUMENTED_FLAGS: &[(&[&str], &str)] = &[
(&["groups", "list"], "environment"),
(&["inventory", "list"], "environment"),
(&["inventory", "apps"], "environment"),
(&["inventory", "sync"], "environment"),
(&["inventory", "import-seed"], "environment"),
(&["init"], "environment"),
(&["catalog", "list"], "environment"),
(&["catalog", "show"], "environment"),
(&["catalog", "plan"], "environment"),
(&["license", "status"], "environment"),
(&["license", "bind"], "environment"),
(&["remediate", "analyse"], "environment"),
(&["remediate", "draft"], "environment"),
(&["upgrade", "plan"], "environment"),
(&["upgrade", "preview"], "environment"),
(&["upgrade", "readiness"], "environment"),
(&["enroll", "create"], "name"),
(&["enroll", "create"], "max-hosts"),
(&["enroll", "create"], "expires-days"),
(&["remediate", "analyse"], "host"),
(&["remediate", "analyse"], "app"),
(&["remediate", "analyse"], "group"),
(&["remediate", "draft"], "name"),
];

fn long_flag_help(path: &[&str], flag: &str) -> String {
let mut cmd = Cli::command();
let mut current = &mut cmd;
for name in path {
current = current
.find_subcommand_mut(name)
.unwrap_or_else(|| panic!("missing subcommand {name} in {}", path.join(" ")));
}
let argument = current
.get_arguments()
.find(|arg| arg.get_long() == Some(flag))
.unwrap_or_else(|| panic!("missing --{flag} on {}", path.join(" ")));
argument
.get_help()
.or_else(|| argument.get_long_help())
.map(|text| text.to_string())
.unwrap_or_default()
}

#[test]
fn previously_blank_flags_have_one_line_help() {
assert_eq!(DOCUMENTED_FLAGS.len(), 23);
for (path, flag) in DOCUMENTED_FLAGS {
let help = long_flag_help(path, flag);
let label = format!("{} --{flag}", path.join(" "));
assert!(!help.trim().is_empty(), "empty help for `{label}`");
assert!(
!help.contains('\n'),
"help for `{label}` should be one line: {help}"
);
if *flag == "environment" {
assert!(
help.to_ascii_lowercase().contains("tenant slug"),
"`{label}` should say what --environment resolves to: {help}"
);
}
}

let hosts = long_flag_help(&["enroll", "create"], "max-hosts");
assert!(hosts.contains("500"), "{hosts}");
assert!(hosts.to_ascii_lowercase().contains("host"), "{hosts}");
let days = long_flag_help(&["enroll", "create"], "expires-days");
assert!(days.contains("30"), "{days}");
assert!(days.to_ascii_lowercase().contains("day"), "{days}");
}
Loading