apply: Add deployment.replaceUnknownProfiles
option and --force-replace-unknown-profiles
switch
If `deployment.replaceUnknownProfiles` is set to false, a diverged hive config (in a shared git repo for example) won't result in accidentally undoing another applied configuration profile. The deployment option is set to true so that fiction is minimized from aggressive garbage collection, first time profile application and low contention hives.
This commit is contained in:
parent
0927fe9dc1
commit
3ee97c2a76
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; [
|
environment.systemPackages = with pkgs; [
|
||||||
vim wget curl
|
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, ... }: {
|
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.
|
# environment variable to specify a ssh_config file.
|
||||||
deployment.targetPort = 1234;
|
deployment.targetPort = 1234;
|
||||||
|
|
||||||
|
# Override the default for this target host
|
||||||
|
deployment.replaceUnknownProfiles = false;
|
||||||
|
|
||||||
time.timeZone = "America/Los_Angeles";
|
time.timeZone = "America/Los_Angeles";
|
||||||
|
|
||||||
boot.loader.grub.device = "/dev/sda";
|
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")
|
.help("Do not use gzip")
|
||||||
.long_help("Disables the use of gzip when copying closures to the remote host.")
|
.long_help("Disables the use of gzip when copying closures to the remote host.")
|
||||||
.takes_value(false))
|
.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> {
|
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_gzip(!local_args.is_present("no-gzip"));
|
||||||
options.set_progress_bar(!local_args.is_present("verbose"));
|
options.set_progress_bar(!local_args.is_present("verbose"));
|
||||||
options.set_upload_keys(!local_args.is_present("no-keys"));
|
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") {
|
if local_args.is_present("keep-result") {
|
||||||
options.set_gc_roots(hive_base.join(".gcroots"));
|
options.set_gc_roots(hive_base.join(".gcroots"));
|
||||||
|
|
|
@ -10,10 +10,10 @@ use tokio::sync::{Mutex, Semaphore};
|
||||||
use super::{Hive, Host, CopyOptions, NodeConfig, Profile, StoreDerivation, ProfileMap, host};
|
use super::{Hive, Host, CopyOptions, NodeConfig, Profile, StoreDerivation, ProfileMap, host};
|
||||||
use crate::progress::{Progress, TaskProgress, OutputStyle};
|
use crate::progress::{Progress, TaskProgress, OutputStyle};
|
||||||
|
|
||||||
/// Amount of RAM reserved for the system, in MB.
|
/// Amount of RAM reserved for the system, in MB.
|
||||||
const EVAL_RESERVE_MB: u64 = 1024;
|
const EVAL_RESERVE_MB: u64 = 1024;
|
||||||
|
|
||||||
/// Estimated amount of RAM needed to evaluate one host, in MB.
|
/// Estimated amount of RAM needed to evaluate one host, in MB.
|
||||||
const EVAL_PER_HOST_MB: u64 = 512;
|
const EVAL_PER_HOST_MB: u64 = 512;
|
||||||
|
|
||||||
const BATCH_OPERATION_LABEL: &'static str = "(...)";
|
const BATCH_OPERATION_LABEL: &'static str = "(...)";
|
||||||
|
@ -493,6 +493,24 @@ impl Deployment {
|
||||||
|
|
||||||
let mut bar = multi.create_task_progress(name.to_string());
|
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() {
|
if self.options.upload_keys && !target.config.keys.is_empty() {
|
||||||
bar.log("Uploading keys...");
|
bar.log("Uploading keys...");
|
||||||
|
|
||||||
|
@ -609,6 +627,9 @@ pub struct DeploymentOptions {
|
||||||
|
|
||||||
/// Directory to create GC roots for node profiles in.
|
/// Directory to create GC roots for node profiles in.
|
||||||
gc_roots: Option<PathBuf>,
|
gc_roots: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Ignore the node-level `deployment.replaceUnknownProfiles` option.
|
||||||
|
force_replace_unknown_profiles: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DeploymentOptions {
|
impl Default for DeploymentOptions {
|
||||||
|
@ -619,6 +640,7 @@ impl Default for DeploymentOptions {
|
||||||
gzip: true,
|
gzip: true,
|
||||||
upload_keys: true,
|
upload_keys: true,
|
||||||
gc_roots: None,
|
gc_roots: None,
|
||||||
|
force_replace_unknown_profiles: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -644,6 +666,10 @@ impl DeploymentOptions {
|
||||||
self.gc_roots = Some(path.as_ref().to_owned());
|
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 {
|
fn to_copy_options(&self) -> CopyOptions {
|
||||||
let options = CopyOptions::default();
|
let options = CopyOptions::default();
|
||||||
|
|
||||||
|
|
|
@ -120,6 +120,22 @@ let
|
||||||
type = types.attrsOf keyType;
|
type = types.attrsOf keyType;
|
||||||
default = {};
|
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
|
result
|
||||||
}
|
}
|
||||||
|
async fn active_derivation_known(&mut self) -> NixResult<bool> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
fn set_progress_bar(&mut self, bar: TaskProgress) {
|
fn set_progress_bar(&mut self, bar: TaskProgress) {
|
||||||
self.progress_bar = bar;
|
self.progress_bar = bar;
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,10 @@ pub trait Host: Send + Sync + std::fmt::Debug {
|
||||||
Err(NixError::Unsupported)
|
Err(NixError::Unsupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
/// 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.
|
/// Activates a system profile on the host, if it runs NixOS.
|
||||||
///
|
///
|
||||||
/// The profile must already exist on the host. You should probably use deploy instead.
|
/// The profile must already exist on the host. You should probably use deploy instead.
|
||||||
|
|
|
@ -7,7 +7,7 @@ use async_trait::async_trait;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use super::{CopyDirection, CopyOptions, Host, key_uploader};
|
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::util::CommandExecution;
|
||||||
use crate::progress::TaskProgress;
|
use crate::progress::TaskProgress;
|
||||||
|
|
||||||
|
@ -70,6 +70,27 @@ impl Host for Ssh {
|
||||||
let command = self.ssh(&v);
|
let command = self.ssh(&v);
|
||||||
self.run_command(command).await
|
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) {
|
fn set_progress_bar(&mut self, bar: TaskProgress) {
|
||||||
self.progress_bar = bar;
|
self.progress_bar = bar;
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,9 @@ pub enum NixError {
|
||||||
#[snafu(display("Invalid NixOS system profile"))]
|
#[snafu(display("Invalid NixOS system profile"))]
|
||||||
InvalidProfile,
|
InvalidProfile,
|
||||||
|
|
||||||
|
#[snafu(display("Unknown active profile: {}", store_path))]
|
||||||
|
ActiveProfileUnknown { store_path: String },
|
||||||
|
|
||||||
#[snafu(display("Nix Error: {}", message))]
|
#[snafu(display("Nix Error: {}", message))]
|
||||||
Unknown { message: String },
|
Unknown { message: String },
|
||||||
}
|
}
|
||||||
|
@ -105,6 +108,9 @@ pub struct NodeConfig {
|
||||||
allow_local_deployment: bool,
|
allow_local_deployment: bool,
|
||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(rename = "replaceUnknownProfiles")]
|
||||||
|
replace_unknown_profiles: bool,
|
||||||
|
|
||||||
#[validate(custom = "validate_keys")]
|
#[validate(custom = "validate_keys")]
|
||||||
keys: HashMap<String, Key>,
|
keys: HashMap<String, Key>,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue