use std::path::Path; use std::str::FromStr; use anyhow::anyhow; use clap::ArgMatches; use indexmap::{IndexMap, IndexSet}; use patch_db::HasModel; use rpc_toolkit::command; use serde::{Deserialize, Serialize}; use self::docker::DockerAction; use crate::config::{Config, ConfigSpec}; use crate::context::RpcContext; use crate::id::{Id, InvalidId}; use crate::s9pk::manifest::PackageId; use crate::util::{IoFormat, ValuePrimative, Version, display_serializable, parse_stdin_deserializable}; use crate::volume::Volumes; use crate::{Error, ResultExt}; pub mod docker; // TODO: create RPC endpoint that looks up the appropriate action and calls `execute` #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)] pub struct ActionId = String>(Id); impl FromStr for ActionId { type Err = InvalidId; fn from_str(s: &str) -> Result { Ok(ActionId(Id::try_from(s.to_owned())?)) } } impl From for String { fn from(value: ActionId) -> Self { value.0.into() } } impl> AsRef> for ActionId { fn as_ref(&self) -> &ActionId { self } } impl> std::fmt::Display for ActionId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self.0) } } impl> AsRef for ActionId { fn as_ref(&self) -> &str { self.0.as_ref() } } impl> AsRef for ActionId { fn as_ref(&self) -> &Path { self.0.as_ref().as_ref() } } impl<'de, S> Deserialize<'de> for ActionId where S: AsRef, Id: Deserialize<'de>, { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { Ok(ActionId(Deserialize::deserialize(deserializer)?)) } } #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Actions(pub IndexMap); #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "version")] pub enum ActionResult { #[serde(rename = "0")] V0(ActionResultV0), } #[derive(Debug, Serialize, Deserialize)] pub struct ActionResultV0 { pub message: String, pub value: ValuePrimative, pub copyable: bool, pub qr: bool, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum DockerStatus { Running, Stopped, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct Action { pub name: String, pub description: String, #[serde(default)] pub warning: Option, pub implementation: ActionImplementation, pub allowed_statuses: IndexSet, #[serde(default)] pub input_spec: ConfigSpec, } impl Action { pub async fn execute( &self, ctx: &RpcContext, pkg_id: &PackageId, pkg_version: &Version, action_id: &ActionId, volumes: &Volumes, input: Option, ) -> Result { if let Some(ref input) = input { self.input_spec .matches(&input) .with_kind(crate::ErrorKind::ConfigSpecViolation)?; } self.implementation .execute( ctx, pkg_id, pkg_version, Some(&format!("{}Action", action_id)), volumes, input, true, ) .await? .map_err(|e| Error::new(anyhow!("{}", e.1), crate::ErrorKind::Action)) } } #[derive(Clone, Debug, Deserialize, Serialize, HasModel)] #[serde(rename_all = "kebab-case")] #[serde(tag = "type")] pub enum ActionImplementation { Docker(DockerAction), } impl ActionImplementation { pub async fn execute Deserialize<'de>>( &self, ctx: &RpcContext, pkg_id: &PackageId, pkg_version: &Version, name: Option<&str>, volumes: &Volumes, input: Option, allow_inject: bool, ) -> Result, Error> { match self { ActionImplementation::Docker(action) => { action .execute(ctx, pkg_id, pkg_version, name, volumes, input, allow_inject) .await } } } pub async fn sandboxed Deserialize<'de>>( &self, ctx: &RpcContext, pkg_id: &PackageId, pkg_version: &Version, volumes: &Volumes, input: Option, ) -> Result, Error> { match self { ActionImplementation::Docker(action) => { action .sandboxed(ctx, pkg_id, pkg_version, volumes, input) .await } } } } fn display_action_result(action_result: ActionResult, matches: &ArgMatches<'_>) { if matches.is_present("format") { return display_serializable(action_result, matches); } match action_result { ActionResult::V0(ar) => { println!("{}: {}", ar.message, serde_json::to_string(&ar.value).unwrap()); }, } } #[command(about = "Executes an action", display(display_action_result))] pub async fn action( #[context] ctx: RpcContext, #[arg(rename = "id")] pkg_id: PackageId, #[arg(rename = "action-id")] action_id: ActionId, #[arg(stdin, parse(parse_stdin_deserializable))] input: Option, #[allow(unused_variables)] #[arg(long = "format")] format: Option, ) -> Result { let mut db = ctx.db.handle(); let manifest = crate::db::DatabaseModel::new() .package_data() .idx_model(&pkg_id) .and_then(|p| p.installed()) .expect(&mut db) .await .with_kind(crate::ErrorKind::NotFound)? .manifest() .get(&mut db, true) .await? .to_owned(); if let Some(action) = manifest.actions.0.get(&action_id) { action .execute( &ctx, &manifest.id, &manifest.version, &action_id, &manifest.volumes, input, ) .await } else { Err(Error::new( anyhow!("Action not found in manifest"), crate::ErrorKind::NotFound, )) } } pub struct NoOutput; impl<'de> Deserialize<'de> for NoOutput { fn deserialize(_: D) -> Result where D: serde::Deserializer<'de>, { Ok(NoOutput) } }