diff --git a/Jenkinsfile b/Jenkinsfile index 8526906376c..daf5fed5778 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -154,7 +154,7 @@ pipeline { sh script: 'typos', label: 'check webapp api doc typos' } dir('relay') { - sh script: 'typos --exclude "*.asc" --exclude "*.pem" --exclude "*.cert" --exclude "*.priv" --exclude "*.pub" --exclude "*.signed" --exclude "*.log" --exclude "*.json"', label: 'check relayd typos' + sh script: 'typos --exclude "*.license" --exclude "*.asc" --exclude "*.pem" --exclude "*.cert" --exclude "*.priv" --exclude "*.pub" --exclude "*.signed" --exclude "*.log" --exclude "*.json"', label: 'check relayd typos' } } } diff --git a/relay/sources/Makefile b/relay/sources/Makefile index ec15f34c40a..c17add396c3 100644 --- a/relay/sources/Makefile +++ b/relay/sources/Makefile @@ -118,9 +118,10 @@ install: build # rudder-package new implementation install -m 755 rudder-package/target/release/rudder-package $(DESTDIR)/opt/rudder/bin/rudder-package install -m 755 rudder-package/tools/rudder_plugins_key.gpg $(DESTDIR)/opt/rudder/etc/rudder-pkg/rudder_plugins_key.gpg + install -m 755 rudder-package/tools/rudder-package.sh $(DESTDIR)/opt/rudder/share/commands/package install -m 755 rudder-pkg/rudder-pkg.conf $(DESTDIR)/opt/rudder/etc/rudder-pkg/rudder-pkg.conf # rudder-pkg kept as backup for now - install -m 755 rudder-pkg/rudder-pkg $(DESTDIR)/opt/rudder/share/commands/package + install -m 755 rudder-pkg/rudder-pkg $(DESTDIR)/opt/rudder/share/python/rudder-pkg/rudder-pkg ln -ns ../share/commands/package $(DESTDIR)/opt/rudder/bin/rudder-pkg install -m 755 rudder-pkg/rudder_plugins_key.pub $(DESTDIR)/opt/rudder/etc/rudder-pkg/rudder_plugins_key.pub install -m 644 autocomplete/rudder-pkg.sh $(DESTDIR)/etc/bash_completion.d/ diff --git a/relay/sources/rudder-package/Cargo.lock b/relay/sources/rudder-package/Cargo.lock index 3c21a712b0c..dcf7c1eeb23 100644 --- a/relay/sources/rudder-package/Cargo.lock +++ b/relay/sources/rudder-package/Cargo.lock @@ -214,7 +214,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.39", ] [[package]] @@ -223,6 +223,29 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "cli-table" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adfbb116d9e2c4be7011360d0c0bee565712c11e969c9609b25b619366dc379d" +dependencies = [ + "cli-table-derive", + "csv", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cli-table-derive" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af3bfb9da627b0a6c467624fb7963921433774ed435493b5c08a3053e829ad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -288,6 +311,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "diff" version = "0.1.13" @@ -408,9 +452,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -471,7 +515,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.39", ] [[package]] @@ -522,9 +566,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -657,9 +701,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -741,6 +785,12 @@ dependencies = [ "crc", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "memchr" version = "2.6.4" @@ -818,9 +868,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -839,7 +889,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.39", ] [[package]] @@ -850,9 +900,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" dependencies = [ "cc", "libc", @@ -862,9 +912,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" @@ -1043,7 +1093,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.39", "unicode-ident", ] @@ -1056,6 +1106,7 @@ dependencies = [ "assert-json-diff", "base16ct", "clap", + "cli-table", "dir-diff", "env_logger", "flate2", @@ -1071,6 +1122,7 @@ dependencies = [ "serde_json", "serde_toml", "sha2", + "spinners", "tar", "tempfile", "which", @@ -1104,6 +1156,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -1159,22 +1217,22 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.39", ] [[package]] @@ -1257,12 +1315,56 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "spinners" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ef947f358b9c238923f764c72a4a9d42f2d637c46e059dbd319d6e7cfb4f82" +dependencies = [ + "lazy_static", + "maplit", + "strum", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.39" @@ -1441,11 +1543,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -1522,7 +1630,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.39", "wasm-bindgen-shared", ] @@ -1556,7 +1664,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/relay/sources/rudder-package/Cargo.toml b/relay/sources/rudder-package/Cargo.toml index a4784131237..569342a76ad 100644 --- a/relay/sources/rudder-package/Cargo.toml +++ b/relay/sources/rudder-package/Cargo.toml @@ -33,6 +33,8 @@ tar = "0.4.40" tempfile = "3.8.0" which = "5.0.0" flate2 = "1.0.28" +cli-table = "0.4.7" +spinners = "4.1.1" [profile.dev] # Disabling debug info speeds up builds a bunch, diff --git a/relay/sources/rudder-package/Dockerfile b/relay/sources/rudder-package/Dockerfile index 60a5ecca684..cfe7be1283b 100644 --- a/relay/sources/rudder-package/Dockerfile +++ b/relay/sources/rudder-package/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.72.0-bullseye +FROM rust:1.74.0-bullseye LABEL ci=rudder/relay/sources/rudder-package/Dockerfile ARG USER_ID=1000 diff --git a/relay/sources/rudder-package/rust-toolchain.toml b/relay/sources/rudder-package/rust-toolchain.toml index 92115a339a6..639f4f17d95 100644 --- a/relay/sources/rudder-package/rust-toolchain.toml +++ b/relay/sources/rudder-package/rust-toolchain.toml @@ -1,3 +1,2 @@ [toolchain] -channel = "1.72.0" - +channel = "1.74.0" diff --git a/relay/sources/rudder-package/src/archive.rs b/relay/sources/rudder-package/src/archive.rs index c7f47d28774..8a76b8e43e5 100644 --- a/relay/sources/rudder-package/src/archive.rs +++ b/relay/sources/rudder-package/src/archive.rs @@ -1,23 +1,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2023 Normation SAS -use anyhow::{anyhow, bail, Context, Ok, Result}; -use ar::Archive; use core::fmt; -use log::debug; -use serde::{Deserialize, Serialize}; use std::{ fs::{self, *}, io::{Cursor, Read}, path::PathBuf, }; +use anyhow::{anyhow, bail, Context, Ok, Result}; +use ar::Archive; +use log::{debug, info}; +use serde::{Deserialize, Serialize}; + use crate::{ database::{Database, InstalledPlugin}, plugin::Metadata, - versions::RudderVersion, webapp::Webapp, - PACKAGES_DATABASE_PATH, PACKAGES_FOLDER, RUDDER_VERSION_PATH, + PACKAGES_FOLDER, }; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)] @@ -72,17 +72,16 @@ impl fmt::Display for PackageScriptArg { #[derive(Clone)] pub struct Rpkg { - pub path: String, + pub path: PathBuf, pub metadata: Metadata, } impl Rpkg { pub fn from_path(path: &str) -> Result { - let r = Rpkg { - path: String::from(path), - metadata: read_metadata(path).unwrap(), - }; - Ok(r) + Ok(Self { + path: PathBuf::from(path), + metadata: read_metadata(path)?, + }) } fn get_txz_dst(&self, txz_name: &str) -> PathBuf { @@ -156,8 +155,9 @@ impl Rpkg { fn unpack_embedded_txz(&self, txz_name: &str, dst_path: PathBuf) -> Result<(), anyhow::Error> { debug!( - "Extracting archive '{}' in folder '{:?}'", - txz_name, dst_path + "Extracting archive '{}' in folder '{}'", + txz_name, + dst_path.display() ); // Loop over ar archive files let mut archive = Archive::new(File::open(self.path.clone()).unwrap()); @@ -183,23 +183,22 @@ impl Rpkg { Ok(()) } - pub fn is_installed(&self) -> Result { - let current_database = Database::read(PACKAGES_DATABASE_PATH)?; - Ok(current_database.is_installed(self.to_owned())) + pub fn is_installed(&self, db: &Database) -> bool { + db.is_installed(self) } - pub fn install(&self, force: bool, webapp: &mut Webapp) -> Result<()> { - debug!("Installing rpkg '{}'...", self.path); + pub fn install(&self, force: bool, db: &mut Database, webapp: &mut Webapp) -> Result<()> { + info!("Installing rpkg '{}'...", self.path.display()); + let is_upgrade = self.is_installed(db); // Verify webapp compatibility - let webapp_version = RudderVersion::from_path(RUDDER_VERSION_PATH)?; if !(force || self .metadata .version .rudder_version - .is_compatible(&webapp_version.to_string())) + .is_compatible(&webapp.version)) { - bail!("This plugin was built for a Rudder '{}', it is incompatible with your current webapp version '{}'.", self.metadata.version.rudder_version, webapp_version) + bail!("This plugin was built for a Rudder '{}', it is incompatible with your current webapp version '{}'.", self.metadata.version.rudder_version, webapp.version) } // Verify that dependencies are installed if let Some(d) = &self.metadata.depends { @@ -210,9 +209,19 @@ impl Rpkg { // Extract package scripts self.unpack_embedded_txz("script.txz", PathBuf::from(PACKAGES_FOLDER))?; // Run preinst if any - let install_or_upgrade: PackageScriptArg = PackageScriptArg::Install; + let arg = if is_upgrade { + PackageScriptArg::Upgrade + } else { + PackageScriptArg::Install + }; self.metadata - .run_package_script(PackageScript::Preinst, install_or_upgrade)?; + .run_package_script(PackageScript::Preinst, arg)?; + + if is_upgrade { + // First uninstall old version, but without running prerm/portrm scripts + db.uninstall(&self.metadata.name, false, webapp)?; + } + // Extract archive content let keys = self.metadata.content.keys().clone(); for txz_name in keys { @@ -220,29 +229,28 @@ impl Rpkg { } // Update the plugin index file to track installed files // We need to add the content section to the metadata to do so - let mut db = Database::read(PACKAGES_DATABASE_PATH)?; - db.plugins.insert( + db.insert( self.metadata.name.clone(), InstalledPlugin { files: self.get_archive_installed_files()?, metadata: self.metadata.clone(), }, - ); - Database::write(PACKAGES_DATABASE_PATH, db)?; + )?; // Run postinst if any - let install_or_upgrade: PackageScriptArg = PackageScriptArg::Install; + let arg = if self.is_installed(db) { + PackageScriptArg::Upgrade + } else { + PackageScriptArg::Install + }; self.metadata - .run_package_script(PackageScript::Postinst, install_or_upgrade)?; + .run_package_script(PackageScript::Postinst, arg)?; // Update the webapp xml file if the plugin contains one or more jar file debug!("Enabling the associated jars if any"); - match self.metadata.jar_files.clone() { - None => (), - Some(jars) => { - webapp.enable_jars(&jars)?; - } - } - // Restarting webapp - debug!("Install completed"); + webapp.enable_jars(&self.metadata.jar_files)?; + info!( + "Plugin {} was successfully installed", + self.metadata.short_name() + ); Ok(()) } } diff --git a/relay/sources/rudder-package/src/cli.rs b/relay/sources/rudder-package/src/cli.rs index 474e95fc977..0998ed706de 100644 --- a/relay/sources/rudder-package/src/cli.rs +++ b/relay/sources/rudder-package/src/cli.rs @@ -1,10 +1,32 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2023 Normation SAS -use clap::{Parser, Subcommand}; +use std::fmt::Display; + +use clap::{Parser, Subcommand, ValueEnum}; use crate::CONFIG_PATH; +#[derive(ValueEnum, Copy, Clone, Debug, Default)] +pub enum Format { + Json, + #[default] + Human, +} + +impl Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Json => "json", + Self::Human => "human", + } + ) + } +} + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { @@ -22,22 +44,52 @@ pub struct Args { #[derive(Debug, Subcommand)] pub enum Command { + /// Update package index and licenses from the repository + Update {}, + /// Install plugins, locally or from the repository Install { - #[clap(long, short = 'f', help = "Force installation of given plugin")] + #[clap(long, short, help = "Force installation of given plugin")] force: bool, #[clap()] package: Vec, }, - List {}, + /// Upgrade plugins + Upgrade { + #[clap(long, short, help = "Upgrade all installed plugins")] + all: bool, + + #[clap(long, help = "Run all the postinstall scripts of installed plugins")] + all_postinstall: bool, + + #[clap()] + package: Vec, + }, + /// Uninstall plugins Uninstall { #[clap()] package: Vec, }, - Update {}, + /// Display the plugins list + List { + #[clap(long, short, help = "Show all available plugins")] + all: bool, + + #[clap(long, short, help = "Show enabled plugins")] + enabled: bool, + + #[clap(long, short, help = "Output format", default_value_t = Format::Human)] + format: Format, + }, + /// Show detailed information about a plugin + Show { + #[clap()] + package: Vec, + }, + /// Enable installed plugins Enable { #[clap()] - package: Option>, + package: Vec, #[clap(long, short, help = "Enable all installed plugins")] all: bool, @@ -52,4 +104,21 @@ pub enum Command { )] restore: bool, }, + /// Disable installed plugins + Disable { + #[clap()] + package: Vec, + + #[clap(long, short, help = "Disable all installed plugins")] + all: bool, + + #[clap( + long, + short, + help = "Disable all installed plugins incompatible with the Web application version" + )] + incompatible: bool, + }, + /// Test connection to the plugin repository + CheckConnection {}, } diff --git a/relay/sources/rudder-package/src/config.rs b/relay/sources/rudder-package/src/config.rs index 61c8003d1c9..3316f39fb28 100644 --- a/relay/sources/rudder-package/src/config.rs +++ b/relay/sources/rudder-package/src/config.rs @@ -4,6 +4,7 @@ use std::{fmt, fs::read_to_string, path::Path}; use anyhow::Result; +use log::info; use serde::{Deserialize, Serialize}; const PUBLIC_REPO_URL: &str = "https://repository.rudder.io/plugins"; @@ -77,7 +78,15 @@ impl Configuration { } pub fn read(path: &Path) -> Result { - let c = read_to_string(path)?; + let c = if path.exists() { + read_to_string(path)? + } else { + info!( + "'{}' does not exist, using default configuration", + path.display() + ); + "".to_string() + }; Self::parse(&c) } } diff --git a/relay/sources/rudder-package/src/database.rs b/relay/sources/rudder-package/src/database.rs index d8974989f21..44597600108 100644 --- a/relay/sources/rudder-package/src/database.rs +++ b/relay/sources/rudder-package/src/database.rs @@ -5,84 +5,211 @@ use std::{ collections::HashMap, fs::{self, *}, io::BufWriter, - path::PathBuf, + path::{Path, PathBuf}, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; +use log::{debug, info}; use serde::{Deserialize, Serialize}; use super::archive::Rpkg; use crate::{ archive::{PackageScript, PackageScriptArg}, - plugin, + plugin::{self, short_name}, + repo_index::RepoIndex, + repository::Repository, webapp::Webapp, - PACKAGES_DATABASE_PATH, PACKAGES_FOLDER, + PACKAGES_FOLDER, TMP_PLUGINS_FOLDER, }; -use log::debug; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct Database { + #[serde(skip)] + path: PathBuf, pub plugins: HashMap, } impl Database { - pub fn read(path: &str) -> Result { - let data = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read the installed plugin database in {}", path))?; - let database: Database = serde_json::from_str(&data)?; - Ok(database) + pub fn read(path: &Path) -> Result { + Ok(if path.exists() { + let data = std::fs::read_to_string(path).with_context(|| { + format!( + "Failed to read the installed plugin database in {}", + path.display() + ) + })?; + let mut db: Database = serde_json::from_str(&data)?; + db.path = path.to_path_buf(); + db + } else { + debug!("No database yet, using an empty one"); + Self { + path: path.to_path_buf(), + plugins: HashMap::new(), + } + }) } - pub fn write(path: &str, index: Database) -> Result<()> { - debug!("Updating the installed plugin database in '{}'", path); - let file = File::create(path)?; + pub fn insert(&mut self, k: String, v: InstalledPlugin) -> Result<()> { + self.plugins.insert(k, v); + self.write() + } + + pub fn write(&mut self) -> Result<()> { + debug!( + "Updating the installed plugin database in '{}'", + self.path.display() + ); + create_dir_all(self.path.parent().unwrap())?; + let file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&self.path)?; let mut writer = BufWriter::new(file); - serde_json::to_writer_pretty(&mut writer, &index) - .with_context(|| format!("Failed to update the installed plugins database {}", path))?; + serde_json::to_writer_pretty(&mut writer, &self).with_context(|| { + format!( + "Failed to update the installed plugins database {}", + self.path.display() + ) + })?; Ok(()) } - pub fn is_installed(&self, r: Rpkg) -> bool { + pub fn is_installed(&self, r: &Rpkg) -> bool { match self.plugins.get(&r.metadata.name) { None => false, Some(installed) => installed.metadata.version == r.metadata.version, } } - pub fn uninstall(&mut self, plugin_name: &str, webapp: &mut Webapp) -> Result<()> { - // Force to use plugin long qualified name - if !plugin_name.starts_with("rudder-plugin-") { - plugin_name.to_owned().insert_str(0, "rudder-plugin-{}") + /// Return the plugin containing a given jar + pub fn plugin_provides_jar(&self, jar: &String) -> Option<&InstalledPlugin> { + self.plugins + .values() + .find(|p| p.metadata.jar_files.contains(jar)) + } + + pub fn install( + &mut self, + force: bool, + package: &str, + repository: &Repository, + index: Option<&RepoIndex>, + webapp: &mut Webapp, + ) -> Result<()> { + let rpkg_path = if Path::new(&package).exists() && package.ends_with(".rpkg") { + package.to_string() + } else { + // Find compatible plugin if any + let to_dl_and_install = match index.and_then(|i|i.latest_compatible_plugin(&webapp.version, package)) { + None => bail!("Could not find any compatible '{}' plugin with the current Rudder version in the configured repository.", package), + Some(p) => { + debug!("Found a compatible plugin in the repository:\n{:?}", p); + p + } + }; + let dest = Path::new(TMP_PLUGINS_FOLDER).join( + Path::new(&to_dl_and_install.path) + .file_name() + .ok_or_else(|| { + anyhow!( + "Could not retrieve filename from path '{}'", + to_dl_and_install.path + ) + })?, + ); + // Download rpkg + repository.download(&to_dl_and_install.path, &dest)?; + dest.as_path().display().to_string() }; + let rpkg = Rpkg::from_path(&rpkg_path)?; + if self.plugins.get(&rpkg.metadata.name).is_some() { + info!( + "Plugin {} already installed, upgrading", + rpkg.metadata.short_name() + ); + } + rpkg.install(force, self, webapp)?; + Ok(()) + } + + pub fn uninstall( + &mut self, + plugin_name: &str, + run_rm_scripts: bool, + webapp: &mut Webapp, + ) -> Result<()> { + let short_name = short_name(plugin_name); // Return Ok if not installed if !self.plugins.contains_key(plugin_name) { - debug!("Plugin {} is not installed.", plugin_name); + info!("Plugin {} is not installed.", short_name); return Ok(()); } - debug!("Uninstalling plugin {}", plugin_name); // Disable the jar files if any let installed_plugin = self.plugins.get(plugin_name).ok_or(anyhow!( "Could not extract data for plugin {} in the database", - plugin_name + short_name ))?; + debug!( + "Uninstalling plugin {} (version {})", + short_name, installed_plugin.metadata.version + ); installed_plugin.disable(webapp)?; - installed_plugin - .metadata - .run_package_script(PackageScript::Prerm, PackageScriptArg::None)?; + if run_rm_scripts { + installed_plugin + .metadata + .run_package_script(PackageScript::Prerm, PackageScriptArg::None)?; + } match installed_plugin.remove_installed_files() { Ok(()) => (), Err(e) => debug!("{}", e), } - installed_plugin - .metadata - .run_package_script(PackageScript::Postrm, PackageScriptArg::None)?; + if run_rm_scripts { + installed_plugin + .metadata + .run_package_script(PackageScript::Postrm, PackageScriptArg::None)?; + } // Remove associated package scripts and plugin folder - fs::remove_dir_all( - PathBuf::from(PACKAGES_FOLDER).join(installed_plugin.metadata.name.clone()), - )?; + let plugin_dir = PathBuf::from(PACKAGES_FOLDER).join(&installed_plugin.metadata.name); + if plugin_dir.exists() { + fs::remove_dir_all(&plugin_dir).context(format!( + "Could not remove {} plugin folder '{}'", + short_name, + plugin_dir.display() + ))?; + } // Update the database self.plugins.remove(plugin_name); - Database::write(PACKAGES_DATABASE_PATH, self.to_owned())?; + self.write()?; + info!("Plugin {} successfully uninstalled", short_name); + Ok(()) + } + + pub fn enabled_plugins_save(&self, backup_path: &Path, webapp: &mut Webapp) -> Result<()> { + let saved = webapp + .jars()? + .iter() + // Let's ignore unknown jars + .flat_map(|j| self.plugin_provides_jar(j)) + .map(|p| format!("enable {}", p.metadata.name)) + .collect::>() + .join("\n"); + fs::write(backup_path, saved)?; + Ok(()) + } + + pub fn enabled_plugins_restore(&self, backup_path: &Path, webapp: &mut Webapp) -> Result<()> { + for line in read_to_string(backup_path)?.lines() { + let plugin_name = line.trim().split(' ').nth(1); + match plugin_name { + None => debug!("Malformed line in plugin backup status file"), + Some(x) => match self.plugins.get(x) { + None => debug!("Plugin {} is not installed, it could not be enabled", x), + Some(y) => y.enable(webapp)?, + }, + } + } Ok(()) } } @@ -97,34 +224,43 @@ pub struct InstalledPlugin { impl InstalledPlugin { pub fn disable(&self, webapp: &mut Webapp) -> Result<()> { - debug!("Disabling plugin {}", self.metadata.name); - match &self.metadata.jar_files { - None => { - println!("Plugin {} does not support the enable/disable feature, it will always be enabled if installed.", self.metadata.name); - Ok(()) - } - Some(jars) => webapp.disable_jars(jars), + info!("Disabling plugin {}", self.metadata.short_name()); + if self.metadata.jar_files.is_empty() { + debug!("Plugin {} does not support the enable/disable feature, it will always be enabled if installed.", self.metadata.name); + Ok(()) + } else { + webapp.disable_jars(&self.metadata.jar_files) } } + pub fn enable(&self, webapp: &mut Webapp) -> Result<()> { - debug!("Enabling plugin {}", self.metadata.name); - match &self.metadata.jar_files { - None => { - println!("Plugin {} does not support the enable/disable feature, it will always be enabled if installed.", self.metadata.name); - Ok(()) - } - Some(jars) => webapp.enable_jars(jars), + info!("Enabling plugin {}", self.metadata.short_name()); + if self.metadata.jar_files.is_empty() { + debug!("Plugin {} does not support the enable/disable feature, it will always be enabled if installed.", self.metadata.name); + Ok(()) + } else { + webapp.enable_jars(&self.metadata.jar_files) } } pub fn remove_installed_files(&self) -> Result<()> { - self.files.clone().into_iter().try_for_each(|f| { + // Remove by decreasing path length to + // empty directories before trying to remove them. + let mut to_remove = self.files.clone(); + to_remove.sort_by(|a, b| b.len().partial_cmp(&a.len()).unwrap()); + to_remove.into_iter().try_for_each(|f| { let m = PathBuf::from(f.clone()); if m.is_dir() { - debug!("Removing file '{}'", f); - fs::remove_dir(f).map_err(anyhow::Error::from) + let is_empty = m.read_dir()?.next().is_none(); + if is_empty { + debug!("Removing folder '{}'", f); + fs::remove_dir(f).map_err(anyhow::Error::from) + } else { + debug!("Not removing folder '{}' as it's not empty", f); + Ok(()) + } } else { - debug!("Removing folder '{}' if empty", f); + debug!("Removing file '{}'", f); fs::remove_file(f).map_err(anyhow::Error::from) } }) @@ -138,7 +274,6 @@ mod tests { use ::std::fs::read_to_string; use assert_json_diff::assert_json_eq; use pretty_assertions::assert_eq; - use tempfile::TempDir; use super::*; use crate::archive; @@ -149,21 +284,33 @@ mod tests { .expect("Unable to parse file './tests/pluginèdatabase_parsing.json'"); let db: Database = serde_json::from_str(&data).unwrap(); assert_eq!( - db.plugins["rudder-plugin-aix"].metadata.plugin_type, + db.plugins["rudder-plugin-aix"].metadata.package_type, archive::PackageType::Plugin ); + for p in db.plugins { + println!("{}", p.1.metadata); + } } #[test] fn test_adding_a_plugin_to_db() { use crate::versions; - let mut a = Database::read("./tests/database/plugin_database_update_sample.json").unwrap(); + fs::copy( + "./tests/database/plugin_database_update_sample.json", + "./tests/database/plugin_database_update_sample.json.test", + ) + .unwrap(); + let mut a = Database::read(Path::new( + "./tests/database/plugin_database_update_sample.json.test", + )) + .unwrap(); let addon = InstalledPlugin { files: vec![String::from("/tmp/my_path")], metadata: plugin::Metadata { - plugin_type: archive::PackageType::Plugin, + package_type: archive::PackageType::Plugin, name: String::from("my_name"), + description: None, version: versions::ArchiveVersion::from_str("0.0.0-0.0").unwrap(), build_date: String::from("2023-10-13T10:03:34+00:00"), depends: None, @@ -172,25 +319,20 @@ mod tests { String::from("files.txz"), String::from("/opt/rudder/share/plugins"), )]), - jar_files: None, + jar_files: vec![], }, }; - a.plugins.insert(addon.metadata.name.clone(), addon); - let dir = TempDir::new().unwrap(); - let target_path = dir - .path() - .join("target.json") - .into_os_string() - .into_string() - .unwrap(); - let _ = Database::write(&target_path.clone(), a); + a.insert(addon.metadata.name.clone(), addon).unwrap(); let reference: serde_json::Value = serde_json::from_str( &read_to_string("./tests/database/plugin_database_update_sample.json.expected") .unwrap(), ) .unwrap(); - let generated: serde_json::Value = - serde_json::from_str(&read_to_string(target_path).unwrap()).unwrap(); + let generated: serde_json::Value = serde_json::from_str( + &read_to_string("./tests/database/plugin_database_update_sample.json.test").unwrap(), + ) + .unwrap(); + fs::remove_file("./tests/database/plugin_database_update_sample.json.test").unwrap(); assert_json_eq!(reference, generated); } } diff --git a/relay/sources/rudder-package/src/dependency.rs b/relay/sources/rudder-package/src/dependency.rs index d30ba58395f..154a11f22db 100644 --- a/relay/sources/rudder-package/src/dependency.rs +++ b/relay/sources/rudder-package/src/dependency.rs @@ -3,7 +3,7 @@ use std::{process::Command, str}; -use log::{debug, error}; +use log::{debug, warn}; use regex::Regex; use serde::{Deserialize, Serialize}; use which::which; @@ -66,7 +66,7 @@ pub trait IsInstalled { impl IsInstalled for PythonDependency { fn is_installed(&self) -> bool { - error!("Deprecated dependency type 'python' with value '{}'. It is up to you to make sure it is installed, ignoring.", self.0); + warn!("Deprecated dependency type 'python' with value '{}'. It is up to you to make sure it is installed, ignoring.", self.0); true } } @@ -98,7 +98,7 @@ impl IsInstalled for AptDependency { impl IsInstalled for RpmDependency { fn is_installed(&self) -> bool { let mut binding = Command::new("rpm"); - let cmd = binding.arg("-q").arg(self.0.clone()); + let cmd = binding.arg("-q").arg("--").arg(&self.0); let result = match CmdOutput::new(cmd) { Ok(a) => a, Err(e) => { @@ -117,7 +117,7 @@ impl IsInstalled for RpmDependency { impl IsInstalled for BinaryDependency { fn is_installed(&self) -> bool { - match which(self.0.clone()) { + match which(&self.0) { Ok(_) => { debug!("'binary' base dependency '{}' found on the system", self.0); true diff --git a/relay/sources/rudder-package/src/lib.rs b/relay/sources/rudder-package/src/lib.rs index f455ed70f9e..d64aa8709e0 100644 --- a/relay/sources/rudder-package/src/lib.rs +++ b/relay/sources/rudder-package/src/lib.rs @@ -9,6 +9,8 @@ mod cmd; mod config; mod database; mod dependency; +mod license; +mod list; mod plugin; mod repo_index; mod repository; @@ -21,14 +23,26 @@ use std::{ process, }; -use crate::{cli::Command, signature::SignatureVerifier, webapp::Webapp}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use clap::Parser; -use log::{debug, error, LevelFilter}; +use log::{debug, error, info, warn, LevelFilter}; -use crate::{config::Configuration, repository::Repository}; +use crate::{ + cli::Command, + config::Configuration, + database::Database, + license::Licenses, + list::ListOutput, + plugin::{long_names, short_name}, + repo_index::RepoIndex, + repository::Repository, + signature::SignatureVerifier, + versions::RudderVersion, + webapp::Webapp, +}; const PACKAGES_FOLDER: &str = "/var/rudder/packages"; +const LICENSES_FOLDER: &str = "/opt/rudder/etc/plugins/licenses"; const WEBAPP_XML_PATH: &str = "/opt/rudder/share/webapps/rudder.xml"; const PACKAGES_DATABASE_PATH: &str = "/var/rudder/packages/index.json"; const CONFIG_PATH: &str = "/opt/rudder/etc/rudder-pkg/rudder-pkg.conf"; @@ -56,7 +70,10 @@ pub fn run() -> Result<()> { process::exit(1); } + // Read CLI args let args = cli::Args::parse(); + + // Setup logger early let filter = if args.debug { LevelFilter::Debug } else { @@ -68,194 +85,224 @@ pub fn run() -> Result<()> { .format_target(false) .filter_level(filter) .init(); + + // Parse configuration file debug!("Parsed CLI arguments: {args:?}"); let cfg = Configuration::read(Path::new(&args.config)) .with_context(|| format!("Reading configuration from '{}'", &args.config))?; + debug!("Parsed configuration: {cfg:?}"); + + // Now initialize all common data structures let verifier = SignatureVerifier::new(PathBuf::from(SIGNATURE_KEYRING_PATH)); let repo = Repository::new(&cfg, verifier)?; - let mut webapp = Webapp::new(PathBuf::from(WEBAPP_XML_PATH)); - debug!("Parsed configuration: {cfg:?}"); + let webapp_version = RudderVersion::from_path(RUDDER_VERSION_PATH)?; + let mut webapp = Webapp::new(PathBuf::from(WEBAPP_XML_PATH), webapp_version); + let mut db = Database::read(Path::new(PACKAGES_DATABASE_PATH))?; + let index = RepoIndex::from_path(REPOSITORY_INDEX_PATH)?; + let licenses = Licenses::from_path(Path::new(LICENSES_FOLDER))?; match args.command { Command::Install { force, package } => { - return action::install(force, package, repo, &mut webapp); + let mut errors = false; + for p in &long_names(package) { + if let Err(e) = db.install(force, p, &repo, index.as_ref(), &mut webapp) { + errors = true; + error!("Installation of {} failed: {e}", short_name(p)); + } + } + if errors { + bail!("Some plugin installation failed"); + } else { + info!("Installation ran successfully"); + } } Command::Uninstall { package } => { - return action::uninstall(package, &mut webapp); - } - _ => { - error!("This command is not implemented"); + let mut errors = false; + for p in &long_names(package) { + if let Err(e) = db.uninstall(p, true, &mut webapp) { + errors = true; + error!("Uninstallation of {} failed: {e}", short_name(p)); + } + } + if errors { + bail!("Some plugin uninstallation failed"); + } else { + info!("Uninstallation ran successfully"); + } } - } - Ok(()) -} - -pub mod action { - use anyhow::{anyhow, bail, Result}; - use flate2::read::GzDecoder; - use log::debug; - use tar::Archive; - - use crate::archive::Rpkg; - use crate::database::Database; - use crate::repo_index::RepoIndex; - use crate::repository::Repository; - use crate::versions::RudderVersion; - use crate::{ - webapp::Webapp, PACKAGES_DATABASE_PATH, REPOSITORY_INDEX_PATH, RUDDER_VERSION_PATH, - TMP_PLUGINS_FOLDER, - }; - use std::fs; - use std::fs::File; - use std::io::BufRead; - use std::path::{Path, PathBuf}; - - pub fn uninstall(packages: Vec, webapp: &mut Webapp) -> Result<()> { - let mut db = Database::read(PACKAGES_DATABASE_PATH)?; - packages.iter().try_for_each(|p| db.uninstall(p, webapp)) - } - - pub fn install( - force: bool, - packages: Vec, - repository: Repository, - webapp: &mut Webapp, - ) -> Result<()> { - packages.iter().try_for_each(|package| { - let rpkg_path = if Path::new(&package).exists() { - package.clone() + Command::Upgrade { + all, + package, + all_postinstall, + } => { + if all_postinstall { + let mut errors = false; + for p in db.plugins.values() { + if let Err(e) = p.metadata.run_package_script( + archive::PackageScript::Postinst, + archive::PackageScriptArg::Upgrade, + ) { + errors = true; + // Don't fail and continue + error!( + "Postinst script for {} failed: {e}", + p.metadata.short_name() + ); + } + } + if errors { + bail!("Some scripts failed"); + } else { + info!("All postinstall scripts ran successfully"); + } } else { - // Find compatible plugin if any - let webapp_version = RudderVersion::from_path(RUDDER_VERSION_PATH)?; - let index = RepoIndex::from_path(REPOSITORY_INDEX_PATH)?; - let to_dl_and_install = match index.get_compatible_plugin(webapp_version, package) { - None => bail!("Could not find any compatible '{}' plugin with the current Rudder version in the configured repository.", package), - Some(p) => { - debug!("Found a compatible plugin in the repository:\n{:?}", p); - p + // Normal upgrades + let to_upgrade: Vec = if all { + db.plugins.keys().cloned().collect() + } else { + let packages = long_names(package); + for p in &packages { + if db.plugins.get(p).is_none() { + bail!( + "Plugin {} is not installed, stopping upgrade", + short_name(p) + ) + } } + packages }; - let dest = Path::new(TMP_PLUGINS_FOLDER).join( - Path::new(&to_dl_and_install.path) - .file_name() - .ok_or_else(|| { - anyhow!( - "Could not retrieve filename from path '{}'", - to_dl_and_install.path - ) - })?, - ); - // Download rpkg - repository.clone().download(&to_dl_and_install.path, &dest)?; - dest.as_path().display().to_string() - }; - let rpkg = Rpkg::from_path(&rpkg_path)?; - rpkg.install(force, webapp)?; - Ok(()) - })?; - webapp.apply_changes() - } - - pub fn list() -> Result<()> { - let db = Database::read(PACKAGES_DATABASE_PATH); - println!("Installed plugins:\n{:?}", db); - Ok(()) - } - - pub fn update(repository: Repository) -> Result<()> { - // Update the index file - debug!("Updating repository index"); - let rudder_version = RudderVersion::from_path(RUDDER_VERSION_PATH)?; - let remote_index = format!( - "{}.{}/rpkg.index", - rudder_version.major, rudder_version.minor - ); - repository.download_unsafe(&remote_index, &PathBuf::from(REPOSITORY_INDEX_PATH))?; - - // Update the licenses - if let Some(x) = repository.get_username() { - debug!("Updating licenses"); - let license_folder = PathBuf::from("/opt/rudder/etc/plugins/licenses"); - let archive_name = format!("{}-license.tar.gz", x); - let local_archive_path = &license_folder.clone().join(archive_name.clone()); - if let Err(e) = repository.download_unsafe( - &format!("licences/{}/{}", x, archive_name), - local_archive_path, - ) { - bail!( - "Could not download licenses from configured repository.\n{}", - e + let mut errors = false; + for p in &to_upgrade { + if let Err(e) = db.install(false, p, &repo, index.as_ref(), &mut webapp) { + errors = true; + error!("Could not upgrade {}: {e}", short_name(p)) + } + } + if errors { + bail!("Some plugins were not upgraded correctly"); + } else { + info!("All plugins were upgraded successfully"); + } + } + } + Command::List { + all, + enabled, + format, + } => ListOutput::new(all, enabled, &licenses, &db, index.as_ref(), &webapp)? + .display(format)?, + Command::Show { package } => { + for p in long_names(package) { + println!( + "{}", + db.plugins + .get(&p) + .ok_or_else(|| anyhow!("Could not find plugin"))? + .metadata ) } - // Decompress archive - let mut archive = Archive::new(GzDecoder::new(File::open(local_archive_path)?)); - archive.unpack(license_folder)?; - } else { - debug!("Not updating licenses as no configured credentials were found") } - Ok(()) - } - - pub fn enable( - mut w: Webapp, - packages: Option>, - all: bool, - snapshot: bool, - restore: bool, - backup_path: Option, - ) -> Result<()> { - let db = Database::read(PACKAGES_DATABASE_PATH)?; - // If all is passed, enabled all installed plugins - let to_enabled = if all { - Some(db.plugins.keys().cloned().collect()) - } else { - packages - }; - // If package names are passed, enabled them - if let Some(x) = to_enabled { - return x.iter().try_for_each(|p| match db.plugins.get(p) { - None => { - println!("Plugin {} not found installed", p); - Ok(()) - } - Some(installed_plugin) => installed_plugin.enable(&mut w), - }); + Command::Update {} => { + repo.update(&webapp)?; + info!("Index and licenses successfully updated") } - let backup_path = match backup_path { - None => format!("{}/plugins_status.backup", TMP_PLUGINS_FOLDER), - Some(p) => p, - }; - if snapshot { - let mut enabled_jars_from_plugins = Vec::::new(); - let enabled_jar = w.jars()?; - for (k, v) in db.plugins.clone() { - if let Some(j) = v.metadata.jar_files { - if j.iter().any(|x| enabled_jar.contains(x)) { - enabled_jars_from_plugins.push(format!("enable {}", k)) + Command::Enable { + package, + all, + save, + restore, + } => { + // If all is passed, enabled all installed plugins + let to_enable: Vec = if all { + db.plugins.keys().cloned().collect() + } else { + long_names(package) + }; + if to_enable.is_empty() { + let backup_path = Path::new(TMP_PLUGINS_FOLDER).join("plugins_status.backup"); + if save { + db.enabled_plugins_save(&backup_path, &mut webapp)?; + info!("Plugins status successfully saved"); + } else if restore { + db.enabled_plugins_restore(&backup_path, &mut webapp)?; + info!("Plugins status successfully restored"); + } else { + bail!("No plugin provided"); + } + } else { + let mut errors = false; + for p in &to_enable { + match db.plugins.get(p) { + None => { + warn!("Plugin {} not installed", short_name(p)) + } + Some(p) => { + if let Err(e) = p.enable(&mut webapp) { + errors = true; + error!("Could not enable plugin {}: {e}", p.metadata.short_name()); + } + } } } + if errors { + error!("Some plugins could not be enabled"); + } else { + info!("Plugins successfully enabled"); + } } - fs::write(backup_path, enabled_jars_from_plugins.join("\n"))?; - return Ok(()); } + Command::Disable { + package, + all, + incompatible, + } => { + let to_disable: Vec = if all { + db.plugins.keys().cloned().collect() + } else if incompatible { + db.plugins + .iter() + .filter(|(_, p)| { + !webapp + .version + .is_compatible(&p.metadata.version.rudder_version) + }) + .map(|(p, _)| p.to_string()) + .collect() + } else { + long_names(package) + }; - if restore { - let file = File::open(backup_path)?; - let buf = std::io::BufReader::new(file); - buf.lines().for_each(|l| { - let binding = l.expect("Could not read line from plugin backup status file"); - let plugin_name = binding.trim().split(' ').nth(1); - match plugin_name { - None => debug!("Malformed line in plugin backup status file"), - Some(x) => match db.plugins.get(x) { - None => debug!("Plugin {} is not installed, it could not be enabled", x), - Some(y) => { - let _ = y.enable(&mut w); + let mut errors = false; + for p in &to_disable { + match db.plugins.get(p) { + None => { + warn!("Plugin {} not installed", short_name(p)) + } + Some(p) => { + if let Err(e) = p.disable(&mut webapp) { + errors = true; + error!("Could not disable plugin {}: {e}", p.metadata.short_name()); } - }, + } } - }) + } + if errors { + error!("Some plugins could not be disabled"); + } else { + info!("Plugins successfully disabled"); + } } - Ok(()) + Command::CheckConnection {} => repo.test_connection()?, + } + // Restart if needed + webapp.apply_changes()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + #[test] + fn i_am_not_root() { + assert!(!super::am_i_root().unwrap()) } } diff --git a/relay/sources/rudder-package/src/license.rs b/relay/sources/rudder-package/src/license.rs new file mode 100644 index 00000000000..4d04c6d0ea9 --- /dev/null +++ b/relay/sources/rudder-package/src/license.rs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2023 Normation SAS + +use anyhow::{anyhow, Result}; +use serde::Serialize; +use std::{collections::HashMap, fs, path::Path}; + +/// Very simple signature file reader +/// We mainly need to extract expiration date for each plugin + +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +pub struct License { + pub start_date: String, + pub end_date: String, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Licenses { + pub inner: HashMap, +} + +impl Licenses { + pub fn from_path(path: &Path) -> Result { + fn find_key_value<'a>(key: &'a str, content: &'a str) -> Option<&'a str> { + content + .lines() + .find(|l| l.starts_with(&format!("{key}="))) + .and_then(|l| l.split('=').nth(1)) + } + + let mut res = HashMap::new(); + if path.exists() { + for entry in fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if path.extension().map(|e| e.to_string_lossy().into_owned()) + == Some("license".to_string()) + { + let s = fs::read_to_string(path)?; + let plugin = find_key_value("softwareid", &s) + .ok_or(anyhow!("Could not find software id in license"))? + .to_owned(); + let start_date = find_key_value("startdate", &s) + .ok_or(anyhow!("Could not find start date in license"))? + .to_owned(); + let end_date = find_key_value("enddate", &s) + .ok_or(anyhow!("Could not find end date in license"))? + .to_owned(); + res.insert( + plugin, + License { + start_date, + end_date, + }, + ); + } + } + } + Ok(Self { inner: res }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_parses_license() { + let licenses = Licenses::from_path(Path::new("tests/licenses")).unwrap(); + dbg!(licenses); + } +} diff --git a/relay/sources/rudder-package/src/list.rs b/relay/sources/rudder-package/src/list.rs new file mode 100644 index 00000000000..efe265995a1 --- /dev/null +++ b/relay/sources/rudder-package/src/list.rs @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2023 Normation SAS + +use std::collections::HashSet; + +use anyhow::Result; +use serde::Serialize; + +use crate::{ + cli::Format, + database::Database, + license::{License, Licenses}, + plugin::PluginType, + repo_index::RepoIndex, + webapp::Webapp, +}; + +pub struct ListOutput { + inner: Vec, +} + +/// Content we want to display about each plugin +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +struct ListEntry { + /// Plugin name ("short") + name: String, + /// Full plugin version + #[serde(skip_serializing_if = "Option::is_none")] + version: Option, + /// None means no higher version is available + #[serde(skip_serializing_if = "Option::is_none")] + latest_version: Option, + installed: bool, + /// Only true for enabled web plugins + enabled: bool, + #[serde(rename = "type")] + plugin_type: PluginType, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + license: Option, +} + +impl ListOutput { + fn human_table(self) -> Result<()> { + use cli_table::{print_stdout, Cell, Style, Table}; + + let table = self + .inner + .into_iter() + .map(|e| { + vec![ + e.name.cell(), + e.version.as_ref().unwrap_or(&"".to_string()).cell(), + if e.latest_version == e.version { + "".to_string() + } else { + e.latest_version.unwrap_or("".to_string()) + } + .cell(), + e.plugin_type.cell(), + match (e.installed, e.enabled, e.plugin_type) { + (false, _, _) => "", + (true, true, _) => "enabled", + (true, false, PluginType::Web) => "disabled", + (true, false, PluginType::Standalone) => "", + } + .cell(), + e.license + .map(|l| l.end_date) + .unwrap_or("".to_string()) + .cell(), + e.description.unwrap_or("".to_string()).cell(), + ] + }) + .table() + .title(vec![ + "Name".cell().bold(true), + "Installed".cell().bold(true), + "Latest".cell().bold(true), + "Type".cell().bold(true), + "Status".cell().bold(true), + "License valid until".cell().bold(true), + "Description".cell().bold(true), + ]); + print_stdout(table)?; + Ok(()) + } + + fn json(&self) -> Result<()> { + let out = serde_json::to_string(&self.inner)?; + println!("{}", out); + Ok(()) + } + + pub fn new( + show_all: bool, + show_only_enabled: bool, + licenses: &Licenses, + db: &Database, + index: Option<&RepoIndex>, + webapp: &Webapp, + ) -> Result { + let mut plugins: Vec = vec![]; + // Principles: + // + // * By default, display installed plugins. + // * If an index is available and "all", also add available plugins. + // * If "enabled", display installed package minus disabled ones. + let jars = webapp.jars()?; + let installed_plugins: Vec<&String> = db.plugins.keys().collect(); + let enabled_plugins: HashSet = jars + .into_iter() + .flat_map(|j| db.plugin_provides_jar(&j)) + .map(|p| p.metadata.name.clone()) + .collect(); + let latest = index.map(|i| i.latest_compatible_plugins(&webapp.version)); + + for p in db.plugins.values() { + let name = p.metadata.short_name().to_string(); + let enabled = enabled_plugins.contains(&p.metadata.name); + let latest_version = index + .and_then(|i| i.latest_compatible_plugin(&webapp.version, &p.metadata.name)) + .map(|p| p.metadata.version.to_string()); + + let e = ListEntry { + name, + version: Some(p.metadata.version.to_string()), + latest_version, + plugin_type: p.metadata.plugin_type(), + enabled, + installed: true, + description: p.metadata.description.clone(), + license: licenses.inner.get(&p.metadata.name).cloned(), + }; + if !show_only_enabled || enabled { + // Standalone plugins are always considered enabled + // (even if some might not be: disabled systemd service for notify, etc.) + plugins.push(e); + } + } + + if show_all { + if let Some(available) = latest { + for p in available { + if !installed_plugins.contains(&&p.metadata.name) { + let name = p.metadata.short_name().to_string(); + let e = ListEntry { + name, + version: None, + latest_version: Some(p.metadata.version.to_string()), + plugin_type: p.metadata.plugin_type(), + installed: false, + enabled: false, + description: p.metadata.description.clone(), + license: licenses.inner.get(&p.metadata.name).cloned(), + }; + plugins.push(e); + } + } + } + } + + // Sort by name alphabetical order + plugins.sort_by_key(|e| e.name.clone()); + Ok(Self { inner: plugins }) + } + + pub fn display(self, format: Format) -> Result<()> { + match format { + Format::Json => self.json()?, + Format::Human => self.human_table()?, + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::path::{Path, PathBuf}; + + use super::ListOutput; + use crate::{ + cli::Format, database::Database, license::Licenses, repo_index::RepoIndex, + versions::RudderVersion, webapp::Webapp, + }; + + #[test] + fn it_lists_plugins() { + let w = Webapp::new( + PathBuf::from("tests/webapp_xml/example.xml"), + RudderVersion::from_path("./tests/versions/rudder-server-version").unwrap(), + ); + let r = RepoIndex::from_path("./tests/repo_index.json") + .unwrap() + .unwrap(); + let d = Database::read(Path::new( + "./tests/database/plugin_database_update_sample.json", + )) + .unwrap(); + let l = Licenses::from_path(Path::new("tests/licenses")).unwrap(); + let out = ListOutput::new(true, false, &l, &d, Some(&r), &w).unwrap(); + out.display(Format::Human).unwrap(); + } +} diff --git a/relay/sources/rudder-package/src/plugin.rs b/relay/sources/rudder-package/src/plugin.rs index 4a5536ce908..7e03789068e 100644 --- a/relay/sources/rudder-package/src/plugin.rs +++ b/relay/sources/rudder-package/src/plugin.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2023 Normation SAS -use std::{collections::HashMap, path::Path, process::Command}; +use std::{collections::HashMap, fmt::Display, path::Path, process::Command}; use anyhow::bail; use log::debug; @@ -11,32 +11,114 @@ use crate::{ archive::{self, PackageScript, PackageScriptArg}, cmd::CmdOutput, dependency::Dependencies, - versions, PACKAGES_FOLDER, + versions::{self, RudderVersion}, + PACKAGES_FOLDER, }; +pub fn long_names(l: Vec) -> Vec { + l.into_iter() + .map(|p| { + if p.starts_with("rudder-plugin-") { + p + } else { + format!("rudder-plugin-{p}") + } + }) + .collect() +} + +pub fn short_name(p: &str) -> &str { + p.strip_prefix("rudder-plugin-").unwrap_or(p) +} + #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[serde(rename_all = "kebab-case")] pub struct Metadata { #[serde(rename = "type")] - pub plugin_type: archive::PackageType, + pub package_type: archive::PackageType, pub name: String, pub version: versions::ArchiveVersion, - #[serde(rename(serialize = "build-date", deserialize = "build-date"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, pub build_date: String, #[serde(skip_serializing_if = "Option::is_none")] pub depends: Option, - #[serde(rename(serialize = "build-commit", deserialize = "build-commit"))] pub build_commit: String, pub content: HashMap, - #[serde(rename(serialize = "jar-files", deserialize = "jar-files"))] - #[serde(skip_serializing_if = "Option::is_none")] - pub jar_files: Option>, + #[serde(default)] + pub jar_files: Vec, +} + +/// Not present in metadata but computed from them +/// +/// Allows exposing to the user if the plugin will appear in the interface or not. +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum PluginType { + Web, + Standalone, +} + +impl Display for PluginType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Web => "web", + Self::Standalone => "standalone", + }) + } +} + +// Used by the "show" command +impl Display for Metadata { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!( + "Name: {} +Version: {} +Description: {} +Type: {} plugin +Build-date: {} +Build-commit: {}", + self.short_name(), + self.version, + self.description.as_ref().unwrap_or(&"".to_owned()), + self.plugin_type(), + self.build_date, + self.build_commit + ))?; + f.write_str("\nJar files:")?; + if self.jar_files.is_empty() { + f.write_str(" none")?; + } else { + f.write_str("\n")?; + for j in self.jar_files.iter() { + write!(f, " {}", j)?; + } + } + f.write_str("\nContents:\n")?; + for (a, p) in self.content.iter() { + writeln!(f, " {}: {}", a, p)?; + } + Ok(()) + } } impl Metadata { - pub fn is_compatible(&self, webapp_version: &str) -> bool { + pub fn is_compatible(&self, webapp_version: &RudderVersion) -> bool { self.version.rudder_version.is_compatible(webapp_version) } + pub fn plugin_type(&self) -> PluginType { + if self.jar_files.is_empty() { + PluginType::Standalone + } else { + PluginType::Web + } + } + + pub fn short_name(&self) -> &str { + short_name(&self.name) + } + pub fn run_package_script( &self, script: PackageScript, @@ -44,7 +126,11 @@ impl Metadata { ) -> Result<(), anyhow::Error> { debug!( "Running package script '{}' with args '{}' for plugin '{}' in version '{}-{}'...", - script, arg, self.name, self.version.rudder_version, self.version.plugin_version + script, + arg, + self.short_name(), + self.version.rudder_version, + self.version.plugin_version ); let package_script_path = Path::new(PACKAGES_FOLDER) .join(self.name.clone()) diff --git a/relay/sources/rudder-package/src/repo_index.rs b/relay/sources/rudder-package/src/repo_index.rs index 1b5af8d784c..a5f73545529 100644 --- a/relay/sources/rudder-package/src/repo_index.rs +++ b/relay/sources/rudder-package/src/repo_index.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2023 Normation SAS -use std::fs; +use std::{collections::HashSet, fs, path::Path}; use serde::{Deserialize, Serialize}; @@ -11,37 +11,52 @@ use crate::{plugin, versions::RudderVersion}; pub struct RepoIndex(Vec); impl RepoIndex { - pub fn from_path(path: &str) -> Result { - let data = fs::read_to_string(path)?; - let index: RepoIndex = serde_json::from_str(&data)?; - Ok(index) + /// Try to read the index. We need to handle the case where the server has not access + /// to the repository and work offline. + pub fn from_path(path: &str) -> Result, anyhow::Error> { + if Path::new(path).exists() { + let data = fs::read_to_string(path)?; + let index: RepoIndex = serde_json::from_str(&data)?; + Ok(Some(index)) + } else { + Ok(None) + } } - pub fn get_compatible_plugins(&self, webapp_version: RudderVersion) -> Vec { - self.clone() + pub fn inner(&self) -> &[Plugin] { + self.0.as_slice() + } + + // What we need to do with the index: + // + // * get latest version of a given plugin (for install) + // * get latest version of all plugins (for list) + + pub fn latest_compatible_plugins(&self, webapp_version: &RudderVersion) -> Vec<&Plugin> { + let names = self .0 + .iter() + .filter(|p| webapp_version.is_compatible(&p.metadata.version.rudder_version)) + .map(|p| &p.metadata.name) + .collect::>(); + names .into_iter() - .filter(|x| { - let distant_package_webapp_version = x.metadata.version.rudder_version.to_string(); - webapp_version.is_compatible(&distant_package_webapp_version) - }) + .flat_map(|n| self.latest_compatible_plugin(webapp_version, n)) .collect() } - pub fn get_compatible_plugin( + pub fn latest_compatible_plugin( &self, - webapp_version: RudderVersion, + webapp_version: &RudderVersion, plugin_name: &str, - ) -> Option { - self.get_compatible_plugins(webapp_version) - .into_iter() - .find(|x| { - [ - plugin_name.to_string(), - format!("rudder-plugin-{}", plugin_name), - ] - .contains(&x.metadata.name) + ) -> Option<&Plugin> { + self.0 + .iter() + .filter(|p| { + plugin_name == p.metadata.name + && webapp_version.is_compatible(&p.metadata.version.rudder_version) }) + .max_by_key(|p| &p.metadata.version) } } @@ -50,7 +65,7 @@ pub struct Plugin { pub path: String, #[serde(flatten)] - metadata: plugin::Metadata, + pub metadata: plugin::Metadata, } #[cfg(test)] @@ -64,51 +79,56 @@ mod tests { #[test] fn test_plugin_index_parsing() { - let index: RepoIndex = RepoIndex::from_path("./tests/repo_index.json").unwrap(); + let index: RepoIndex = RepoIndex::from_path("./tests/repo_index.json") + .unwrap() + .unwrap(); let expected = RepoIndex( vec![ Plugin { metadata: plugin::Metadata { - plugin_type: archive::PackageType::Plugin, + package_type: archive::PackageType::Plugin, name: String::from("rudder-plugin-aix"), version: versions::ArchiveVersion::from_str("8.0.0~beta2-2.1").unwrap(), + description: None, build_date: String::from("2023-09-14T14:31:35+00:00"), build_commit: String::from("2198ca7c0aa0a4e19f04e0ace099520371641f92"), content: HashMap::from([ (String::from("files.txz"), String::from("/opt/rudder/share/plugins")), ]), depends: None, - jar_files: Some(vec![String::from("/opt/rudder/share/plugins/aix/aix.jar")]), + jar_files: vec![String::from("/opt/rudder/share/plugins/aix/aix.jar")], }, path: String::from("./8.0/aix/release/rudder-plugin-aix-8.0.0~beta2-2.1.rpkg"), }, Plugin { metadata: plugin::Metadata { - plugin_type: archive::PackageType::Plugin, + package_type: archive::PackageType::Plugin, name: String::from("rudder-plugin-aix"), version: versions::ArchiveVersion::from_str("8.0.0~rc1-2.1").unwrap(), + description: None, build_date: String::from("2023-10-13T09:44:54+00:00"), build_commit: String::from("cdcf8a4b01124b9b309903cafd95b3a161a9c35c"), content: HashMap::from([ (String::from("files.txz"), String::from("/opt/rudder/share/plugins")), ]), depends: None, - jar_files: Some(vec![String::from("/opt/rudder/share/plugins/aix/aix.jar")]), + jar_files: vec![String::from("/opt/rudder/share/plugins/aix/aix.jar")], }, path: String::from("./8.0/aix/rudder-plugin-aix-8.0.0~rc1-2.1.rpkg/release/rudder-plugin-aix-8.0.0~rc1-2.1.rpkg"), }, Plugin { metadata: plugin::Metadata { - plugin_type: archive::PackageType::Plugin, + package_type: archive::PackageType::Plugin, name: String::from("rudder-plugin-vault"), version: versions::ArchiveVersion::from_str("8.0.0~rc1-2.1-nightly").unwrap(), + description: None, build_date: String::from("2023-10-07T20:38:18+00:00"), build_commit: String::from("747126d505b3cac0403014cf35a4caf3a3ec886f"), content: HashMap::from([ (String::from("files.txz"), String::from("/opt/rudder/")), ]), depends: None, - jar_files: None, + jar_files: vec![], }, path: String::from("./8.0/rudder-plugin-vault-8.0.0~rc1-2.1-nightly.rpkg/nightly/rudder-plugin-vault-8.0.0~rc1-2.1-nightly.rpkg"), }, diff --git a/relay/sources/rudder-package/src/repository.rs b/relay/sources/rudder-package/src/repository.rs index 5b04e4d4f41..00a596a29c4 100644 --- a/relay/sources/rudder-package/src/repository.rs +++ b/relay/sources/rudder-package/src/repository.rs @@ -3,20 +3,24 @@ use std::{ fs::{self, File}, - path::Path, + path::{Path, PathBuf}, }; -use anyhow::{anyhow, bail, Result}; -use log::debug; +use anyhow::{anyhow, bail, Context, Result}; +use flate2::read::GzDecoder; +use log::{debug, info}; use reqwest::{ blocking::{Client, Response}, Proxy, StatusCode, Url, }; +use tar::Archive; use tempfile::tempdir; use crate::{ config::{Configuration, Credentials}, signature::{SignatureVerifier, VerificationSuccess}, + webapp::Webapp, + LICENSES_FOLDER, REPOSITORY_INDEX_PATH, }; static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -33,6 +37,8 @@ impl Repository { pub fn new(config: &Configuration, verifier: SignatureVerifier) -> Result { let mut client = Client::builder() .use_native_tls() + // Enforce HTTPS at client level + .https_only(true) .user_agent(APP_USER_AGENT); if let Some(proxy_cfg) = &config.proxy { @@ -43,7 +49,15 @@ impl Repository { } client = client.proxy(proxy) } - let server = Url::parse(&config.url)?; + // We need to ensure URL ends with a slash + let url = if config.url.ends_with("plugins") { + let mut u = config.url.to_owned(); + u.push('/'); + u + } else { + config.url.to_owned() + }; + let server = Url::parse(&url)?; Ok(Self { inner: client.build()?, @@ -129,7 +143,44 @@ impl Repository { } pub fn test_connection(&self) -> Result<()> { - self.get("")?; + self.get("") + .context(format!("Could not connect with {}", self.server))?; + info!("Connection with {}: OK", self.server); + Ok(()) + } + + /// Update index and licences + pub fn update(&self, webapp: &Webapp) -> Result<()> { + // Update the index file + debug!("Updating repository index"); + let rudder_version = &webapp.version; + let remote_index = format!( + "{}.{}/rpkg.index", + rudder_version.major, rudder_version.minor + ); + self.download_unsafe(&remote_index, &PathBuf::from(REPOSITORY_INDEX_PATH))?; + + // Update the licenses + if let Some(user) = self.get_username() { + debug!("Updating licenses"); + let license_folder = PathBuf::from(LICENSES_FOLDER); + let archive_name = format!("{}-license.tar.gz", user); + let local_archive_path = &license_folder.clone().join(archive_name.clone()); + if let Err(e) = self.download_unsafe( + &format!("licenses/{}/{}", user, archive_name), + local_archive_path, + ) { + bail!( + "Could not download licenses from configured repository.\n{}", + e + ) + } + // Decompress archive + let mut archive = Archive::new(GzDecoder::new(File::open(local_archive_path)?)); + archive.unpack(license_folder)?; + } else { + debug!("Not updating licenses as no configured credentials were found") + } Ok(()) } } @@ -149,7 +200,7 @@ mod tests { let verifier = SignatureVerifier::new(PathBuf::from("tools/rudder_plugins_key.gpg")); let repo = Repository::new(&config, verifier).unwrap(); let dst = NamedTempFile::new().unwrap(); - repo.download_unsafe("rpm/rudder_rpm_key.pub", dst.path()) + repo.download_unsafe("../rpm/rudder_rpm_key.pub", dst.path()) .unwrap(); let contents = read_to_string(dst.path()).unwrap(); assert!(contents.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK----")) @@ -162,7 +213,7 @@ mod tests { let repo = Repository::new(&config, verifier).unwrap(); let dst = NamedTempFile::new().unwrap(); repo.download( - "plugins/8.0/consul/release/rudder-plugin-consul-8.0.3-2.1.rpkg", + "8.0/consul/release/rudder-plugin-consul-8.0.3-2.1.rpkg", dst.path(), ) .unwrap(); diff --git a/relay/sources/rudder-package/src/versions.rs b/relay/sources/rudder-package/src/versions.rs index 26d0f639bf7..75047a7d446 100644 --- a/relay/sources/rudder-package/src/versions.rs +++ b/relay/sources/rudder-package/src/versions.rs @@ -9,12 +9,18 @@ use log::debug; use regex::Regex; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct ArchiveVersion { pub rudder_version: RudderVersion, pub plugin_version: PluginVersion, } +impl Display for ArchiveVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}-{}", self.rudder_version, self.plugin_version) + } +} + impl FromStr for ArchiveVersion { type Err = Error; fn from_str(s: &str) -> std::result::Result { @@ -31,7 +37,7 @@ impl FromStr for ArchiveVersion { } } -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, PartialOrd, Ord)] pub enum RudderVersionMode { Alpha { version: u32 }, Beta { version: u32 }, @@ -110,7 +116,7 @@ impl FromStr for RudderVersionMode { // Checking if a rudder version is a nightly or not is not important for plugin compatibility // So it is not implemented -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, PartialOrd, Ord)] pub struct RudderVersion { pub major: u32, pub minor: u32, @@ -119,11 +125,8 @@ pub struct RudderVersion { } impl RudderVersion { - pub fn is_compatible(&self, webapp_version: &str) -> bool { - match RudderVersion::from_str(webapp_version) { - Ok(w) => *self == w, - Err(_) => false, - } + pub fn is_compatible(&self, webapp_version: &RudderVersion) -> bool { + self == webapp_version } pub fn from_path(path: &str) -> Result { @@ -137,7 +140,7 @@ impl RudderVersion { Some(c) => c, }; debug!( - "Raw Rudder version read from '{}' file: '{}'.", + "Rudder version read from '{}' file: '{}'.", path, &caps["raw_rudder_version"] ); RudderVersion::from_str(&caps["raw_rudder_version"]) @@ -201,22 +204,29 @@ impl FromStr for PluginVersion { }) } } + impl PartialOrd for PluginVersion { fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PluginVersion { + fn cmp(&self, other: &Self) -> Ordering { if self.major < other.major { - Some(Ordering::Less) + Ordering::Less } else if self.major > other.major { - Some(Ordering::Greater) + Ordering::Greater } else if self.minor < other.minor { - Some(Ordering::Less) + Ordering::Less } else if self.minor > other.minor { - Some(Ordering::Greater) + Ordering::Greater } else if self.nightly && !other.nightly { - Some(Ordering::Less) + Ordering::Less } else if !self.nightly && other.nightly { - Some(Ordering::Greater) + Ordering::Greater } else { - Some(Ordering::Equal) + Ordering::Equal } } } @@ -281,6 +291,19 @@ mod tests { let right = PluginVersion::from_str(b).unwrap(); assert!(left > right, "{:?} is not less than {:?}", left, right); } + + #[rstest] + #[case("8.0.0-1.0", "7.3.0-1.0")] + #[case("8.0.0-10.0", "8.0.0-2.0")] + #[case("8.0.0~alpha2-1.0", "8.0.0~alpha1-1.0")] + #[case("8.0.0-1.1", "8.0.0-1.0")] + #[case("8.0.0~beta1-1.0", "8.0.0~alpha1-2.0")] + fn test_archive_version_greater_than(#[case] a: &str, #[case] b: &str) { + let left = ArchiveVersion::from_str(a).unwrap(); + let right = ArchiveVersion::from_str(b).unwrap(); + assert!(left > right, "{:?} is not less than {:?}", left, right); + } + #[rstest] #[case("8.0.0-1.1")] #[case("8.0.0-1.1-nightly")] @@ -433,7 +456,9 @@ mod tests { ) { let m = ArchiveVersion::from_str(metadata_version).unwrap(); assert_eq!( - m.rudder_version.clone().is_compatible(webapp_version), + m.rudder_version + .clone() + .is_compatible(&RudderVersion::from_str(webapp_version).unwrap()), is_compatible, "Unexpected compatibility checkfor webapp version '{}' and metadata version {:?}'", webapp_version, diff --git a/relay/sources/rudder-package/src/webapp.rs b/relay/sources/rudder-package/src/webapp.rs index 46bf4996deb..2bc6fc4a549 100644 --- a/relay/sources/rudder-package/src/webapp.rs +++ b/relay/sources/rudder-package/src/webapp.rs @@ -1,32 +1,38 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2023 Normation SAS -use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; -use quick_xml::reader::Reader; -use quick_xml::Writer; -use std::collections::HashSet; -use std::fs; -use std::io::{Cursor, Write}; -use std::path::PathBuf; - -use std::process::Command; +use std::{ + collections::HashSet, + fs, + io::{Cursor, Write}, + path::PathBuf, + process::Command, +}; use anyhow::Result; use log::debug; +use quick_xml::{ + events::{BytesEnd, BytesStart, BytesText, Event}, + reader::Reader, + Writer, +}; +use spinners::{Spinner, Spinners}; -use crate::cmd::CmdOutput; +use crate::{cmd::CmdOutput, versions::RudderVersion}; /// We want to write the file after each plugin to avoid half-installs pub struct Webapp { pub path: PathBuf, pending_changes: bool, + pub version: RudderVersion, } impl Webapp { - pub fn new(path: PathBuf) -> Self { + pub fn new(path: PathBuf, version: RudderVersion) -> Self { Self { path, pending_changes: false, + version, } } @@ -81,18 +87,20 @@ impl Webapp { writer.write_event(Event::Start(e))?; } Event::Text(e) => { + // there are existing jars if in_extra_classpath { + in_extra_classpath = false; let jars_t = e.unescape()?; let mut jars: HashSet<&str> = HashSet::from_iter(jars_t.split(',')); for p in present { let changed = jars.insert(p); - if changed && !self.pending_changes { + if changed { self.pending_changes = true; } } for a in absent { let changed = jars.remove(a.as_str()); - if changed && !self.pending_changes { + if changed { self.pending_changes = true; } } @@ -102,7 +110,22 @@ impl Webapp { writer.write_event(Event::Text(e))?; } } + Event::End(e) if e.name().as_ref() == b"Set" => { + // there are no existing jars, but the section exists + if in_extra_classpath { + in_extra_classpath = false; + let mut jars: HashSet<&str> = HashSet::new(); + for p in present { + jars.insert(p); + self.pending_changes = true; + } + let jar_value: Vec<&str> = jars.into_iter().collect(); + writer.write_event(Event::Text(BytesText::new(&jar_value.join(","))))?; + } + writer.write_event(Event::End(e))?; + } Event::End(e) if e.name().as_ref() == b"Configure" => { + // there is not entry at all if !extra_classpath_found && !present.is_empty() { // Create the element if needed let mut start = BytesStart::new("Set"); @@ -151,13 +174,17 @@ impl Webapp { /// Synchronous restart of the web application pub fn apply_changes(&mut self) -> Result<()> { if self.pending_changes { - debug!("Restarting the Web application to apply plugin changes"); + let mut sp = Spinner::new( + Spinners::Dots, + "Restarting the Web application to apply changes".into(), + ); let mut systemctl = Command::new("/usr/bin/systemctl"); systemctl .arg("--no-ask-password") .arg("restart") .arg("rudder-jetty"); let _ = CmdOutput::new(&mut systemctl)?; + sp.stop_with_symbol("🗸"); self.pending_changes = false; } else { debug!("No need to restart the Web application"); @@ -178,12 +205,15 @@ mod tests { #[test] fn it_reads_jars() { - let w = Webapp::new(PathBuf::from("tests/webapp_xml/example.xml")); + let w = Webapp::new( + PathBuf::from("tests/webapp_xml/example.xml"), + RudderVersion::from_path("./tests/versions/rudder-server-version").unwrap(), + ); let jars = w.jars().unwrap(); assert_eq!( jars, vec![ - "/opt/rudder/share/plugins/auth-backends/auth-backends.jar", + "/opt/rudder/share/plugins/dsc/dsc.jar", "/opt/rudder/share/plugins/api-authorizations/api-authorizations.jar" ] ); @@ -198,7 +228,10 @@ mod tests { let expected = path::Path::new(&sample).with_extension("xml.expected"); let target = temp_dir.path().join(origin); fs::copy(sample, target.clone()).unwrap(); - let mut x = Webapp::new(target.clone()); + let mut x = Webapp::new( + target.clone(), + RudderVersion::from_path("./tests/versions/rudder-server-version").unwrap(), + ); let _ = x.enable_jars(&[String::from(jar_name)]); assert_eq!( fs::read_to_string(target).unwrap(), @@ -216,7 +249,10 @@ mod tests { let expected = path::Path::new(&sample).with_extension("xml.expected"); let target = temp_dir.path().join(origin); fs::copy(sample, target.clone()).unwrap(); - let mut x = Webapp::new(target.clone()); + let mut x = Webapp::new( + target.clone(), + RudderVersion::from_path("./tests/versions/rudder-server-version").unwrap(), + ); let _ = x.disable_jars(&[String::from(jar_name)]); assert_eq!( fs::read_to_string(target).unwrap(), diff --git a/relay/sources/rudder-package/tests/database/plugin_database_update_sample.json.expected b/relay/sources/rudder-package/tests/database/plugin_database_update_sample.json.expected index 5207dac0f82..53e9f32f856 100644 --- a/relay/sources/rudder-package/tests/database/plugin_database_update_sample.json.expected +++ b/relay/sources/rudder-package/tests/database/plugin_database_update_sample.json.expected @@ -241,6 +241,7 @@ "version": "8.0.0~rc2-2.2", "build-date": "2023-10-13T10:02:19+00:00", "build-commit": "45b05f29381170c7a61484a59dcffec550a9c531", + "jar-files": [], "depends": { "python": [ "python3-requests" @@ -286,6 +287,7 @@ "version": "0.0.0-0.0", "build-date": "2023-10-13T10:03:34+00:00", "build-commit": "2abc53fb8b2d1c667a91b1a1da2f941a99872cdf", + "jar-files": [], "content": { "files.txz": "/opt/rudder/share/plugins" } diff --git a/relay/sources/rudder-package/tests/licenses/example.license b/relay/sources/rudder-package/tests/licenses/example.license new file mode 100644 index 00000000000..906c111b1d9 --- /dev/null +++ b/relay/sources/rudder-package/tests/licenses/example.license @@ -0,0 +1,13 @@ +header=rudder-license-v1 +algorithm=SHA512WithRSA +digest=b2bc7a49db69a1c39ac6fa0cad4230edf5e9b4eb0 +digestdate=2023-09-20T19:26:36+00:00 +keyid=2a5ba55f +---- signed information about the license +licensee=Normation +softwareid=rudder-plugin-dsc +minversion=0.0-0.0 +maxversion=99.99-99.99 +startdate=2020-03-31T00:00:00Z +enddate=2024-09-28T12:00:00Z +---- diff --git a/relay/sources/rudder-package/tests/webapp_xml/example.xml b/relay/sources/rudder-package/tests/webapp_xml/example.xml index 8739d85db10..96964f68f92 100644 --- a/relay/sources/rudder-package/tests/webapp_xml/example.xml +++ b/relay/sources/rudder-package/tests/webapp_xml/example.xml @@ -6,7 +6,7 @@ /rudder /opt/rudder/share/webapps/rudder.war - /opt/rudder/share/plugins/auth-backends/auth-backends.jar,/opt/rudder/share/plugins/api-authorizations/api-authorizations.jar + /opt/rudder/share/plugins/dsc/dsc.jar,/opt/rudder/share/plugins/api-authorizations/api-authorizations.jar /var/rudder/tmp/jetty/jetty-rudder.war.dir diff --git a/relay/sources/rudder-package/tools/rudder-package.sh b/relay/sources/rudder-package/tools/rudder-package.sh new file mode 100755 index 00000000000..97becfd8450 --- /dev/null +++ b/relay/sources/rudder-package/tools/rudder-package.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# Entry point for the "rudder package" command. + +if [ "${RUDDER_PKG_COMPAT}" = "1" ]; then + exec /opt/rudder/share/python/rudder-pkg/rudder-pkg "$@" +else + exec /opt/rudder/bin/rudder-package "$@" +fi