diff --git a/README.md b/README.md index c353203a..14d54929 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ If you want to deploy multiple flakes or a subset of profiles with one invocatio Running in this mode, if any of the deploys fails, the deploy will be aborted and all successful deploys rolled back. `--rollback-succeeded false` can be used to override this behavior, otherwise the `auto-rollback` argument takes precedent. +You can also filter which profiles are deployed by group using `deploy --groups [ ...]`. A profile is selected if any of the requested groups appears in its merged `groups` set. + If you require a signing key to push closures to your server, specify the path to it in the `LOCAL_KEY` environment variable. Check out `deploy --help` for CLI flags! Remember to check there before making one-time changes to things like hostnames. @@ -204,6 +206,10 @@ This is a set of options that can be put in any of the above definitions, with t # This is an optional list of arguments that will be passed to SSH. sshOpts = [ "-p" "2121" ]; + # Optional groups used for filtering deployments. + # Can be a string or a list of strings; values from profile > node > deploy are merged as a set (deduplicated). + groups = [ "web" "prod" ]; + # Fast connection to the node. If this is true, copy the whole closure instead of letting the node substitute. # This defaults to `false` fastConnection = false; diff --git a/examples/groups/README.md b/examples/groups/README.md new file mode 100644 index 00000000..806842bf --- /dev/null +++ b/examples/groups/README.md @@ -0,0 +1,16 @@ + + +# Example group-based deployment + +This example shows how to assign `groups` at deploy, node, and profile levels, then filter +deployments with `--groups`. + +Example usage: +- Deploy only profiles matching the `web` group: + - `nix run github:serokell/deploy-rs -- --groups web` +- Deploy only profiles matching `blue` or `db`: + - `nix run github:serokell/deploy-rs -- --groups blue db` diff --git a/examples/groups/flake.nix b/examples/groups/flake.nix new file mode 100644 index 00000000..fc9d6b13 --- /dev/null +++ b/examples/groups/flake.nix @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2025 Serokell +# +# SPDX-License-Identifier: MPL-2.0 + +{ + description = "Deploy two profiles and filter by groups"; + + inputs.deploy-rs.url = "github:serokell/deploy-rs"; + + outputs = { self, nixpkgs, deploy-rs }: { + deploy = { + groups = [ "prod" ]; + nodes.example = { + hostname = "localhost"; + groups = [ "web" "edge" ]; + profiles = { + hello = { + groups = "blue"; + user = "balsoft"; + path = deploy-rs.lib.x86_64-linux.setActivate nixpkgs.legacyPackages.x86_64-linux.hello "./bin/hello"; + }; + cowsay = { + groups = [ "green" "db" ]; + user = "balsoft"; + path = deploy-rs.lib.x86_64-linux.setActivate nixpkgs.legacyPackages.x86_64-linux.cowsay "./bin/cowsay"; + }; + }; + }; + }; + + checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib; + }; +} diff --git a/interface.json b/interface.json index a96d1c2d..b578853d 100644 --- a/interface.json +++ b/interface.json @@ -18,6 +18,19 @@ "type": "string" } }, + "groups": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, "fastConnection": { "type": "boolean" }, diff --git a/src/cli.rs b/src/cli.rs index 020b11f5..80ab567e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -74,6 +74,9 @@ pub struct Opts { /// Override the SSH options used #[arg(long, allow_hyphen_values = true)] ssh_opts: Option, + /// Filter profiles by group (merged from profile/node/deploy) + #[arg(long, num_args = 1..)] + groups: Option>, /// Override if the connecting to the target node should be considered fast #[arg(long)] fast_connection: Option, @@ -547,6 +550,17 @@ async fn run_deploy( log_dir.as_deref(), ); + if let Some(ref groups) = cmd_overrides.groups { + if !deploy_data + .merged_settings + .groups + .iter() + .any(|g| groups.contains(g)) + { + continue; + } + } + let mut deploy_defs = deploy_data.defs()?; if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { @@ -570,6 +584,11 @@ async fn run_deploy( parts.push((deploy_flake, deploy_data, deploy_defs)); } + if parts.is_empty() { + info!("No profiles matched selection."); + return Ok(()); + } + if interactive { prompt_deployment(&parts[..])?; } else { @@ -701,6 +720,7 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { ssh_user: opts.ssh_user, profile_user: opts.profile_user, ssh_opts: opts.ssh_opts, + groups: opts.groups, fast_connection: opts.fast_connection, auto_rollback: opts.auto_rollback, hostname: opts.hostname, diff --git a/src/data.rs b/src/data.rs index 12b0f01b..cd1a7b1e 100644 --- a/src/data.rs +++ b/src/data.rs @@ -3,8 +3,9 @@ // SPDX-License-Identifier: MPL-2.0 use merge::Merge; +use serde::de::Deserializer; use serde::Deserialize; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::path::PathBuf; #[derive(Deserialize, Debug, Clone, Merge)] @@ -37,6 +38,40 @@ pub struct GenericSettings { pub remote_build: Option, #[serde(rename(deserialize = "interactiveSudo"))] pub interactive_sudo: Option, + #[serde( + default, + rename(deserialize = "groups"), + deserialize_with = "deserialize_groups" + )] + #[merge(strategy = merge_groups)] + pub groups: BTreeSet, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum StringOrVec { + String(String), + Vec(Vec), +} + +fn deserialize_groups<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + Ok(match value { + None => BTreeSet::new(), + Some(StringOrVec::String(s)) => { + let mut set = BTreeSet::new(); + set.insert(s); + set + } + Some(StringOrVec::Vec(v)) => v.into_iter().collect(), + }) +} + +fn merge_groups(left: &mut BTreeSet, right: BTreeSet) { + left.extend(right); } #[derive(Deserialize, Debug, Clone)] diff --git a/src/lib.rs b/src/lib.rs index 91ab7c76..d840ee0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,6 +157,7 @@ pub struct CmdOverrides { pub ssh_user: Option, pub profile_user: Option, pub ssh_opts: Option, + pub groups: Option>, pub fast_connection: Option, pub auto_rollback: Option, pub hostname: Option,