diff --git a/integration-tests/allow-apply-all/default.nix b/integration-tests/allow-apply-all/default.nix new file mode 100644 index 0000000..cb4d7a8 --- /dev/null +++ b/integration-tests/allow-apply-all/default.nix @@ -0,0 +1,19 @@ +{ pkgs ? import ../nixpkgs.nix }: + +let + tools = pkgs.callPackage ../tools.nix { + targets = [ "alpha" ]; + }; +in tools.makeTest { + name = "colmena-allow-apply-all"; + + bundle = ./.; + + testScript = '' + logs = deployer.fail("cd /tmp/bundle && run-copy-stderr ${tools.colmenaExec} apply") + + assert "no filter supplied" in logs + + deployer.succeed("cd /tmp/bundle && run-copy-stderr ${tools.colmenaExec} apply --on @target") + ''; +} diff --git a/integration-tests/allow-apply-all/hive.nix b/integration-tests/allow-apply-all/hive.nix new file mode 100644 index 0000000..6c5a5b0 --- /dev/null +++ b/integration-tests/allow-apply-all/hive.nix @@ -0,0 +1,14 @@ +let + tools = import ./tools.nix { + insideVm = true; + targets = ["alpha"]; + }; +in { + meta = { + nixpkgs = tools.pkgs; + allowApplyAll = false; + }; + + deployer = tools.getStandaloneConfigFor "deployer"; + alpha = tools.getStandaloneConfigFor "alpha"; +} diff --git a/integration-tests/default.nix b/integration-tests/default.nix index a7450d8..89655a1 100644 --- a/integration-tests/default.nix +++ b/integration-tests/default.nix @@ -8,6 +8,8 @@ flakes-streaming = import ./flakes { evaluator = "streaming"; }; parallel = import ./parallel {}; + allow-apply-all = import ./allow-apply-all {}; + apply-stable = let test = import ./apply { pkgs = import ./nixpkgs-stable.nix; }; in test.override (old: { diff --git a/manual/src/release-notes.md b/manual/src/release-notes.md index cd681bf..9ca8f57 100644 --- a/manual/src/release-notes.md +++ b/manual/src/release-notes.md @@ -7,6 +7,7 @@ - In `apply-local`, we now only escalate privileges during activation ([#85](https://github.com/zhaofengli/colmena/issues/85)). - Impure overlays are no longer imported by default if a path is specified in `meta.nixpkgs` ([#39](https://github.com/zhaofengli/colmena/issues/39)) - GC roots are now created right after the builds are complete, as opposed to after activation. +- The [`meta.allowApplyAll`](./reference/meta.md#allowapplyall) option has been added. If set to false, deployments without a node filter (`--on`) are disallowed. ## [Release 0.3.0](https://github.com/zhaofengli/colmena/releases/tag/v0.3.0) (2022/04/27) diff --git a/src/command/apply.rs b/src/command/apply.rs index f84e048..fcab351 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -151,6 +151,16 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<( .map(NodeFilter::new) .transpose()?; + if !filter.is_some() { + // User did not specify node, we should check meta and see rules + let meta = hive.get_meta_config().await?; + if !meta.allow_apply_all { + log::error!("No node filter is specified and meta.allowApplyAll is set to false."); + log::error!("Hint: Filter the nodes with --on."); + quit::with_code(1); + } + } + let goal_arg = local_args.value_of("goal").unwrap(); let goal = Goal::from_str(goal_arg).unwrap(); diff --git a/src/nix/hive/eval.nix b/src/nix/hive/eval.nix index 1595c26..062c219 100644 --- a/src/nix/hive/eval.nix +++ b/src/nix/hive/eval.nix @@ -202,11 +202,21 @@ let }; }; }; + + # Add required config Key here since we don't want to eval nixpkgs + metaConfigKeys = [ + "name" "description" + "machinesFile" + "allowApplyAll" + ]; + + metaConfig = lib.filterAttrs (n: v: elem n metaConfigKeys) hive.meta; in { inherit nodes toplevel deploymentConfig deploymentConfigSelected - evalAll evalSelected evalSelectedDrvPaths introspect; + evalAll evalSelected evalSelectedDrvPaths introspect + metaConfig; meta = hive.meta; diff --git a/src/nix/hive/mod.rs b/src/nix/hive/mod.rs index 2986682..8ff2582 100644 --- a/src/nix/hive/mod.rs +++ b/src/nix/hive/mod.rs @@ -10,7 +10,7 @@ use std::convert::AsRef; use tempfile::{NamedTempFile, TempPath}; use tokio::process::Command; -use tokio::sync::RwLock; +use tokio::sync::OnceCell; use serde::Serialize; use validator::Validate; @@ -22,7 +22,7 @@ use super::{ NodeFilter, NixExpression, ProfileDerivation, - StorePath, + StorePath, MetaConfig, }; use super::deployment::TargetNode; use crate::error::ColmenaResult; @@ -58,8 +58,7 @@ pub struct Hive { /// Whether to pass --show-trace in Nix commands. show_trace: bool, - /// The cached machines_file expression. - machines_file: RwLock>>, + meta_config: OnceCell, } struct NixInstantiate<'hive> { @@ -113,12 +112,12 @@ impl Hive { pub fn new(path: HivePath) -> ColmenaResult { let context_dir = path.context_dir(); - Ok(Self { + Ok(Self{ path, context_dir, assets: Assets::new(), show_trace: false, - machines_file: RwLock::new(None), + meta_config: OnceCell::new(), }) } @@ -126,6 +125,13 @@ impl Hive { self.context_dir.as_ref().map(|p| p.as_ref()) } + pub async fn get_meta_config(&self) -> ColmenaResult<&MetaConfig> { + self.meta_config.get_or_try_init(||async { + self.nix_instantiate("hive.metaConfig").eval() + .capture_json().await + }).await + } + pub fn set_show_trace(&mut self, value: bool) { self.show_trace = value; } @@ -142,7 +148,7 @@ impl Hive { let mut options = NixOptions::default(); options.set_show_trace(self.show_trace); - if let Some(machines_file) = self.machines_file().await? { + if let Some(machines_file) = &self.get_meta_config().await?.machines_file { options.set_builders(Some(format!("@{}", machines_file))); } @@ -319,22 +325,6 @@ impl Hive { } } - /// Retrieve the machinesFile setting for the Hive. - async fn machines_file(&self) -> ColmenaResult> { - if let Some(machines_file) = &*self.machines_file.read().await { - return Ok(machines_file.clone()); - } - - let expr = "toJSON (hive.meta.machinesFile or null)"; - let s: String = self.nix_instantiate(expr).eval() - .capture_json().await?; - - let parsed: Option = serde_json::from_str(&s).unwrap(); - self.machines_file.write().await.replace(parsed.clone()); - - Ok(parsed) - } - /// Returns the base expression from which the evaluated Hive can be used. fn get_base_expression(&self) -> String { self.assets.get_base_expression(self.path()) diff --git a/src/nix/hive/options.nix b/src/nix/hive/options.nix index e19c4c0..4451649 100644 --- a/src/nix/hive/options.nix +++ b/src/nix/hive/options.nix @@ -282,6 +282,19 @@ with builtins; rec { default = {}; type = types.attrsOf types.unspecified; }; + allowApplyAll = lib.mkOption { + description = '' + Whether to allow deployments without a node filter set. + + If set to false, a node filter must be specified with `--on` when + deploying. + + It helps prevent accidental deployments to the entire cluster + when tags are used (e.g., `@production` and `@staging`). + ''; + default = true; + type = types.bool; + }; }; }; } diff --git a/src/nix/hive/tests/mod.rs b/src/nix/hive/tests/mod.rs index 37a9334..4a00a12 100644 --- a/src/nix/hive/tests/mod.rs +++ b/src/nix/hive/tests/mod.rs @@ -542,3 +542,22 @@ fn test_hive_introspect() { assert_eq!("true", eval); } + +#[test] +fn test_hive_get_meta() { + let hive = TempHive::new(r#" + { + meta.allowApplyAll = false; + meta.specialArgs = { + this_is_new = false; + }; + } + "#); + + let eval = block_on(hive.get_meta_config()) + .unwrap(); + + eprintln!("{:?}", eval); + + assert!(!eval.allow_apply_all); +} diff --git a/src/nix/mod.rs b/src/nix/mod.rs index 0be6608..0a43548 100644 --- a/src/nix/mod.rs +++ b/src/nix/mod.rs @@ -82,6 +82,15 @@ pub struct NodeConfig { keys: HashMap, } +#[derive(Debug, Clone, Validate, Deserialize)] +pub struct MetaConfig { + #[serde(rename = "allowApplyAll")] + pub allow_apply_all: bool, + + #[serde(rename = "machinesFile")] + pub machines_file: Option, +} + /// Nix options. #[derive(Debug, Clone, Default)] pub struct NixOptions {