-
Notifications
You must be signed in to change notification settings - Fork 147
feat: add group-based deployment filtering #365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <!-- | ||
| SPDX-FileCopyrightText: 2025 Serokell <https://serokell.io/> | ||
|
|
||
| SPDX-License-Identifier: MPL-2.0 | ||
| --> | ||
|
|
||
| # 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` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # SPDX-FileCopyrightText: 2025 Serokell <https://serokell.io/> | ||
| # | ||
| # 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; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,19 @@ | |||||||
| "type": "string" | ||||||||
| } | ||||||||
| }, | ||||||||
| "groups": { | ||||||||
| "anyOf": [ | ||||||||
| { | ||||||||
| "type": "string" | ||||||||
| }, | ||||||||
| { | ||||||||
| "type": "array", | ||||||||
| "items": { | ||||||||
| "type": "string" | ||||||||
| } | ||||||||
|
||||||||
| } | |
| }, | |
| "uniqueItems": true |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -74,6 +74,9 @@ pub struct Opts { | |||||||||||||
| /// Override the SSH options used | ||||||||||||||
| #[arg(long, allow_hyphen_values = true)] | ||||||||||||||
| ssh_opts: Option<String>, | ||||||||||||||
| /// Filter profiles by group (merged from profile/node/deploy) | ||||||||||||||
| #[arg(long, num_args = 1..)] | ||||||||||||||
| groups: Option<Vec<String>>, | ||||||||||||||
| /// Override if the connecting to the target node should be considered fast | ||||||||||||||
| #[arg(long)] | ||||||||||||||
| fast_connection: Option<bool>, | ||||||||||||||
|
|
@@ -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."); | ||||||||||||||
|
||||||||||||||
| info!("No profiles matched selection."); | |
| if cmd_overrides.groups.is_some() { | |
| error!("No profiles matched selection after applying group filters. This may indicate a misspelled group name or missing group assignments."); | |
| } else { | |
| info!("No profiles matched selection."); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<bool>, | ||
| #[serde(rename(deserialize = "interactiveSudo"))] | ||
| pub interactive_sudo: Option<bool>, | ||
| #[serde( | ||
| default, | ||
| rename(deserialize = "groups"), | ||
| deserialize_with = "deserialize_groups" | ||
| )] | ||
| #[merge(strategy = merge_groups)] | ||
| pub groups: BTreeSet<String>, | ||
| } | ||
|
|
||
| #[derive(Deserialize)] | ||
| #[serde(untagged)] | ||
| enum StringOrVec { | ||
| String(String), | ||
| Vec(Vec<String>), | ||
| } | ||
|
|
||
| fn deserialize_groups<'de, D>(deserializer: D) -> Result<BTreeSet<String>, D::Error> | ||
| where | ||
| D: Deserializer<'de>, | ||
| { | ||
| let value = Option::<StringOrVec>::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<String>, right: BTreeSet<String>) { | ||
| left.extend(right); | ||
| } | ||
|
Comment on lines
+57
to
75
|
||
|
|
||
| #[derive(Deserialize, Debug, Clone)] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README documentation states groups values are "merged as a set (deduplicated)" from "profile > node > deploy", but the
>notation is used throughout the README (line 185) to denote override priority (where a profile's value takes precedence over the node's value). For groups specifically, ALL values from all three levels are unioned together — a profile with no groups still inherits groups from node and deploy levels. This union semantics is different from the override semantics of all other generic settings and is not clearly distinguished in the documentation. The comment should explicitly state that unlike other settings, groups from all levels are combined (not overridden), so users don't expect only the most specific level's groups to be used.