diff --git a/Release.toml b/Release.toml index 2e58fff40ab..7042693af2f 100644 --- a/Release.toml +++ b/Release.toml @@ -278,4 +278,6 @@ version = "1.19.3" "(1.19.2, 1.19.3)" = [ "migrate_v1.19.3_prairiedog-config-file-v0-1-0.lz4", "migrate_v1.19.3_prairiedog-services-cfg-v0-1-0.lz4", + "migrate_v1.19.3_thar-be-updates-config-file-v0-1-0.lz4", + "migrate_v1.19.3_thar-be-updates-affected-services-v0-1-0.lz4", ] diff --git a/packages/os/os.spec b/packages/os/os.spec index ea949aca563..38e08640059 100644 --- a/packages/os/os.spec +++ b/packages/os/os.spec @@ -33,6 +33,7 @@ Source13: cis-checks-k8s-metadata-json %endif Source14: certdog-toml Source15: prairiedog-toml +Source16: thar-be-updates-toml # 1xx sources: systemd units Source100: apiserver.service @@ -474,7 +475,7 @@ install -d %{buildroot}%{_cross_datadir}/updog install -p -m 0644 %{_cross_repo_root_json} %{buildroot}%{_cross_datadir}/updog install -d %{buildroot}%{_cross_templatedir} -install -p -m 0644 %{S:5} %{S:6} %{S:7} %{S:8} %{S:14} %{S:15} %{buildroot}%{_cross_templatedir} +install -p -m 0644 %{S:5} %{S:6} %{S:7} %{S:8} %{S:14} %{S:15} %{S:16} %{buildroot}%{_cross_templatedir} install -d %{buildroot}%{_cross_unitdir} install -p -m 0644 \ @@ -585,6 +586,7 @@ install -p -m 0644 %{S:400} %{S:401} %{S:402} %{buildroot}%{_cross_licensedir} %files -n %{_cross_os}thar-be-updates %{_cross_bindir}/thar-be-updates %{_cross_tmpfilesdir}/thar-be-updates.conf +%{_cross_templatedir}/thar-be-updates-toml %files -n %{_cross_os}host-containers %{_cross_bindir}/host-containers diff --git a/packages/os/thar-be-updates-toml b/packages/os/thar-be-updates-toml new file mode 100644 index 00000000000..d3f6abb37d2 --- /dev/null +++ b/packages/os/thar-be-updates-toml @@ -0,0 +1,5 @@ +[required-extensions] +updates = "v1" +std = { version = "v1", helpers = ["default"] } ++++ +version-lock = "{{{default "latest" settings.updates.version-lock}}}" diff --git a/sources/Cargo.lock b/sources/Cargo.lock index d847ec26335..c5f0c42f544 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -4094,15 +4094,12 @@ dependencies = [ name = "thar-be-updates" version = "0.1.0" dependencies = [ - "apiclient", "bottlerocket-release", "chrono", - "constants", "fs2", "generate-readme", - "http", "log", - "models", + "modeled-types", "nix", "num-derive", "num-traits", @@ -4114,10 +4111,24 @@ dependencies = [ "simplelog", "snafu", "tempfile", - "tokio", + "toml 0.8.8", "update_metadata", ] +[[package]] +name = "thar-be-updates-affected-services-v0-1-0" +version = "0.1.0" +dependencies = [ + "migration-helpers", +] + +[[package]] +name = "thar-be-updates-config-file-v0-1-0" +version = "0.1.0" +dependencies = [ + "migration-helpers", +] + [[package]] name = "thiserror" version = "1.0.56" diff --git a/sources/Cargo.toml b/sources/Cargo.toml index 4bc7cdfb0a7..4273fd1ba37 100644 --- a/sources/Cargo.toml +++ b/sources/Cargo.toml @@ -66,6 +66,8 @@ members = [ "api/migration/migrations/v1.19.2/add-ecs-enable-container-metadata", "api/migration/migrations/v1.19.3/prairiedog-config-file-v0-1-0", "api/migration/migrations/v1.19.3/prairiedog-services-cfg-v0-1-0", + "api/migration/migrations/v1.19.3/thar-be-updates-config-file-v0-1-0", + "api/migration/migrations/v1.19.3/thar-be-updates-affected-services-v0-1-0", "bloodhound", diff --git a/sources/api/migration/migrations/v1.19.3/thar-be-updates-affected-services-v0-1-0/Cargo.toml b/sources/api/migration/migrations/v1.19.3/thar-be-updates-affected-services-v0-1-0/Cargo.toml new file mode 100644 index 00000000000..c9c247791d3 --- /dev/null +++ b/sources/api/migration/migrations/v1.19.3/thar-be-updates-affected-services-v0-1-0/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "thar-be-updates-affected-services-v0-1-0" +version = "0.1.0" +authors = ["Jarrett Tierney "] +license = "Apache-2.0 OR MIT" +edition = "2021" +publish = false +exclude = ["README.md"] + +[dependencies] +migration-helpers = { path = "../../../migration-helpers", version = "0.1.0" } diff --git a/sources/api/migration/migrations/v1.19.3/thar-be-updates-affected-services-v0-1-0/src/main.rs b/sources/api/migration/migrations/v1.19.3/thar-be-updates-affected-services-v0-1-0/src/main.rs new file mode 100644 index 00000000000..31ae155ce84 --- /dev/null +++ b/sources/api/migration/migrations/v1.19.3/thar-be-updates-affected-services-v0-1-0/src/main.rs @@ -0,0 +1,21 @@ +use migration_helpers::common_migrations::{ + MetadataListReplacement, ReplaceMetadataListsMigration, +}; +use migration_helpers::{migrate, Result}; +use std::process; +fn run() -> Result<()> { + migrate(ReplaceMetadataListsMigration(vec![ + MetadataListReplacement { + setting: "settings.updates", + metadata: "affected-services", + old_vals: &["updog"], + new_vals: &["updog", "thar-be-updates"], + }, + ])) +} +fn main() { + if let Err(e) = run() { + eprintln!("{}", e); + process::exit(1); + } +} diff --git a/sources/api/migration/migrations/v1.19.3/thar-be-updates-config-file-v0-1-0/Cargo.toml b/sources/api/migration/migrations/v1.19.3/thar-be-updates-config-file-v0-1-0/Cargo.toml new file mode 100644 index 00000000000..b164d18a114 --- /dev/null +++ b/sources/api/migration/migrations/v1.19.3/thar-be-updates-config-file-v0-1-0/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "thar-be-updates-config-file-v0-1-0" +version = "0.1.0" +edition = "2021" +authors = ["Jarrett Tierney "] +license = "Apache-2.0 OR MIT" +publish = false +exclude = ["README.md"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +migration-helpers = { path = "../../../migration-helpers", version = "0.1.0" } diff --git a/sources/api/migration/migrations/v1.19.3/thar-be-updates-config-file-v0-1-0/src/main.rs b/sources/api/migration/migrations/v1.19.3/thar-be-updates-config-file-v0-1-0/src/main.rs new file mode 100644 index 00000000000..96101e6f516 --- /dev/null +++ b/sources/api/migration/migrations/v1.19.3/thar-be-updates-config-file-v0-1-0/src/main.rs @@ -0,0 +1,17 @@ +use migration_helpers::common_migrations::AddPrefixesMigration; +use migration_helpers::{migrate, Result}; +use std::process; + +fn run() -> Result<()> { + migrate(AddPrefixesMigration(vec![ + "configuration-files.thar-be-updates-toml", + "services.thar-be-updates", + ])) +} + +fn main() { + if let Err(e) = run() { + eprintln!("{}", e); + process::exit(1); + } +} diff --git a/sources/api/thar-be-updates/Cargo.toml b/sources/api/thar-be-updates/Cargo.toml index 14875e261d1..c91746d25ef 100644 --- a/sources/api/thar-be-updates/Cargo.toml +++ b/sources/api/thar-be-updates/Cargo.toml @@ -10,14 +10,11 @@ build = "build.rs" exclude = ["README.md"] [dependencies] -apiclient = { path = "../apiclient", version = "0.1" } -constants = { path = "../../constants", version = "0.1" } bottlerocket-release = { path = "../../bottlerocket-release", version = "0.1" } chrono = { version = "0.4", default-features = false, features = ["std", "serde", "clock"] } fs2 = "0.4" -http = "0.2" log = "0.4" -models = { path = "../../models", version = "0.1" } +modeled-types = { path = "../../models/modeled-types", version = "0.1" } nix = "0.26" num-derive = "0.3" num-traits = "0.2" @@ -29,7 +26,7 @@ signpost = { path = "../../updater/signpost", version = "0.1" } simplelog = "0.12" snafu = "0.7" tempfile = "3" -tokio = { version = "~1.32", default-features = false, features = ["macros", "rt-multi-thread"] } # LTS +toml = "0.8" update_metadata = { path = "../../updater/update_metadata", version = "0.1" } [build-dependencies] diff --git a/sources/api/thar-be-updates/src/error.rs b/sources/api/thar-be-updates/src/error.rs index 3e22a1cba4c..9ba623af7db 100644 --- a/sources/api/thar-be-updates/src/error.rs +++ b/sources/api/thar-be-updates/src/error.rs @@ -1,5 +1,4 @@ use crate::status::{UpdateCommand, UpdateState}; -use http::StatusCode; use num_derive::{FromPrimitive, ToPrimitive}; use snafu::Snafu; use std::path::PathBuf; @@ -54,20 +53,16 @@ pub enum Error { source: std::io::Error, }, - #[snafu(display("Error sending {} to {}: {}", method, uri, source))] - APIRequest { - method: String, - uri: String, - #[snafu(source(from(apiclient::Error, Box::new)))] - source: Box, + #[snafu(display("Error reading configuration file at {}: {}", path.display(), source))] + ReadConfig { + path: PathBuf, + source: std::io::Error, }, - #[snafu(display("Error {} when sending {} to {}: {}", code, method, uri, response_body))] - APIResponse { - method: String, - uri: String, - code: StatusCode, - response_body: String, + #[snafu(display("Error deserializing configuration at {}: {}", path.display(), source))] + Deserialization { + path: PathBuf, + source: toml::de::Error, }, #[snafu(display("Error deserializing response as JSON from {}: {}", uri, source))] @@ -120,6 +115,9 @@ pub enum Error { source: serde_json::Error, }, + #[snafu(display("Failed to get required setting '{}'", setting))] + GetSettingOption { setting: String }, + #[snafu(display("Failed to parse version string '{}' into semver version", version))] SemVer { version: String, @@ -149,9 +147,6 @@ pub enum Error { #[snafu(display("Logger setup error: {}", source))] Logger { source: log::SetLoggerError }, - - #[snafu(display("Unable to create a tokio runtime: {}", source))] - Runtime { source: std::io::Error }, } /// Map errors to specific exit codes to return to caller diff --git a/sources/api/thar-be-updates/src/main.rs b/sources/api/thar-be-updates/src/main.rs index e45a3473426..fd2a41fe393 100644 --- a/sources/api/thar-be-updates/src/main.rs +++ b/sources/api/thar-be-updates/src/main.rs @@ -24,7 +24,7 @@ use simplelog::{Config as LogConfig, LevelFilter, SimpleLogger}; use snafu::ensure; use snafu::{OptionExt, ResultExt}; use std::fs::File; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::{exit, Command}; use std::str::FromStr; use std::{env, process}; @@ -37,12 +37,13 @@ use thar_be_updates::status::{ }; const UPDATE_STATUS_DIR: &str = "/run/cache/thar-be-updates"; +const DEFAULT_CONFIG_FILE: &str = "/etc/thar-be-updates.toml"; /// Stores the command line arguments struct Args { subcommand: UpdateCommand, log_level: LevelFilter, - socket_path: String, + config_path: PathBuf, } /// Prints an usage message @@ -59,10 +60,9 @@ fn usage() -> ! { deactivate Reverts update activation by marking current active partition for boot Global options: - [ --socket-path PATH ] Bottlerocket API socket path (default {}) + [ --config-path PATH ] configuration file (default {}) [ --log-level trace|debug|info|warn|error ] (default info)", - program_name, - constants::API_SOCKET, + program_name, DEFAULT_CONFIG_FILE, ); process::exit(2); } @@ -77,7 +77,7 @@ fn usage_msg>(msg: S) -> ! { fn parse_args(args: std::env::Args) -> Args { let mut subcommand = None; let mut log_level = None; - let mut socket_path = None; + let mut config_path = None; let mut iter = args.skip(1).peekable(); while let Some(arg) = iter.next() { @@ -91,11 +91,11 @@ fn parse_args(args: std::env::Args) -> Args { })); } - "--socket-path" => { - socket_path = Some( - iter.next() - .unwrap_or_else(|| usage_msg("Did not give argument to --socket-path")), - ) + "--config-path" => { + config_path = + Some(PathBuf::from(iter.next().unwrap_or_else(|| { + usage_msg("Did not give argument to --config-path") + }))) } // Assume any arguments not prefixed with '-' is a subcommand s if !s.starts_with('-') => { @@ -112,7 +112,7 @@ fn parse_args(args: std::env::Args) -> Args { Args { subcommand: subcommand.unwrap_or_else(|| usage()), log_level: log_level.unwrap_or(LevelFilter::Info), - socket_path: socket_path.unwrap_or_else(|| constants::API_SOCKET.to_string()), + config_path: config_path.unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_FILE)), } } @@ -180,7 +180,10 @@ macro_rules! fork_and_return { /// Spawns updog process to get list of updates and check if any of them can be updated to. /// Returns true if there is an available update, returns false otherwise. -fn refresh(status: &mut UpdateStatus, socket_path: &str) -> Result { +fn refresh

(status: &mut UpdateStatus, config_path: P) -> Result +where + P: AsRef, +{ fork_and_return!({ debug!("Spawning 'updog whats'"); let output = Command::new("updog") @@ -194,7 +197,7 @@ fn refresh(status: &mut UpdateStatus, socket_path: &str) -> Result { } let update_info: Vec = serde_json::from_slice(&output.stdout).context(error::UpdateInfoSnafu)?; - status.update_available_updates(socket_path, update_info) + status.update_available_updates(config_path, update_info) }) } @@ -255,15 +258,18 @@ fn deactivate(status: &mut UpdateStatus) -> Result<()> { } /// Given the update command, this drives the update state machine. -fn drive_state_machine( +fn drive_state_machine

( update_status: &mut UpdateStatus, operation: &UpdateCommand, - socket_path: &str, -) -> Result<()> { + config_path: P, +) -> Result<()> +where + P: AsRef, +{ let new_state = match (operation, update_status.update_state()) { (UpdateCommand::Refresh, UpdateState::Idle) | (UpdateCommand::Refresh, UpdateState::Available) => { - if refresh(update_status, socket_path)? { + if refresh(update_status, config_path)? { // Transitions state to `Available` if there is an available update UpdateState::Available } else { @@ -273,7 +279,7 @@ fn drive_state_machine( } // Refreshing the list of updates is allowed under every update state (UpdateCommand::Refresh, _) => { - refresh(update_status, socket_path)?; + refresh(update_status, config_path)?; // No need to transition state here as we're already beyond `Available` update_status.update_state().to_owned() } @@ -348,7 +354,7 @@ fn run() -> Result<()> { // The commands inside drive_state_machine update the update_status object (hence &mut) to // reflect success or failure, and we want to reflect that in our status file regardless of // success, so we store the result rather than returning early here. - let result = drive_state_machine(&mut update_status, &args.subcommand, &args.socket_path); + let result = drive_state_machine(&mut update_status, &args.subcommand, &args.config_path); write_update_status(&update_status)?; result } diff --git a/sources/api/thar-be-updates/src/status.rs b/sources/api/thar-be-updates/src/status.rs index 2a23b8881d8..47c0e93fb24 100644 --- a/sources/api/thar-be-updates/src/status.rs +++ b/sources/api/thar-be-updates/src/status.rs @@ -2,19 +2,28 @@ use crate::error; use crate::error::Result; use bottlerocket_release::BottlerocketRelease; use chrono::{DateTime, Utc}; -use model::modeled_types::FriendlyVersion; +use modeled_types::FriendlyVersion; use serde::{Deserialize, Serialize}; use signpost::State; use snafu::{OptionExt, ResultExt}; use std::convert::TryInto; +use std::fs; use std::fs::File; use std::os::unix::process::ExitStatusExt; +use std::path::Path; use std::process::Output; -use tokio::runtime::Runtime; pub const UPDATE_LOCKFILE: &str = "/run/lock/thar-be-updates.lock"; pub const UPDATE_STATUS_FILE: &str = "/run/cache/thar-be-updates/status.json"; +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +struct UpdatesSettings { + // Version to update to when updating via the API. + #[serde(default, skip_serializing_if = "Option::is_none")] + version_lock: Option, +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub enum UpdateState { Idle, @@ -105,23 +114,19 @@ pub fn get_update_status(_lockfile: &File) -> Result { }) } -/// Retrieves settings from the API. +/// Retrieves settings from the configuration file. /// -/// NOTE: this function creates its own tokio runtime to make the async apiclient call. It should -/// not be called if you're running another tokio runtime. The program structure requires forking -/// to handle long-running update actions, and the tokio runtime uses threading, which generally -/// isn't safe over forks; instead, we create and drop one here for the short period we need it. -fn get_settings(socket_path: &str) -> Result { - let uri = "/settings"; - let method = "GET"; - - let rt = Runtime::new().context(error::RuntimeSnafu)?; - let try_response_body = - rt.block_on(async { apiclient::raw_request(&socket_path, uri, method, None).await }); - let (_code, response_body) = - try_response_body.context(error::APIRequestSnafu { method, uri })?; - - serde_json::from_str(&response_body).context(error::ResponseJsonSnafu { uri }) +fn get_settings

(config_path: P) -> Result +where + P: AsRef, +{ + let config_path = config_path.as_ref(); + let config_str = fs::read_to_string(config_path).context(error::ReadConfigSnafu { + path: config_path.to_path_buf(), + })?; + toml::from_str(&config_str).context(error::DeserializationSnafu { + path: config_path.to_path_buf(), + }) } // This is how the UpdateStatus is stored on disk @@ -260,22 +265,25 @@ impl UpdateStatus { /// Checks the list of updates to for an available update. /// If the 'version-lock'ed version is available returns true. Otherwise returns false - pub fn update_available_updates( + pub fn update_available_updates

( &mut self, - socket_path: &str, + config_path: P, updates: Vec, - ) -> Result { + ) -> Result + where + P: AsRef, + { // Extract the version to store self.available_updates = updates.iter().map(|u| u.version.to_owned()).collect(); // Check if the 'version-lock'ed update is available as the 'chosen' update // Retrieve the 'version-lock' setting - let settings = get_settings(socket_path)?; - let locked_version: FriendlyVersion = serde_json::from_value( - settings["updates"]["version-lock"].to_owned(), - ) - .context(error::GetSettingSnafu { - setting: "/settings/updates/version-lock", - })?; + let settings = get_settings(config_path)?; + let locked_version: FriendlyVersion = + settings + .version_lock + .context(error::GetSettingOptionSnafu { + setting: "version-lock", + })?; if locked_version == "latest" { // Set chosen_update to the latest version available diff --git a/sources/models/shared-defaults/defaults.toml b/sources/models/shared-defaults/defaults.toml index 9f72f058b32..4bf314705c5 100644 --- a/sources/models/shared-defaults/defaults.toml +++ b/sources/models/shared-defaults/defaults.toml @@ -46,16 +46,24 @@ restart-commands = ["/bin/systemctl try-restart host-containerd.service"] version-lock = "latest" ignore-waves = false +[services.thar-be-updates] +configuration-files = ["thar-be-updates-toml"] +restart-commands = [] + [services.updog] configuration-files = ["updog-toml"] restart-commands = [] +[configuration-files.thar-be-updates-toml] +path = "/etc/thar-be-updates.toml" +template-path = "/usr/share/templates/thar-be-updates-toml" + [configuration-files.updog-toml] path = "/etc/updog.toml" template-path = "/usr/share/templates/updog-toml" [metadata.settings.updates] -affected-services = ["updog"] +affected-services = ["updog", "thar-be-updates"] seed.setting-generator = "bork seed" # HostContainers