Merge pull request #20 from jasonrm/unknown-profiles
Warn on unknown remote profiles replacement, error out if node-level option is explicitly enabled
This commit is contained in:
commit
a165520076
8 changed files with 99 additions and 4 deletions
13
README.md
13
README.md
|
@ -61,6 +61,16 @@ Here is a sample `hive.nix` with two nodes, with some common configurations appl
|
|||
environment.systemPackages = with pkgs; [
|
||||
vim wget curl
|
||||
];
|
||||
|
||||
# By default, Colmena will replace unknown remote profile
|
||||
# (unknown means the profile isn't in the nix store on the
|
||||
# host running Colmena) during apply (with the default goal,
|
||||
# boot, and switch).
|
||||
# If you share a hive with others, or use multiple machines,
|
||||
# and are not careful to always commit/push/pull changes
|
||||
# you can accidentaly overwrite a remote profile so in those
|
||||
# scenarios you might want to change this default to false.
|
||||
# deployment.replaceUnknownProfiles = true;
|
||||
};
|
||||
|
||||
host-a = { name, nodes, ... }: {
|
||||
|
@ -87,6 +97,9 @@ Here is a sample `hive.nix` with two nodes, with some common configurations appl
|
|||
# environment variable to specify a ssh_config file.
|
||||
deployment.targetPort = 1234;
|
||||
|
||||
# Override the default for this target host
|
||||
deployment.replaceUnknownProfiles = false;
|
||||
|
||||
time.timeZone = "America/Los_Angeles";
|
||||
|
||||
boot.loader.grub.device = "/dev/sda";
|
||||
|
|
|
@ -91,6 +91,12 @@ To upload keys without building or deploying the rest of the configuration, use
|
|||
.help("Do not use gzip")
|
||||
.long_help("Disables the use of gzip when copying closures to the remote host.")
|
||||
.takes_value(false))
|
||||
.arg(Arg::with_name("force-replace-unknown-profiles")
|
||||
.long("force-replace-unknown-profiles")
|
||||
.help("Ignore all targeted nodes deployment.replaceUnknownProfiles setting")
|
||||
.long_help(r#"If `deployment.replaceUnknownProfiles` is set for a target, using this switch
|
||||
will treat deployment.replaceUnknownProfiles as though it was set true and perform unknown profile replacement."#)
|
||||
.takes_value(false))
|
||||
}
|
||||
|
||||
pub fn subcommand() -> App<'static, 'static> {
|
||||
|
@ -186,6 +192,7 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
|
|||
options.set_gzip(!local_args.is_present("no-gzip"));
|
||||
options.set_progress_bar(!local_args.is_present("verbose"));
|
||||
options.set_upload_keys(!local_args.is_present("no-keys"));
|
||||
options.set_force_replace_unknown_profiles(local_args.is_present("force-replace-unknown-profiles"));
|
||||
|
||||
if local_args.is_present("keep-result") {
|
||||
options.set_gc_roots(hive_base.join(".gcroots"));
|
||||
|
|
|
@ -493,6 +493,24 @@ impl Deployment {
|
|||
|
||||
let mut bar = multi.create_task_progress(name.to_string());
|
||||
|
||||
// FIXME: Would be nicer to check remote status before spending time evaluating/building
|
||||
if !target.config.replace_unknown_profiles {
|
||||
bar.log("Checking remote profile...");
|
||||
match target.host.active_derivation_known().await {
|
||||
Ok(_) => {
|
||||
bar.log("Remote profile known");
|
||||
}
|
||||
Err(e) => {
|
||||
if self.options.force_replace_unknown_profiles {
|
||||
bar.log("warning: remote profile is unknown, but unknown profiles are being ignored");
|
||||
} else {
|
||||
bar.failure(&format!("Failed: {}", e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.options.upload_keys && !target.config.keys.is_empty() {
|
||||
bar.log("Uploading keys...");
|
||||
|
||||
|
@ -609,6 +627,9 @@ pub struct DeploymentOptions {
|
|||
|
||||
/// Directory to create GC roots for node profiles in.
|
||||
gc_roots: Option<PathBuf>,
|
||||
|
||||
/// Ignore the node-level `deployment.replaceUnknownProfiles` option.
|
||||
force_replace_unknown_profiles: bool,
|
||||
}
|
||||
|
||||
impl Default for DeploymentOptions {
|
||||
|
@ -619,6 +640,7 @@ impl Default for DeploymentOptions {
|
|||
gzip: true,
|
||||
upload_keys: true,
|
||||
gc_roots: None,
|
||||
force_replace_unknown_profiles: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -644,6 +666,10 @@ impl DeploymentOptions {
|
|||
self.gc_roots = Some(path.as_ref().to_owned());
|
||||
}
|
||||
|
||||
pub fn set_force_replace_unknown_profiles(&mut self, enable: bool) {
|
||||
self.force_replace_unknown_profiles = enable;
|
||||
}
|
||||
|
||||
fn to_copy_options(&self) -> CopyOptions {
|
||||
let options = CopyOptions::default();
|
||||
|
||||
|
|
|
@ -120,6 +120,22 @@ let
|
|||
type = types.attrsOf keyType;
|
||||
default = {};
|
||||
};
|
||||
replaceUnknownProfiles = lib.mkOption {
|
||||
description = ''
|
||||
Allow a configuration to be applied to a host running a profile we
|
||||
have no knowledge of. By setting this option to false, you reduce
|
||||
the likelyhood of rolling back changes made via another Colmena user.
|
||||
|
||||
Unknown profiles are usually the result of either:
|
||||
- The node had a profile applied, locally or by another Colmena.
|
||||
- The host running Colmena garbage-collecting the profile.
|
||||
|
||||
To force profile replacement on all targeted nodes during apply,
|
||||
use the flag `--force-replace-unknown-profiles`.
|
||||
'';
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -92,6 +92,9 @@ impl Host for Local {
|
|||
|
||||
result
|
||||
}
|
||||
async fn active_derivation_known(&mut self) -> NixResult<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
fn set_progress_bar(&mut self, bar: TaskProgress) {
|
||||
self.progress_bar = bar;
|
||||
}
|
||||
|
|
|
@ -102,6 +102,9 @@ pub trait Host: Send + Sync + std::fmt::Debug {
|
|||
Err(NixError::Unsupported)
|
||||
}
|
||||
|
||||
/// Check if the active profile is known to the host running Colmena
|
||||
async fn active_derivation_known(&mut self) -> NixResult<bool>;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
/// Activates a system profile on the host, if it runs NixOS.
|
||||
///
|
||||
|
|
|
@ -7,7 +7,7 @@ use async_trait::async_trait;
|
|||
use tokio::process::Command;
|
||||
|
||||
use super::{CopyDirection, CopyOptions, Host, key_uploader};
|
||||
use crate::nix::{StorePath, Profile, Goal, NixResult, NixCommand, Key, SYSTEM_PROFILE};
|
||||
use crate::nix::{StorePath, Profile, Goal, NixResult, NixCommand, NixError, Key, SYSTEM_PROFILE};
|
||||
use crate::util::CommandExecution;
|
||||
use crate::progress::TaskProgress;
|
||||
|
||||
|
@ -70,6 +70,27 @@ impl Host for Ssh {
|
|||
let command = self.ssh(&v);
|
||||
self.run_command(command).await
|
||||
}
|
||||
async fn active_derivation_known(&mut self) -> NixResult<bool> {
|
||||
let paths = self.ssh(&["realpath", SYSTEM_PROFILE])
|
||||
.capture_output()
|
||||
.await;
|
||||
|
||||
match paths {
|
||||
Ok(paths) => {
|
||||
for path in paths.lines().into_iter() {
|
||||
let remote_profile: StorePath = path.to_string().try_into().unwrap();
|
||||
if remote_profile.exists() {
|
||||
return Ok(true);
|
||||
}
|
||||
return Err(NixError::ActiveProfileUnknown {
|
||||
store_path: path.to_string(),
|
||||
});
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
fn set_progress_bar(&mut self, bar: TaskProgress) {
|
||||
self.progress_bar = bar;
|
||||
}
|
||||
|
|
|
@ -68,6 +68,9 @@ pub enum NixError {
|
|||
#[snafu(display("Invalid NixOS system profile"))]
|
||||
InvalidProfile,
|
||||
|
||||
#[snafu(display("Unknown active profile: {}", store_path))]
|
||||
ActiveProfileUnknown { store_path: String },
|
||||
|
||||
#[snafu(display("Nix Error: {}", message))]
|
||||
Unknown { message: String },
|
||||
}
|
||||
|
@ -105,6 +108,9 @@ pub struct NodeConfig {
|
|||
allow_local_deployment: bool,
|
||||
tags: Vec<String>,
|
||||
|
||||
#[serde(rename = "replaceUnknownProfiles")]
|
||||
replace_unknown_profiles: bool,
|
||||
|
||||
#[validate(custom = "validate_keys")]
|
||||
keys: HashMap<String, Key>,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue