summaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
authorLukasz Woznicki <75632179+makr11st@users.noreply.github.com>2021-11-24 20:54:56 +0000
committerGitHub <noreply@github.com>2021-11-24 20:54:56 +0000
commita4ffeccf60090e4456755bc53a6e3b8c8038e855 (patch)
tree9583f187114913a92866571920dd3bb205bd50a3 /plugins
parent8217e80670e76dbf9168780f5e0545355a39f8f3 (diff)
Restructure directories of the workspace (#559)
* Restructure directories of the workspace * Rename c8y_translator_lib to c8y_translator * Update comment on how to get dummy plugin path Signed-off-by: Lukasz Woznicki <lukasz.woznicki@softwareag.com>
Diffstat (limited to 'plugins')
-rw-r--r--plugins/tedge_apama_plugin/Cargo.toml15
-rw-r--r--plugins/tedge_apama_plugin/src/error.rs26
-rw-r--r--plugins/tedge_apama_plugin/src/main.rs175
-rw-r--r--plugins/tedge_apt_plugin/Cargo.toml28
-rw-r--r--plugins/tedge_apt_plugin/src/error.rs33
-rw-r--r--plugins/tedge_apt_plugin/src/main.rs262
-rw-r--r--plugins/tedge_apt_plugin/src/module_check.rs76
-rw-r--r--plugins/tedge_apt_plugin/tests/main.rs163
-rw-r--r--plugins/tedge_docker_plugin/tedge_docker_plugin.sh112
-rw-r--r--plugins/tedge_dummy_plugin/Cargo.toml11
-rw-r--r--plugins/tedge_dummy_plugin/src/main.rs102
-rw-r--r--plugins/tedge_dummy_plugin/src/tests.rs0
12 files changed, 1003 insertions, 0 deletions
diff --git a/plugins/tedge_apama_plugin/Cargo.toml b/plugins/tedge_apama_plugin/Cargo.toml
new file mode 100644
index 00000000..bc0c155a
--- /dev/null
+++ b/plugins/tedge_apama_plugin/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "tedge_apama_plugin"
+version = "0.4.3"
+authors = ["thin-edge.io team <info@thin-edge.io>"]
+edition = "2018"
+license = "Apache-2.0"
+description = "thin.edge.io plugin for installing apama projects"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+structopt = "0.3"
+thiserror = "1.0"
+serde = { version = "1", features = ["derive"] }
+zip = "0.5"
diff --git a/plugins/tedge_apama_plugin/src/error.rs b/plugins/tedge_apama_plugin/src/error.rs
new file mode 100644
index 00000000..0e32850c
--- /dev/null
+++ b/plugins/tedge_apama_plugin/src/error.rs
@@ -0,0 +1,26 @@
+#[derive(thiserror::Error, Debug)]
+pub enum InternalError {
+ #[error("Fail to run `{cmd}`: {from}")]
+ ExecError { cmd: String, from: std::io::Error },
+
+ #[error(transparent)]
+ FromIo(#[from] std::io::Error),
+
+ #[error(transparent)]
+ FromUtf8(#[from] std::string::FromUtf8Error),
+
+ #[error(transparent)]
+ FromZipError(#[from] zip::result::ZipError),
+
+ #[error("Apama not installed at /opt/softwareag/Apama")]
+ ApamaNotInstalled,
+}
+
+impl InternalError {
+ pub fn exec_error(cmd: impl Into<String>, from: std::io::Error) -> InternalError {
+ InternalError::ExecError {
+ cmd: cmd.into(),
+ from,
+ }
+ }
+}
diff --git a/plugins/tedge_apama_plugin/src/main.rs b/plugins/tedge_apama_plugin/src/main.rs
new file mode 100644
index 00000000..de71c9c1
--- /dev/null
+++ b/plugins/tedge_apama_plugin/src/main.rs
@@ -0,0 +1,175 @@
+mod error;
+
+use crate::error::InternalError;
+use std::fs::{self, File};
+use std::os::unix::prelude::ExitStatusExt;
+use std::path::Path;
+use std::process::{Command, ExitStatus, Stdio};
+use structopt::StructOpt;
+
+/// This plugin supports the installation, update and removal of a single unversioned apama project named "project".
+/// Installation of multiple parallel projects is not supported.
+/// Installing a project will replace the existing project with the new one.
+/// Delta update of a project(for eg: updating just the `mon` file definitions in the project) is not supported either.
+#[derive(StructOpt)]
+struct ApamaCli {
+ #[structopt(subcommand)]
+ operation: PluginOp,
+}
+
+#[derive(StructOpt)]
+pub enum PluginOp {
+ /// List the one and only apama project if one is installed
+ List,
+
+ /// Install an apama project
+ Install {
+ module: String,
+ #[structopt(short = "v", long = "--module-version")]
+ version: Option<String>,
+ #[structopt(long = "--file")]
+ file_path: String,
+ },
+
+ /// Remove an apama project
+ Remove {
+ module: String,
+ #[structopt(short = "v", long = "--module-version")]
+ version: Option<String>,
+ },
+
+ /// Prepare a sequences of install/remove commands
+ Prepare,
+
+ /// Finalize a sequences of install/remove commands
+ Finalize,
+}
+
+const APAMA_ENV_EXE: &str = "/opt/softwareag/Apama/bin/apama_env";
+const TEDGE_APAMA_PROJECT_DIR: &str = "/etc/tedge/apama/project";
+const TMP_APAMA_PROJECT_DIR: &str = "/tmp/tedge_apama_project";
+const APAMA_PROJECT_NAME: &str = "project";
+
+fn run(operation: PluginOp) -> Result<ExitStatus, InternalError> {
+ let success = ExitStatus::from_raw(0);
+
+ let tedge_env_exe_path = Path::new(APAMA_ENV_EXE);
+
+ if !tedge_env_exe_path.exists() {
+ return Err(InternalError::ApamaNotInstalled);
+ }
+
+ let tedge_apama_project_path = Path::new(TEDGE_APAMA_PROJECT_DIR);
+ let tmp_apama_project_path = Path::new(TMP_APAMA_PROJECT_DIR);
+
+ let status = match operation {
+ // Since there can only be a single project named `project`, print its name if installed
+ PluginOp::List => {
+ if tedge_apama_project_path.exists() {
+ println!("{}\t", APAMA_PROJECT_NAME)
+ }
+ success
+ }
+
+ PluginOp::Prepare => success,
+
+ PluginOp::Finalize => {
+ // Cleanup any temporary artefacts created by this plugin
+ if tmp_apama_project_path.exists() {
+ fs::remove_dir_all(tmp_apama_project_path)?;
+ }
+ success
+ }
+
+ PluginOp::Install {
+ module: _,
+ version: _,
+ file_path,
+ } => {
+ let archive_path = Path::new(&file_path);
+ let archive_file = File::open(&archive_path)?;
+
+ let mut archive = zip::ZipArchive::new(archive_file)?;
+
+ // TODO: Validate the zip to be valid apama project before extraction?
+ println!("Extracting the archive at {}", file_path);
+ archive.extract(tmp_apama_project_path)?;
+ println!("Extraction successful");
+
+ // Deleting existing project as the rename API expects the target dir to be empty
+ if tedge_apama_project_path.exists() {
+ println!("Removing existing project at {}", TEDGE_APAMA_PROJECT_DIR);
+ fs::remove_dir_all(tedge_apama_project_path)?;
+ println!("Removal of existing project successful");
+ }
+
+ println!(
+ "Installing newly extracted project to {}",
+ TEDGE_APAMA_PROJECT_DIR
+ );
+ fs::create_dir_all(tedge_apama_project_path)?;
+ fs::rename(tmp_apama_project_path, tedge_apama_project_path)?;
+ println!("Installation of new project successful");
+
+ println!("Restarting apama to load the new project");
+ run_cmd("service", "apama restart")?;
+ println!("Restart of apama service successful");
+
+ success
+ }
+
+ PluginOp::Remove {
+ module: _,
+ version: _,
+ } => {
+ if tedge_apama_project_path.exists() {
+ println!("Stopping apama service");
+ run_cmd("service", "apama stop")?;
+ println!("Stopping apama service successful");
+
+ println!("Removing existing project at {}", TEDGE_APAMA_PROJECT_DIR);
+ fs::remove_dir_all(tedge_apama_project_path)?;
+ println!("Removal of existing project successful");
+ }
+
+ success
+ }
+ };
+
+ Ok(status)
+}
+
+fn run_cmd(cmd: &str, args: &str) -> Result<ExitStatus, InternalError> {
+ let args: Vec<&str> = args.split_whitespace().collect();
+ let status = Command::new(cmd)
+ .args(args)
+ .stdin(Stdio::null())
+ .status()
+ .map_err(|err| InternalError::exec_error(cmd, err))?;
+ Ok(status)
+}
+
+fn main() {
+ // On usage error, the process exits with a status code of 1
+ let apama = ApamaCli::from_args();
+
+ match run(apama.operation) {
+ Ok(status) if status.success() => {
+ std::process::exit(0);
+ }
+
+ Ok(status) => {
+ if status.code().is_some() {
+ std::process::exit(2);
+ } else {
+ eprintln!("Interrupted by a signal!");
+ std::process::exit(4);
+ }
+ }
+
+ Err(err) => {
+ eprintln!("ERROR: {}", err);
+ std::process::exit(5);
+ }
+ }
+}
diff --git a/plugins/tedge_apt_plugin/Cargo.toml b/plugins/tedge_apt_plugin/Cargo.toml
new file mode 100644
index 00000000..0d7ca421
--- /dev/null
+++ b/plugins/tedge_apt_plugin/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "tedge_apt_plugin"
+version = "0.4.3"
+authors = ["thin-edge.io team <info@thin-edge.io>"]
+edition = "2018"
+license = "Apache-2.0"
+description = "Thin.edge.io plugin for software management using apt"
+
+[package.metadata.deb]
+assets = [
+ ["target/release/tedge_apt_plugin", "/etc/tedge/sm-plugins/apt", "755"],
+]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+structopt = "0.3"
+thiserror = "1.0"
+csv = "1.1"
+serde = { version = "1", features = ["derive"] }
+
+[dev-dependencies]
+anyhow = "1.0"
+hamcrest2 = "0.3"
+reqwest = { version = "0.11", default-features = false, features = [ "blocking", "rustls-tls" ] }
+serial_test = "0.5"
+tedge_utils = { path = "../../crates/common/tedge_utils" }
+test-case = "1.2"
diff --git a/plugins/tedge_apt_plugin/src/error.rs b/plugins/tedge_apt_plugin/src/error.rs
new file mode 100644
index 00000000..07c8399a
--- /dev/null
+++ b/plugins/tedge_apt_plugin/src/error.rs
@@ -0,0 +1,33 @@
+#[derive(thiserror::Error, Debug)]
+pub enum InternalError {
+ #[error("Fail to run `{cmd}`: {from}")]
+ ExecError { cmd: String, from: std::io::Error },
+
+ #[error(transparent)]
+ FromIo(#[from] std::io::Error),
+
+ #[error(transparent)]
+ FromUtf8(#[from] std::string::FromUtf8Error),
+
+ #[error("Parsing Debian package failed for `{file}`")]
+ ParsingError { file: String },
+
+ #[error(transparent)]
+ FromCsv(#[from] csv::Error),
+
+ #[error("Validation of {package} failed with version mismatch. Installed version: {installed}, Expected version: {expected}")]
+ VersionMismatch {
+ package: String,
+ installed: String,
+ expected: String,
+ },
+}
+
+impl InternalError {
+ pub fn exec_error(cmd: impl Into<String>, from: std::io::Error) -> InternalError {
+ InternalError::ExecError {
+ cmd: cmd.into(),
+ from,
+ }
+ }
+}
diff --git a/plugins/tedge_apt_plugin/src/main.rs b/plugins/tedge_apt_plugin/src/main.rs
new file mode 100644
index 00000000..44a0e0c8
--- /dev/null
+++ b/plugins/tedge_apt_plugin/src/main.rs
@@ -0,0 +1,262 @@
+mod error;
+mod module_check;
+
+use crate::error::InternalError;
+use crate::module_check::PackageMetadata;
+use serde::Deserialize;
+use std::io::{self};
+use std::process::{Command, ExitStatus, Stdio};
+use structopt::StructOpt;
+
+#[derive(StructOpt)]
+struct AptCli {
+ #[structopt(subcommand)]
+ operation: PluginOp,
+}
+
+#[derive(StructOpt)]
+pub enum PluginOp {
+ /// List all the installed modules
+ List,
+
+ /// Install a module
+ Install {
+ module: String,
+ #[structopt(short = "v", long = "--module-version")]
+ version: Option<String>,
+ #[structopt(long = "--file")]
+ file_path: Option<String>,
+ },
+
+ /// Uninstall a module
+ Remove {
+ module: String,
+ #[structopt(short = "v", long = "--module-version")]
+ version: Option<String>,
+ },
+
+ /// Install or remove multiple modules at once
+ UpdateList,
+
+ /// Prepare a sequences of install/remove commands
+ Prepare,
+
+ /// Finalize a sequences of install/remove commands
+ Finalize,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "lowercase")]
+enum UpdateAction {
+ Install,
+ Remove,
+}
+#[derive(Debug, Deserialize)]
+struct SoftwareModuleUpdate {
+ pub action: UpdateAction,
+ pub name: String,
+ #[serde(default)]
+ pub version: Option<String>,
+ #[serde(default)]
+ pub path: Option<String>,
+}
+
+fn run(operation: PluginOp) -> Result<ExitStatus, InternalError> {
+ let status = match operation {
+ PluginOp::List {} => {
+ let apt = Command::new("apt")
+ .args(vec!["--manual-installed", "list"])
+ .stdout(Stdio::piped()) // To pipe apt.stdout into awk.stdin
+ .spawn()
+ .map_err(|err| InternalError::exec_error("apt", err))?;
+
+ // apt output = openssl/focal-security,now 1.1.1f-1ubuntu2.3 amd64 [installed]
+ // awk -F '[/ ]' = $1 ^ $2 ^ $3 ^ $4
+ // awk print = name ^ ^ version ^
+ let status = Command::new("awk")
+ .args(vec![
+ "-F",
+ "[/ ]",
+ r#"{if ($1 != "Listing...") { print $1"\t"$3}}"#,
+ ])
+ .stdin(apt.stdout.unwrap()) // Cannot panic: apt.stdout has been set
+ .status()
+ .map_err(|err| InternalError::exec_error("awk", err))?;
+
+ status
+ }
+
+ PluginOp::Install {
+ module,
+ version,
+ file_path,
+ } => {
+ let (installer, _metadata) = get_installer(module, version, file_path)?;
+ run_cmd("apt-get", &format!("install --quiet --yes {}", installer))?
+ }
+
+ PluginOp::Remove { module, version } => {
+ if let Some(version) = version {
+ // check the version mentioned present or not
+ run_cmd(
+ "apt-get",
+ &format!("remove --quiet --yes {}={}", module, version),
+ )?
+ } else {
+ run_cmd("apt-get", &format!("remove --quiet --yes {}", module))?
+ }
+ }
+
+ PluginOp::UpdateList => {
+ let mut updates: Vec<SoftwareModuleUpdate> = Vec::new();
+ let mut rdr = csv::ReaderBuilder::new()
+ .has_headers(false)
+ .delimiter(b'\t')
+ .from_reader(io::stdin());
+ for result in rdr.deserialize() {
+ updates.push(result?);
+ }
+
+ // Maintaining this metadata list to keep the debian package symlinks until the installation is complete,
+ // which will get cleaned up once it goes out of scope after this block
+ let mut metadata_vec = Vec::new();
+ let mut args: Vec<String> = Vec::new();
+ args.push("install".into());
+ args.push("--quiet".into());
+ args.push("--yes".into());
+ for update_module in updates {
+ match update_module.action {
+ UpdateAction::Install => {
+ let (installer, metadata) = get_installer(
+ update_module.name,
+ update_module.version,
+ update_module.path,
+ )?;
+ args.push(installer);
+ metadata_vec.push(metadata);
+ }
+ UpdateAction::Remove => {
+ if let Some(version) = update_module.version {
+ validate_version(update_module.name.as_str(), version.as_str())?
+ }
+
+ // Adding a '-' at the end of the package name like 'rolldice-' instructs apt to treat it as removal
+ args.push(format!("{}-", update_module.name))
+ }
+ };
+ }
+
+ println!("apt-get install args: {:?}", args);
+ let status = Command::new("apt-get")
+ .args(args)
+ .stdin(Stdio::null())
+ .status()
+ .map_err(|err| InternalError::exec_error("apt-get", err))?;
+
+ return Ok(status);
+ }
+
+ PluginOp::Prepare => run_cmd("apt-get", "update --quiet --yes")?,
+
+ PluginOp::Finalize => run_cmd("apt-get", "auto-remove --quiet --yes")?,
+ };
+
+ Ok(status)
+}
+
+fn get_installer(
+ module: String,
+ version: Option<String>,
+ file_path: Option<String>,
+) -> Result<(String, Option<PackageMetadata>), InternalError> {
+ match (&version, &file_path) {
+ (None, None) => Ok((module, None)),
+
+ (Some(version), None) => Ok((format!("{}={}", module, version), None)),
+
+ (None, Some(file_path)) => {
+ let mut package = PackageMetadata::try_new(file_path)?;
+ let () =
+ package.validate_package(&[&format!("Package: {}", &module), "Debian package"])?;
+
+ Ok((format!("{}", package.file_path().display()), Some(package)))
+ }
+
+ (Some(version), Some(file_path)) => {
+ let mut package = PackageMetadata::try_new(file_path)?;
+ let () = package.validate_package(&[
+ &format!("Version: {}", &version),
+ &format!("Package: {}", &module),
+ "Debian package",
+ ])?;
+
+ Ok((format!("{}", package.file_path().display()), Some(package)))
+ }
+ }
+}
+
+/// Validate if the provided module version matches the currently installed version
+fn validate_version(module_name: &str, module_version: &str) -> Result<(), InternalError> {
+ // Get the current installed version of the provided package
+ let output = Command::new("apt")
+ .arg("list")
+ .arg("--installed")
+ .arg(module_name)
+ .output()
+ .map_err(|err| InternalError::exec_error("apt-get", err))?;
+
+ let stdout = String::from_utf8(output.stdout)?;
+
+ // Check if the installed version and the provided version match
+ let second_line = stdout.lines().nth(1); //Ignore line 0 which is always 'Listing...'
+ if let Some(package_info) = second_line {
+ if let Some(installed_version) = package_info.split_whitespace().nth(1)
+ // Value at index 0 is the package name
+ {
+ if installed_version != module_version {
+ return Err(InternalError::VersionMismatch {
+ package: module_name.into(),
+ installed: installed_version.into(),
+ expected: module_version.into(),
+ });
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn run_cmd(cmd: &str, args: &str) -> Result<ExitStatus, InternalError> {
+ let args: Vec<&str> = args.split_whitespace().collect();
+ let status = Command::new(cmd)
+ .args(args)
+ .stdin(Stdio::null())
+ .status()
+ .map_err(|err| InternalError::exec_error(cmd, err))?;
+ Ok(status)
+}
+
+fn main() {
+ // On usage error, the process exits with a status code of 1
+ let apt = AptCli::from_args();
+
+ match run(apt.operation) {
+ Ok(status) if status.success() => {
+ std::process::exit(0);
+ }
+
+ Ok(status) => {
+ if status.code().is_some() {
+ std::process::exit(2);
+ } else {
+ eprintln!("Interrupted by a signal!");
+ std::process::exit(4);
+ }
+ }
+
+ Err(err) => {
+ eprintln!("ERROR: {}", err);
+ std::process::exit(5);
+ }
+ }
+}
diff --git a/plugins/tedge_apt_plugin/src/module_check.rs b/plugins/tedge_apt_plugin/src/module_check.rs
new file mode 100644
index 00000000..e6680700
--- /dev/null
+++ b/plugins/tedge_apt_plugin/src/module_check.rs
@@ -0,0 +1,76 @@
+use crate::error::InternalError;
+use std::process::{Command, Stdio};
+use std::{
+ ffi::OsStr,
+ path::{Path, PathBuf},
+};
+
+pub struct PackageMetadata {
+ file_path: PathBuf,
+ metadata: String,
+ remove_modified: bool,
+}
+
+impl PackageMetadata {
+ pub fn try_new(file_path: &str) -> Result<Self, InternalError> {
+ let metadata = String::from_utf8(Self::get_module_metadata(file_path)?)?;
+
+ Ok(Self {
+ file_path: PathBuf::from(file_path),
+ metadata,
+ remove_modified: false,
+ })
+ }
+
+ fn metadata_contains_all(&self, patterns: &[&str]) -> bool {
+ for pattern in patterns {
+ if !&self.metadata.contains(pattern) {
+ return false;
+ };
+ }
+ true
+ }
+
+ fn get_module_metadata(file_path: &str) -> Result<Vec<u8>, InternalError> {
+ Ok(Command::new("dpkg")
+ .arg("-I")
+ .arg(file_path)
+ .stdout(Stdio::piped())
+ .output()?
+ .stdout)
+ }
+
+ pub fn validate_package(&mut self, contain_args: &[&str]) -> Result<(), InternalError> {
+ if self.metadata_contains_all(contain_args) {
+ // In the current implementation using `apt-get` it is required that the file has '.deb' extension (if we use dpkg extension doesn't matter).
+ if self.file_path.extension() != Some(OsStr::new("deb")) {
+ let new_path = PathBuf::from(format!(
+ "{}.deb",
+ self.file_path().to_string_lossy().to_string()
+ ));
+
+ let _res = std::os::unix::fs::symlink(self.file_path(), &new_path);
+ self.file_path = new_path;
+ self.remove_modified = true;
+ }
+
+ Ok(())
+ } else {
+ Err(InternalError::ParsingError {
+ file: self.file_path().to_string_lossy().to_string(),
+ })
+ }
+ }
+
+ pub fn file_path(&self) -> &Path {
+ &self.file_path
+ }
+}
+
+impl Drop for PackageMetadata {
+ fn drop(&mut self) {
+ if self.remove_modified {
+ let _res = std::fs::remove_file(&self.file_path);
+ }
+ }
+}
diff --git a/plugins/tedge_apt_plugin/tests/main.rs b/plugins/tedge_apt_plugin/tests/main.rs
new file mode 100644
index 00000000..c1fcb5b5
--- /dev/null
+++ b/plugins/tedge_apt_plugin/tests/main.rs
@@ -0,0 +1,163 @@
+use hamcrest2::prelude::*;
+use serial_test::serial;
+use std::process::{Command, Stdio};
+use std::sync::Once;
+use tedge_utils::fs::atomically_write_file_sync;
+use test_case::test_case;
+
+const TEDGE_APT_COMMAND: &str = "/etc/tedge/sm-plugins/apt";
+const APT_COMMAND: &str = "/usr/bin/apt-get";
+const PACKAGE_NAME: &str = "rolldice";
+const PACKAGE_VERSION: &str = "1.16-1+b3";
+
+#[cfg(target_arch = "x86_64")]
+const PACKAGE_URL: &str =
+ "http://ftp.br.debian.org/debian/pool/main/r/rolldice/rolldice_1.16-1+b3_amd64.deb";
+
+#[cfg(target_arch = "x86_64")]
+const PACKAGE_FILE_PATH: &str = "/tmp/rolldice_1.16-1+b3_amd64.deb";
+
+#[cfg(target_arch = "aarch64")]
+const PACKAGE_URL: &str =
+ "http://ftp.br.debian.org/debian/pool/main/r/rolldice/rolldice_1.16-1+b3_arm64.deb";
+
+#[cfg(target_arch = "aarch64")]
+const PACKAGE_FILE_PATH: &str = "/tmp/rolldice_1.16-1+b3_arm64.deb";
+
+#[cfg(target_arch = "arm")]
+const PACKAGE_URL: &str =
+ "http://ftp.br.debian.org/debian/pool/main/r/rolldice/rolldice_1.16-1+b3_armhf.deb";
+
+#[cfg(target_arch = "arm")]
+const PACKAGE_FILE_PATH: &str = "/tmp/rolldice_1.16-1+b3_armhf.deb";
+
+static DOWNLOAD_PACKAGE_BINARY: Once = Once::new();
+
+pub fn download_package_binary_once() {
+ DOWNLOAD_PACKAGE_BINARY.call_once_force(|_state| {
+ simple_download(PACKAGE_URL);
+ });
+}
+
+fn simple_download(url: &str) {
+ let response = reqwest::blocking::get(url).unwrap();
+ let content = response.bytes().unwrap();
+
+ atomically_write_file_sync("/tmp/rolldice.deb", PACKAGE_FILE_PATH, content.as_ref()).unwrap();
+}
+
+/// executes a `cmd` with `args`
+/// returns the stdout, stderr and exit code
+fn run_cmd(cmd: &str, args: &str) -> anyhow::Result<(String, String, i32)> {
+ let args = args.split_whitespace().collect::<Vec<&str>>();
+ let output = Command::new(cmd)
+ .args(args)
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()?;
+
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ let stderr = String::from_utf8(output.stderr).unwrap();
+ let status_code = output.status.code().unwrap();
+ Ok((stdout, stderr, status_code))
+}
+
+// Parameters:
+//
+// input command
+//
+// expected stderr
+//
+// expected exit code
+//
+// description about the test
+#[test_case(
+ &format!("install {} --file {}", PACKAGE_NAME, "wrong_path"),
+ "ERROR: Parsing Debian package failed",
+ 5
+ ; "wrong path"
+)]
+#[test_case(
+ &format!("install {} --file {} --module-version {}", PACKAGE_NAME, "not/a/package/path", PACKAGE_VERSION),
+ "ERROR: Parsing Debian package failed",
+ 5
+ ; "wrong path with right version"
+)]
+#[test_case(
+ &format!("install {} --file {} --module-version {}", PACKAGE_NAME, PACKAGE_FILE_PATH, "some_version"),
+ "ERROR: Parsing Debian package failed",
+ 5
+ ; "right path with wrong version"
+)]
+#[test_case(
+ &format!("install {} --file {} --module-version {}", PACKAGE_NAME, "not/a/package/path", "some_version"),
+ "ERROR: Parsing Debian package failed",
+ 5
+ ; "wrong path with wrong version"
+)]
+fn install_from_local_file_fail(
+ input_command: &str,
+ expected_stderr: &str,
+ expected_exit_code: i32,
+) -> anyhow::Result<()> {
+ // no setup needed, wrong arguments are provided to tedge apt plugin
+ let (stdout, stderr, exit_code) = run_cmd(TEDGE_APT_COMMAND, input_command)?;
+
+ // asserting command failed
+ assert_that!(stdout.is_empty(), true);
+ assert_that!(stderr, match_regex(expected_stderr));
+ assert_that!(exit_code, eq(expected_exit_code));
+ Ok(())
+}
+
+// Parameters:
+//
+// input command
+//
+// expected stderr
+//
+// expected exit code
+//
+// description about the test
+#[test_case(
+ &format!("install {} --file {}", PACKAGE_NAME, PACKAGE_FILE_PATH),
+ &format!("The following NEW packages will be installed\n {}", PACKAGE_NAME),
+ 0
+ ; "path"
+)]
+#[serial]
+#[test_case(
+ &format!("install {} --file {} --module-version {}", PACKAGE_NAME, PACKAGE_FILE_PATH, PACKAGE_VERSION),
+ &format!("The following NEW packages will be installed\n {}", PACKAGE_NAME),
+ 0
+ ; "path with version"
+)]
+#[serial]
+#[test_case(
+ &format!("install {} --module-version {} --file {}", PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_FILE_PATH),
+ &format!("The following NEW packages will be installed\n {}", PACKAGE_NAME),
+ 0
+ ; "version with path"
+)]
+#[serial]
+fn install_from_local_file_success(
+ input_command: &str,
+ expected_stdout: &str,
+ expected_exit_code: i32,
+) -> anyhow::Result<()> {
+ // fetching the debian package & removing rolldice in case it is already installed.
+ // only executed once.
+ download_package_binary_once();
+ let _ = run_cmd(APT_COMMAND, &format!("remove {} -y", PACKAGE_NAME))?;
+
+ // execute command to install from local file
+ let (stdout, stderr, exit_code) = run_cmd(TEDGE_APT_COMMAND, input_command)?;
+
+ // asserting success
+ assert_that!(stdout, match_regex(expected_stdout));
+ assert_that!(stderr.is_empty(), true);
+ assert_that!(exit_code, eq(expected_exit_code));
+
+ Ok(())
+}
diff --git a/plugins/tedge_docker_plugin/tedge_docker_plugin.sh b/plugins/tedge_docker_plugin/tedge_docker_plugin.sh
new file mode 100644
index 00000000..9aee79ea
--- /dev/null
+++ b/plugins/tedge_docker_plugin/tedge_docker_plugin.sh
@@ -0,0 +1,112 @@
+#!/bin/sh
+
+usage() {
+ cat << EOF
+USAGE:
+ docker <SUBCOMMAND>
+
+SUBCOMMANDS:
+ list List all the installed modules
+ prepare Prepare a sequences of install/remove commands
+ install Install a module
+ remove Uninstall a module
+ finalize Finalize a sequences of install/remove commands
+EOF
+}
+
+unsupported_args_check() {
+ if ! [ -z $1 ]; then
+ echo "Unsupported arguments: $@"
+ exit 1
+ fi
+}
+
+extract_image_tag_from_args() {
+ IMAGE_NAME="$1"
+ if [ -z "$IMAGE_NAME" ]; then
+ echo "Image name is a mandatory argument"
+ exit 1
+ fi
+ shift # Pop image name from args list
+ IMAGE_TAG=$IMAGE_NAME
+
+ if ! [ -z $1 ]; then
+ case "$1" in
+ --module-version)
+ IMAGE_VERSION="$2"
+ IMAGE_TAG=$IMAGE_NAME:$IMAGE_VERSION
+ shift 2 # Pop --version and the version value from the args list
+ ;;
+ *)
+ echo "Unsupported argument option: $1"
+ exit 1
+ ;;