diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6db30..264c71e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `--ssh-accept-host` was added. +- `--on -` will now read additional apply targets from stdin. ### Fixed diff --git a/doc/guide/wire.md b/doc/guide/wire.md index 4175862..c123070 100644 --- a/doc/guide/wire.md +++ b/doc/guide/wire.md @@ -38,12 +38,12 @@ Ready? Skip to the [Quickstart](./getting-started). The following is the goal for a stable release and not fully implemented. ::: -| Features | Wire | Colmena | -| --------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------- | -| Secret Management | :white_check_mark: | :white_check_mark: | -| Parallel Evaluation | :white_check_mark: | [Experimental](https://colmena.cli.rs/unstable/features/parallelism.html#parallel-evaluation-experimental) | -| Node Tagging | :white_check_mark: | :white_check_mark: | -| `jq` pipeline support | :white_check_mark: | :x:[^2] | -| Magic Rollback | :white_check_mark: (Planned) | :x: | +| Features | Wire | Colmena | +| ------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------- | +| Secret Management | :white_check_mark: | :white_check_mark: | +| Parallel Evaluation | :white_check_mark: | [Experimental](https://colmena.cli.rs/unstable/features/parallelism.html#parallel-evaluation-experimental) | +| Node Tagging | :white_check_mark: | :white_check_mark: | +| Pipeline Support | :white_check_mark: | :x:[^2] | +| Magic Rollback | :white_check_mark: (Planned) | :x: | [^2]: You need to write custom nix code to use Colmena hive metadata inside environments like CI pipelines, bash scripting, etc., which requires a knowledge of its internals. diff --git a/tests/nix/default.nix b/tests/nix/default.nix index e86b027..7858004 100644 --- a/tests/nix/default.nix +++ b/tests/nix/default.nix @@ -29,6 +29,7 @@ in ./suite/test_remote_deploy ./suite/test_local_deploy ./suite/test_keys + ./suite/test_stdin ]; options.wire.testing = mkOption { type = attrsOf ( diff --git a/tests/nix/suite/test_stdin/default.nix b/tests/nix/suite/test_stdin/default.nix new file mode 100644 index 0000000..9b62736 --- /dev/null +++ b/tests/nix/suite/test_stdin/default.nix @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright 2024-2025 wire Contributors + +{ + wire.testing.test_stdin = { + nodes.deployer = { + _wire.deployer = true; + _wire.receiver = true; + }; + testScript = '' + deployer.succeed(f"echo @tag | wire apply --on deployer --no-progress --path {TEST_DIR}/hive.nix --no-keys -vvv >&2") + deployer.succeed("test -f /etc/a") + ''; + }; +} diff --git a/tests/nix/suite/test_stdin/hive.nix b/tests/nix/suite/test_stdin/hive.nix new file mode 100644 index 0000000..433321a --- /dev/null +++ b/tests/nix/suite/test_stdin/hive.nix @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright 2024-2025 wire Contributors + +let + inherit (import ../utils.nix { testName = "test_keys-@IDENT@"; }) makeHive mkHiveNode; +in +makeHive { + meta.nixpkgs = import { localSystem = "x86_64-linux"; }; + deployer = mkHiveNode { hostname = "deployer"; } { + deployment.tags = [ "tag" ]; + environment.etc."a".text = "b"; + }; +} diff --git a/wire/cli/src/apply.rs b/wire/cli/src/apply.rs index a20bf12..b285210 100644 --- a/wire/cli/src/apply.rs +++ b/wire/cli/src/apply.rs @@ -6,8 +6,9 @@ use itertools::{Either, Itertools}; use lib::hive::Hive; use lib::hive::node::{Context, GoalExecutor, Name, StepState}; use lib::{SubCommandModifiers, errors::HiveLibError}; -use miette::{Diagnostic, Result}; +use miette::{Diagnostic, IntoDiagnostic, Result}; use std::collections::HashSet; +use std::io::Read; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use thiserror::Error; @@ -28,12 +29,31 @@ struct NodeError( #[error("{} node(s) failed to apply.", .0.len())] struct NodeErrors(#[related] Vec); +// returns Names and Tags +fn read_apply_targets_from_stdin() -> Result<(Vec, Vec)> { + let mut buf = String::new(); + let mut stdin = std::io::stdin().lock(); + stdin.read_to_string(&mut buf).into_diagnostic()?; + + Ok(buf + .split_whitespace() + .map(|x| ApplyTarget::from(x.to_string())) + .fold((Vec::new(), Vec::new()), |(mut tags, mut names), target| { + match target { + ApplyTarget::Node(name) => names.push(name), + ApplyTarget::Tag(tag) => tags.push(tag), + ApplyTarget::Stdin => {} + } + (tags, names) + })) +} + #[instrument(skip_all, fields(goal = %args.goal, on = %args.on.iter().join(", ")))] pub async fn apply( hive: &mut Hive, args: ApplyArgs, path: PathBuf, - modifiers: SubCommandModifiers, + mut modifiers: SubCommandModifiers, clobber_lock: Arc>, ) -> Result<()> { let header_span = Span::current(); @@ -47,9 +67,21 @@ pub async fn apply( (HashSet::new(), HashSet::new()), |(mut tags, mut names), target| { match target { - ApplyTarget::Tag(tag) => tags.insert(tag.clone()), - ApplyTarget::Node(name) => names.insert(name.clone()), - }; + ApplyTarget::Tag(tag) => { + tags.insert(tag.clone()); + } + ApplyTarget::Node(name) => { + names.insert(name.clone()); + } + ApplyTarget::Stdin => { + // implies non_interactive + modifiers.non_interactive = true; + + let (found_tags, found_names) = read_apply_targets_from_stdin().unwrap(); + names.extend(found_names); + tags.extend(found_tags); + } + } (tags, names) }, ); diff --git a/wire/cli/src/cli.rs b/wire/cli/src/cli.rs index f916560..9d25be8 100644 --- a/wire/cli/src/cli.rs +++ b/wire/cli/src/cli.rs @@ -61,10 +61,15 @@ pub struct Cli { pub enum ApplyTarget { Node(Name), Tag(String), + Stdin, } impl From for ApplyTarget { fn from(value: String) -> Self { + if value == "-" { + return ApplyTarget::Stdin; + } + if let Some(stripped) = value.strip_prefix("@") { ApplyTarget::Tag(stripped.to_string()) } else { @@ -78,6 +83,7 @@ impl Display for ApplyTarget { match self { ApplyTarget::Node(name) => name.fmt(f), ApplyTarget::Tag(tag) => write!(f, "@{tag}"), + ApplyTarget::Stdin => write!(f, "#stdin"), } } } @@ -91,8 +97,11 @@ pub struct ApplyArgs { #[arg(value_enum, default_value_t)] pub goal: Goal, - /// List of literal node names or `@` prefixed tags. - #[arg(short, long, value_name = "NODE | @TAG", num_args = 1..)] + /// List of literal node names, a literal `-`, or `@` prefixed tags. + /// + /// `-` will read additional values from stdin, seperated by whitespace. + /// Any `-` implies `--non-interactive`. + #[arg(short, long, value_name = "NODE | @TAG | `-`", num_args = 1..)] pub on: Vec, #[arg(short, long, default_value_t = 10, value_parser=more_than_zero)]