From 7abd61f09a5d2d27350a192537f15b8fed864171 Mon Sep 17 00:00:00 2001 From: Albert Armea Date: Sun, 4 Jan 2026 21:00:44 -0500 Subject: [PATCH] Add config validation binary and scripts --- crates/shepherd-config/Cargo.toml | 4 + .../src/bin/validate-config.rs | 101 +++++++++ scripts/README.md | 4 + scripts/lib/config.sh | 202 ++++++++++++++++++ scripts/shepherd | 10 + 5 files changed, 321 insertions(+) create mode 100644 crates/shepherd-config/src/bin/validate-config.rs create mode 100644 scripts/lib/config.sh diff --git a/crates/shepherd-config/Cargo.toml b/crates/shepherd-config/Cargo.toml index 299f43a..af3a657 100644 --- a/crates/shepherd-config/Cargo.toml +++ b/crates/shepherd-config/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true description = "Configuration parsing and validation for shepherdd" +[[bin]] +name = "validate-config" +path = "src/bin/validate-config.rs" + [dependencies] shepherd-util = { workspace = true } shepherd-api = { workspace = true } diff --git a/crates/shepherd-config/src/bin/validate-config.rs b/crates/shepherd-config/src/bin/validate-config.rs new file mode 100644 index 0000000..10c27da --- /dev/null +++ b/crates/shepherd-config/src/bin/validate-config.rs @@ -0,0 +1,101 @@ +//! Config validation CLI tool +//! +//! Validates a shepherdd configuration file and reports any errors. + +use shepherd_api::EntryKind; +use shepherd_util::default_config_path; +use std::path::PathBuf; +use std::process::ExitCode; + +fn main() -> ExitCode { + let args: Vec = std::env::args().collect(); + + let config_path = match args.get(1) { + Some(path) => PathBuf::from(path), + None => { + let default_path = default_config_path(); + eprintln!("Usage: validate-config [config-file]"); + eprintln!(); + eprintln!("Validates a shepherdd configuration file."); + eprintln!(); + eprintln!("If no path is provided, uses: {}", default_path.display()); + eprintln!(); + eprintln!("Example:"); + eprintln!(" validate-config {}", default_path.display()); + eprintln!(" validate-config config.example.toml"); + return ExitCode::from(2); + } + }; + + // Check file exists + if !config_path.exists() { + eprintln!("Error: Configuration file not found: {}", config_path.display()); + return ExitCode::from(1); + } + + // Try to load and validate + match shepherd_config::load_config(&config_path) { + Ok(policy) => { + println!("✓ Configuration is valid"); + println!(); + println!("Summary:"); + println!(" Config version: {}", shepherd_config::CURRENT_CONFIG_VERSION); + println!(" Entries: {}", policy.entries.len()); + + // Show entry summary + if !policy.entries.is_empty() { + println!(); + println!("Entries:"); + for entry in &policy.entries { + let kind_str = match &entry.kind { + EntryKind::Process { command, .. } => { + format!("process ({})", command) + } + EntryKind::Snap { snap_name, .. } => { + format!("snap ({})", snap_name) + } + EntryKind::Vm { driver, .. } => { + format!("vm ({})", driver) + } + EntryKind::Media { library_id, .. } => { + format!("media ({})", library_id) + } + EntryKind::Custom { type_name, .. } => { + format!("custom ({})", type_name) + } + }; + println!(" - {} [{}]: {}", entry.id.as_str(), kind_str, entry.label); + } + } + + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("✗ Configuration validation failed"); + eprintln!(); + match &e { + shepherd_config::ConfigError::ReadError(io_err) => { + eprintln!("Failed to read file: {}", io_err); + } + shepherd_config::ConfigError::ParseError(parse_err) => { + eprintln!("TOML parse error:"); + eprintln!(" {}", parse_err); + } + shepherd_config::ConfigError::ValidationFailed { errors } => { + eprintln!("Validation errors ({}):", errors.len()); + for err in errors { + eprintln!(" - {}", err); + } + } + shepherd_config::ConfigError::UnsupportedVersion(ver) => { + eprintln!( + "Unsupported config version: {} (expected {})", + ver, + shepherd_config::CURRENT_CONFIG_VERSION + ); + } + } + ExitCode::from(1) + } + } +} diff --git a/scripts/README.md b/scripts/README.md index 4d7d009..06b3074 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -15,6 +15,9 @@ This directory contains the unified script system for shepherd-launcher. # Building ./shepherd build [--release] +# Configuration +./shepherd config validate [path] + # Development ./shepherd dev run @@ -39,6 +42,7 @@ scripts/ │ ├── common.sh # Logging, error handling, sudo helpers │ ├── deps.sh # Dependency management │ ├── build.sh # Cargo build logic +│ ├── config.sh # Configuration validation │ ├── sway.sh # Nested sway execution │ ├── install.sh # Installation logic │ └── harden.sh # User hardening/unhardening diff --git a/scripts/lib/config.sh b/scripts/lib/config.sh new file mode 100644 index 0000000..2be682b --- /dev/null +++ b/scripts/lib/config.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# Configuration validation logic for shepherd-launcher +# Validates shepherdd configuration files + +# Get the directory containing this script +CONFIG_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source common utilities +# shellcheck source=common.sh +source "$CONFIG_LIB_DIR/common.sh" + +# Default configuration paths +# Uses XDG_CONFIG_HOME or ~/.config/shepherd/config.toml +get_default_config_path() { + if [[ -n "${XDG_CONFIG_HOME:-}" ]]; then + echo "$XDG_CONFIG_HOME/shepherd/config.toml" + else + echo "$HOME/.config/shepherd/config.toml" + fi +} + +EXAMPLE_CONFIG_NAME="config.example.toml" + +# Get path to the validate-config binary +get_validate_binary() { + local release="${1:-false}" + local repo_root + repo_root="$(get_repo_root)" + + if [[ "$release" == "true" ]]; then + echo "$repo_root/target/release/validate-config" + else + echo "$repo_root/target/debug/validate-config" + fi +} + +# Build the validate-config binary if needed +build_validate_binary() { + local release="${1:-false}" + local repo_root + repo_root="$(get_repo_root)" + + verify_repo + require_command cargo rust + + cd "$repo_root" || die "Failed to change directory to $repo_root" + + if [[ "$release" == "true" ]]; then + info "Building validate-config (release mode)..." + cargo build --release --bin validate-config + else + info "Building validate-config..." + cargo build --bin validate-config + fi +} + +# Ensure the validate binary exists, building if necessary +ensure_validate_binary() { + local release="${1:-false}" + local binary + binary="$(get_validate_binary "$release")" + + if [[ ! -x "$binary" ]]; then + build_validate_binary "$release" + fi + + echo "$binary" +} + +# Validate a configuration file +validate_config_file() { + local config_path="$1" + local release="${2:-false}" + + # Build/find the validator + local binary + binary="$(ensure_validate_binary "$release")" + + # Run validation + "$binary" "$config_path" +} + +# Show config help +config_usage() { + cat < [options] + +Commands: + validate [path] Validate a configuration file + help Show this help message + +Options: + --release Use release build of validator + +The validate command checks a configuration file for: + - Valid TOML syntax + - Correct config_version + - Valid entry definitions + - Valid time windows and availability specs + - Warning thresholds that make sense with limits + +Examples: + # Validate the installed config + shepherd config validate + + # Validate a specific file + shepherd config validate /path/to/config.toml + + # Validate the example config in the repo + shepherd config validate config.example.toml + +Default paths: + Installed: $(get_default_config_path) + Example: \$REPO_ROOT/$EXAMPLE_CONFIG_NAME +EOF +} + +# Main entry point for config commands +config_main() { + local subcmd="${1:-}" + shift || true + + case "$subcmd" in + validate) + local config_path="" + local release="false" + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --release) + release="true" + shift + ;; + -h|--help) + config_usage + return 0 + ;; + -*) + die "Unknown option: $1" + ;; + *) + if [[ -z "$config_path" ]]; then + config_path="$1" + else + die "Too many arguments" + fi + shift + ;; + esac + done + + # Determine config path + if [[ -z "$config_path" ]]; then + # Try default path first + local default_config + default_config="$(get_default_config_path)" + if [[ -f "$default_config" ]]; then + config_path="$default_config" + info "Using installed config: $config_path" + else + # Fall back to example in repo + local repo_root + repo_root="$(get_repo_root)" + local example_path="$repo_root/$EXAMPLE_CONFIG_NAME" + if [[ -f "$example_path" ]]; then + config_path="$example_path" + info "Using example config: $config_path" + else + die "No config file found. Specify a path or install shepherdd." + fi + fi + fi + + # Resolve relative paths + if [[ ! "$config_path" = /* ]]; then + config_path="$(pwd)/$config_path" + fi + + # Check file exists + if [[ ! -f "$config_path" ]]; then + die "Configuration file not found: $config_path" + fi + + # Validate + info "Validating: $config_path" + echo "" + validate_config_file "$config_path" "$release" + ;; + + ""|help|-h|--help) + config_usage + ;; + + *) + error "Unknown config command: $subcmd" + echo "" + config_usage + exit 1 + ;; + esac +} diff --git a/scripts/shepherd b/scripts/shepherd index 6cf44a5..03359d3 100755 --- a/scripts/shepherd +++ b/scripts/shepherd @@ -21,6 +21,8 @@ source "$LIB_DIR/sway.sh" source "$LIB_DIR/install.sh" # shellcheck source=lib/harden.sh source "$LIB_DIR/harden.sh" +# shellcheck source=lib/config.sh +source "$LIB_DIR/config.sh" # Version VERSION="0.1.0" @@ -35,6 +37,7 @@ Usage: shepherd [subcommand] [options] Commands: deps Manage system dependencies build Build shepherd binaries + config Configuration validation dev Development commands install Install shepherd to the system harden User hardening for kiosk mode @@ -60,6 +63,10 @@ Hardening: shepherd harden apply --user USER Apply kiosk restrictions shepherd harden revert --user USER Remove restrictions +Configuration: + shepherd config validate Validate installed config + shepherd config validate Validate a specific file + Options: -h, --help Show this help message -V, --version Show version @@ -80,6 +87,9 @@ main() { build) build_main "$@" ;; + config) + config_main "$@" + ;; dev) local subcmd="${1:-}" shift || true