use std::collections::BTreeMap; use std::io::SeekFrom; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::Duration; use color_eyre::eyre::eyre; use emver::VersionRange; use futures::future::BoxFuture; use futures::{FutureExt, StreamExt, TryStreamExt}; use http::header::CONTENT_LENGTH; use http::{Request, Response, StatusCode}; use hyper::Body; use models::{mime, DataUrl}; use reqwest::Url; use rpc_toolkit::command; use rpc_toolkit::yajrc::RpcError; use serde_json::{json, Value}; use tokio::fs::{File, OpenOptions}; use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWriteExt}; use tokio::process::Command; use tokio::sync::oneshot; use tokio_stream::wrappers::ReadDirStream; use tracing::instrument; use self::cleanup::{cleanup_failed, remove_from_current_dependents_lists}; use crate::config::ConfigureContext; use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::db::model::{ CurrentDependencies, CurrentDependencyInfo, CurrentDependents, InstalledPackageInfo, PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryInstalling, PackageDataEntryMatchModelRef, PackageDataEntryRemoving, PackageDataEntryRestoring, PackageDataEntryUpdating, StaticDependencyInfo, StaticFiles, }; use crate::dependencies::{ add_dependent_to_current_dependents_lists, compute_dependency_config_errs, set_dependents_with_live_pointers_to_needs_config, }; use crate::install::cleanup::cleanup; use crate::install::progress::{InstallProgress, InstallProgressTracker}; use crate::notifications::NotificationLevel; use crate::prelude::*; use crate::registry::marketplace::with_query_params; use crate::s9pk::manifest::{Manifest, PackageId}; use crate::s9pk::reader::S9pkReader; use crate::status::{MainStatus, Status}; use crate::util::docker::CONTAINER_TOOL; use crate::util::io::response_to_reader; use crate::util::serde::{display_serializable, Port}; use crate::util::{display_none, AsyncFileExt, Invoke, Version}; use crate::volume::{asset_dir, script_dir}; use crate::{Error, ErrorKind, ResultExt}; pub mod cleanup; pub mod progress; pub const PKG_ARCHIVE_DIR: &str = "package-data/archive"; pub const PKG_PUBLIC_DIR: &str = "package-data/public"; pub const PKG_WASM_DIR: &str = "package-data/wasm"; #[command(display(display_serializable))] pub async fn list(#[context] ctx: RpcContext) -> Result { Ok(ctx.db.peek().await.as_package_data().as_entries()? .iter() .filter_map(|(id, pde)| { let status = match pde.as_match() { PackageDataEntryMatchModelRef::Installed(_) => { "installed" } PackageDataEntryMatchModelRef::Installing(_) => { "installing" } PackageDataEntryMatchModelRef::Updating(_) => { "updating" } PackageDataEntryMatchModelRef::Restoring(_) => { "restoring" } PackageDataEntryMatchModelRef::Removing(_) => { "removing" } PackageDataEntryMatchModelRef::Error(_) => { "error" } }; serde_json::to_value(json!({ "status":status, "id": id.clone(), "version": pde.as_manifest().as_version().de().ok()?})) .ok() }) .collect()) } #[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] pub enum MinMax { Min, Max, } impl Default for MinMax { fn default() -> Self { MinMax::Max } } impl std::str::FromStr for MinMax { type Err = Error; fn from_str(s: &str) -> Result { match s { "min" => Ok(MinMax::Min), "max" => Ok(MinMax::Max), _ => Err(Error::new( eyre!("Must be one of \"min\", \"max\"."), crate::ErrorKind::ParseVersion, )), } } } impl std::fmt::Display for MinMax { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MinMax::Min => write!(f, "min"), MinMax::Max => write!(f, "max"), } } } #[command( custom_cli(cli_install(async, context(CliContext))), display(display_none), metadata(sync_db = true) )] #[instrument(skip_all)] pub async fn install( #[context] ctx: RpcContext, #[arg] id: String, #[arg(short = 'm', long = "marketplace-url", rename = "marketplace-url")] marketplace_url: Option, #[arg(short = 'v', long = "version-spec", rename = "version-spec")] version_spec: Option< String, >, #[arg(long = "version-priority", rename = "version-priority")] version_priority: Option, ) -> Result<(), Error> { let version_str = match &version_spec { None => "*", Some(v) => &*v, }; let version: VersionRange = version_str.parse()?; let marketplace_url = marketplace_url.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap()); let version_priority = version_priority.unwrap_or_default(); let man: Manifest = ctx .client .get(with_query_params( ctx.clone(), format!( "{}/package/v0/manifest/{}?spec={}&version-priority={}", marketplace_url, id, version, version_priority, ) .parse()?, )) .send() .await .with_kind(crate::ErrorKind::Registry)? .error_for_status() .with_kind(crate::ErrorKind::Registry)? .json() .await .with_kind(crate::ErrorKind::Registry)?; let s9pk = ctx .client .get(with_query_params( ctx.clone(), format!( "{}/package/v0/{}.s9pk?spec=={}&version-priority={}", marketplace_url, id, man.version, version_priority, ) .parse()?, )) .send() .await .with_kind(crate::ErrorKind::Registry)? .error_for_status()?; if *man.id != *id || !man.version.satisfies(&version) { return Err(Error::new( eyre!("Fetched package does not match requested id and version"), ErrorKind::Registry, )); } let public_dir_path = ctx .datadir .join(PKG_PUBLIC_DIR) .join(&man.id) .join(man.version.as_str()); tokio::fs::create_dir_all(&public_dir_path).await?; let icon_type = man.assets.icon_type(); let (license_res, instructions_res, icon_res) = tokio::join!( async { tokio::io::copy( &mut response_to_reader( ctx.client .get(with_query_params( ctx.clone(), format!( "{}/package/v0/license/{}?spec=={}", marketplace_url, id, man.version, ) .parse()?, )) .send() .await? .error_for_status()?, ), &mut File::create(public_dir_path.join("LICENSE.md")).await?, ) .await?; Ok::<_, color_eyre::eyre::Report>(()) }, async { tokio::io::copy( &mut response_to_reader( ctx.client .get(with_query_params( ctx.clone(), format!( "{}/package/v0/instructions/{}?spec=={}", marketplace_url, id, man.version, ) .parse()?, )) .send() .await? .error_for_status()?, ), &mut File::create(public_dir_path.join("INSTRUCTIONS.md")).await?, ) .await?; Ok::<_, color_eyre::eyre::Report>(()) }, async { tokio::io::copy( &mut response_to_reader( ctx.client .get(with_query_params( ctx.clone(), format!( "{}/package/v0/icon/{}?spec=={}", marketplace_url, id, man.version, ) .parse()?, )) .send() .await? .error_for_status()?, ), &mut File::create(public_dir_path.join(format!("icon.{}", icon_type))).await?, ) .await?; Ok::<_, color_eyre::eyre::Report>(()) }, ); if let Err(e) = license_res { tracing::warn!("Failed to pre-download license: {}", e); } if let Err(e) = instructions_res { tracing::warn!("Failed to pre-download instructions: {}", e); } if let Err(e) = icon_res { tracing::warn!("Failed to pre-download icon: {}", e); } let progress = Arc::new(InstallProgress::new(s9pk.content_length())); let static_files = StaticFiles::local(&man.id, &man.version, icon_type); ctx.db .mutate(|db| { let pde = match db .as_package_data() .as_idx(&man.id) .map(|x| x.de()) .transpose()? { Some(PackageDataEntry::Installed(PackageDataEntryInstalled { installed, static_files, .. })) => PackageDataEntry::Updating(PackageDataEntryUpdating { install_progress: progress.clone(), static_files, installed, manifest: man.clone(), }), None => PackageDataEntry::Installing(PackageDataEntryInstalling { install_progress: progress.clone(), static_files, manifest: man.clone(), }), _ => { return Err(Error::new( eyre!("Cannot install over a package in a transient state"), crate::ErrorKind::InvalidRequest, )) } }; db.as_package_data_mut().insert(&man.id, &pde) }) .await?; let downloading = download_install_s9pk( ctx.clone(), man.clone(), Some(marketplace_url), Arc::new(InstallProgress::new(s9pk.content_length())), response_to_reader(s9pk), None, ); tokio::spawn(async move { if let Err(e) = downloading.await { let err_str = format!("Install of {}@{} Failed: {}", man.id, man.version, e); tracing::error!("{}", err_str); tracing::debug!("{:?}", e); if let Err(e) = ctx .notification_manager .notify( ctx.db.clone(), Some(man.id), NotificationLevel::Error, String::from("Install Failed"), err_str, (), None, ) .await { tracing::error!("Failed to issue Notification: {}", e); tracing::debug!("{:?}", e); } } Ok::<_, String>(()) }); Ok(()) } #[command(rpc_only, display(display_none))] #[instrument(skip_all)] pub async fn sideload( #[context] ctx: RpcContext, #[arg] manifest: Manifest, #[arg] icon: Option, ) -> Result { let new_ctx = ctx.clone(); let guid = RequestGuid::new(); if let Some(icon) = icon { use tokio::io::AsyncWriteExt; let public_dir_path = ctx .datadir .join(PKG_PUBLIC_DIR) .join(&manifest.id) .join(manifest.version.as_str()); tokio::fs::create_dir_all(&public_dir_path).await?; let invalid_data_url = || Error::new(eyre!("Invalid Icon Data URL"), ErrorKind::InvalidRequest); let data = icon .strip_prefix(&format!( "data:image/{};base64,", manifest.assets.icon_type() )) .ok_or_else(&invalid_data_url)?; let mut icon_file = File::create(public_dir_path.join(format!("icon.{}", manifest.assets.icon_type()))) .await?; icon_file .write_all(&base64::decode(data).with_kind(ErrorKind::InvalidRequest)?) .await?; icon_file.sync_all().await?; } let handler = Box::new(|req: Request| { async move { let content_length = match req.headers().get(CONTENT_LENGTH).map(|a| a.to_str()) { None => None, Some(Err(_)) => { return Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from("Invalid Content Length")) .with_kind(ErrorKind::Network) } Some(Ok(a)) => match a.parse::() { Err(_) => { return Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from("Invalid Content Length")) .with_kind(ErrorKind::Network) } Ok(a) => Some(a), }, }; let progress = Arc::new(InstallProgress::new(content_length)); let install_progress = progress.clone(); new_ctx .db .mutate(|db| { let pde = match db .as_package_data() .as_idx(&manifest.id) .map(|x| x.de()) .transpose()? { Some(PackageDataEntry::Installed(PackageDataEntryInstalled { installed, static_files, .. })) => PackageDataEntry::Updating(PackageDataEntryUpdating { install_progress, installed, manifest: manifest.clone(), static_files, }), None => PackageDataEntry::Installing(PackageDataEntryInstalling { install_progress, static_files: StaticFiles::local( &manifest.id, &manifest.version, &manifest.assets.icon_type(), ), manifest: manifest.clone(), }), _ => { return Err(Error::new( eyre!("Cannot install over a package in a transient state"), crate::ErrorKind::InvalidRequest, )) } }; db.as_package_data_mut().insert(&manifest.id, &pde) }) .await?; let (send, recv) = oneshot::channel(); tokio::spawn(async move { if let Err(e) = download_install_s9pk( new_ctx.clone(), manifest.clone(), None, progress, tokio_util::io::StreamReader::new(req.into_body().map_err(|e| { std::io::Error::new( match &e { e if e.is_connect() => std::io::ErrorKind::ConnectionRefused, e if e.is_timeout() => std::io::ErrorKind::TimedOut, _ => std::io::ErrorKind::Other, }, e, ) })), Some(send), ) .await { let err_str = format!( "Install of {}@{} Failed: {}", manifest.id, manifest.version, e ); tracing::error!("{}", err_str); tracing::debug!("{:?}", e); if let Err(e) = new_ctx .notification_manager .notify( new_ctx.db.clone(), Some(manifest.id.clone()), NotificationLevel::Error, String::from("Install Failed"), err_str, (), None, ) .await { tracing::error!("Failed to issue Notification: {}", e); tracing::debug!("{:?}", e); } } }); if let Ok(_) = recv.await { Response::builder() .status(StatusCode::OK) .body(Body::empty()) .with_kind(ErrorKind::Network) } else { Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from("installation aborted before upload completed")) .with_kind(ErrorKind::Network) } } .boxed() }); ctx.add_continuation( guid.clone(), RpcContinuation::rest(handler, Duration::from_secs(30)), ) .await; Ok(guid) } #[instrument(skip_all)] async fn cli_install( ctx: CliContext, target: String, marketplace_url: Option, version_spec: Option, version_priority: Option, ) -> Result<(), RpcError> { if target.ends_with(".s9pk") { let path = PathBuf::from(target); // inspect manifest no verify let mut reader = S9pkReader::open(&path, false).await?; let manifest = reader.manifest().await?; let icon = reader.icon().await?.to_vec().await?; let icon_str = format!( "data:image/{};base64,{}", manifest.assets.icon_type(), base64::encode(&icon) ); // rpc call remote sideload tracing::debug!("calling package.sideload"); let guid = rpc_toolkit::command_helpers::call_remote( ctx.clone(), "package.sideload", serde_json::json!({ "manifest": manifest, "icon": icon_str }), PhantomData::, ) .await? .result?; tracing::debug!("package.sideload succeeded {:?}", guid); // hit continuation api with guid that comes back let file = tokio::fs::File::open(path).await?; let content_length = file.metadata().await?.len(); let body = Body::wrap_stream(tokio_util::io::ReaderStream::new(file)); let res = ctx .client .post(format!("{}rest/rpc/{}", ctx.base_url, guid,)) .header(CONTENT_LENGTH, content_length) .body(body) .send() .await?; if res.status().as_u16() == 200 { tracing::info!("Package Uploaded") } else { tracing::info!("Package Upload failed: {}", res.text().await?) } } else { let params = match (target.split_once("@"), version_spec) { (Some((pkg, v)), None) => { serde_json::json!({ "id": pkg, "marketplace-url": marketplace_url, "version-spec": v, "version-priority": version_priority }) } (Some(_), Some(_)) => { return Err(crate::Error::new( eyre!("Invalid package id {}", target), ErrorKind::InvalidRequest, ) .into()) } (None, Some(v)) => { serde_json::json!({ "id": target, "marketplace-url": marketplace_url, "version-spec": v, "version-priority": version_priority }) } (None, None) => { serde_json::json!({ "id": target, "marketplace-url": marketplace_url, "version-priority": version_priority }) } }; tracing::debug!("calling package.install"); rpc_toolkit::command_helpers::call_remote( ctx, "package.install", params, PhantomData::<()>, ) .await? .result?; tracing::debug!("package.install succeeded"); } Ok(()) } #[command(display(display_none), metadata(sync_db = true))] pub async fn uninstall( #[context] ctx: RpcContext, #[arg] id: PackageId, ) -> Result { ctx.db .mutate(|db| { let (manifest, static_files, installed) = match db.as_package_data().as_idx(&id).or_not_found(&id)?.de()? { PackageDataEntry::Installed(PackageDataEntryInstalled { manifest, static_files, installed, }) => (manifest, static_files, installed), _ => { return Err(Error::new( eyre!("Package is not installed."), crate::ErrorKind::NotFound, )); } }; let pde = PackageDataEntry::Removing(PackageDataEntryRemoving { manifest, static_files, removing: installed, }); db.as_package_data_mut().insert(&id, &pde) }) .await?; let return_id = id.clone(); tokio::spawn(async move { if let Err(e) = async { cleanup::uninstall(&ctx, ctx.secret_store.acquire().await?.as_mut(), &id).await } .await { let err_str = format!("Uninstall of {} Failed: {}", id, e); tracing::error!("{}", err_str); tracing::debug!("{:?}", e); if let Err(e) = ctx .notification_manager .notify( ctx.db.clone(), // allocating separate handle here because the lifetime of the previous one is the expression Some(id), NotificationLevel::Error, String::from("Uninstall Failed"), err_str, (), None, ) .await { tracing::error!("Failed to issue Notification: {}", e); tracing::debug!("{:?}", e); } } }); Ok(return_id) } #[instrument(skip_all)] pub async fn download_install_s9pk( ctx: RpcContext, temp_manifest: Manifest, marketplace_url: Option, progress: Arc, mut s9pk: impl AsyncRead + Unpin, download_complete: Option>, ) -> Result<(), Error> { let pkg_id = &temp_manifest.id; let version = &temp_manifest.version; let db = ctx.db.peek().await; if let Result::<(), Error>::Err(e) = { let ctx = ctx.clone(); async move { // // Build set of existing manifests let mut manifests = Vec::new(); for (_id, pkg) in db.as_package_data().as_entries()? { let m = pkg.as_manifest().de()?; manifests.push(m); } // Build map of current port -> ssl mappings let port_map = ssl_port_status(&manifests); tracing::info!("SSL Port Map: {:?}", &port_map); // if any of the requested interface lan configs conflict with current state, fail the install for (_id, iface) in &temp_manifest.interfaces.0 { if let Some(cfg) = &iface.lan_config { for (p, lan) in cfg { if p.0 == 80 && lan.ssl || p.0 == 443 && !lan.ssl { return Err(Error::new( eyre!("SSL Conflict with StartOS"), ErrorKind::LanPortConflict, )); } match port_map.get(&p) { Some((ssl, pkg)) => { if *ssl != lan.ssl { return Err(Error::new( eyre!("SSL Conflict with package: {}", pkg), ErrorKind::LanPortConflict, )); } } None => { continue; } } } } } let pkg_archive_dir = ctx .datadir .join(PKG_ARCHIVE_DIR) .join(pkg_id) .join(version.as_str()); tokio::fs::create_dir_all(&pkg_archive_dir).await?; let pkg_archive = pkg_archive_dir.join(AsRef::::as_ref(pkg_id).with_extension("s9pk")); File::delete(&pkg_archive).await?; let mut dst = OpenOptions::new() .create(true) .write(true) .read(true) .open(&pkg_archive) .await?; progress .track_download_during(ctx.db.clone(), pkg_id, || async { let mut progress_writer = InstallProgressTracker::new(&mut dst, progress.clone()); tokio::io::copy(&mut s9pk, &mut progress_writer).await?; progress.download_complete(); if let Some(complete) = download_complete { complete.send(()).unwrap_or_default(); } Ok(()) }) .await?; dst.seek(SeekFrom::Start(0)).await?; let progress_reader = InstallProgressTracker::new(dst, progress.clone()); let mut s9pk_reader = progress .track_read_during(ctx.db.clone(), pkg_id, || { S9pkReader::from_reader(progress_reader, true) }) .await?; install_s9pk( ctx.clone(), pkg_id, version, marketplace_url, &mut s9pk_reader, progress, ) .await?; Ok(()) } } .await { if let Err(e) = cleanup_failed(&ctx, pkg_id).await { tracing::error!("Failed to clean up {}@{}: {}", pkg_id, version, e); tracing::debug!("{:?}", e); } Err(e) } else { Ok::<_, Error>(()) } } #[instrument(skip_all)] pub async fn install_s9pk( ctx: RpcContext, pkg_id: &PackageId, version: &Version, marketplace_url: Option, rdr: &mut S9pkReader>, progress: Arc, ) -> Result<(), Error> { rdr.validate().await?; rdr.validated(); let developer_key = rdr.developer_key().clone(); rdr.reset().await?; let db = ctx.db.peek().await; tracing::info!("Install {}@{}: Unpacking Manifest", pkg_id, version); let manifest = progress .track_read_during(ctx.db.clone(), pkg_id, || rdr.manifest()) .await?; tracing::info!("Install {}@{}: Unpacked Manifest", pkg_id, version); tracing::info!("Install {}@{}: Fetching Dependency Info", pkg_id, version); let mut dependency_info = BTreeMap::new(); for (dep, info) in &manifest.dependencies.0 { let manifest: Option = if let Some(local_man) = db .as_package_data() .as_idx(dep) .map(|pde| pde.as_manifest().de()) { Some(local_man?) } else if let Some(marketplace_url) = &marketplace_url { match ctx .client .get(with_query_params( ctx.clone(), format!( "{}/package/v0/manifest/{}?spec={}", marketplace_url, dep, info.version, ) .parse()?, )) .send() .await .with_kind(crate::ErrorKind::Registry)? .error_for_status() { Ok(a) => Ok(Some( a.json() .await .with_kind(crate::ErrorKind::Deserialization)?, )), Err(e) if e.status() == Some(StatusCode::BAD_REQUEST) || e.status() == Some(StatusCode::NOT_FOUND) => { Ok(None) } Err(e) => Err(e), } .with_kind(crate::ErrorKind::Registry)? } else { None }; let icon_path = if let Some(manifest) = &manifest { let dir = ctx .datadir .join(PKG_PUBLIC_DIR) .join(&manifest.id) .join(manifest.version.as_str()); let icon_path = dir.join(format!("icon.{}", manifest.assets.icon_type())); if tokio::fs::metadata(&icon_path).await.is_err() { if let Some(marketplace_url) = &marketplace_url { tokio::fs::create_dir_all(&dir).await?; let icon = ctx .client .get(with_query_params( ctx.clone(), format!( "{}/package/v0/icon/{}?spec={}", marketplace_url, dep, info.version, ) .parse()?, )) .send() .await .with_kind(crate::ErrorKind::Registry)?; let mut dst = File::create(&icon_path).await?; tokio::io::copy(&mut response_to_reader(icon), &mut dst).await?; dst.sync_all().await?; Some(icon_path) } else { None } } else { Some(icon_path) } } else { None }; dependency_info.insert( dep.clone(), StaticDependencyInfo { title: manifest .as_ref() .map(|x| x.title.clone()) .unwrap_or_else(|| dep.to_string()), icon: if let Some(icon_path) = &icon_path { DataUrl::from_path(icon_path).await? } else { DataUrl::from_slice("image/png", include_bytes!("./package-icon.png")) }, }, ); } tracing::info!("Install {}@{}: Fetched Dependency Info", pkg_id, version); let public_dir_path = ctx .datadir .join(PKG_PUBLIC_DIR) .join(pkg_id) .join(version.as_str()); tokio::fs::create_dir_all(&public_dir_path).await?; tracing::info!("Install {}@{}: Unpacking LICENSE.md", pkg_id, version); progress .track_read_during(ctx.db.clone(), pkg_id, || async { let license_path = public_dir_path.join("LICENSE.md"); let mut dst = File::create(&license_path).await?; tokio::io::copy(&mut rdr.license().await?, &mut dst).await?; dst.sync_all().await?; Ok(()) }) .await?; tracing::info!("Install {}@{}: Unpacked LICENSE.md", pkg_id, version); tracing::info!("Install {}@{}: Unpacking INSTRUCTIONS.md", pkg_id, version); progress .track_read_during(ctx.db.clone(), pkg_id, || async { let instructions_path = public_dir_path.join("INSTRUCTIONS.md"); let mut dst = File::create(&instructions_path).await?; tokio::io::copy(&mut rdr.instructions().await?, &mut dst).await?; dst.sync_all().await?; Ok(()) }) .await?; tracing::info!("Install {}@{}: Unpacked INSTRUCTIONS.md", pkg_id, version); let icon_filename = Path::new("icon").with_extension(manifest.assets.icon_type()); let icon_path = public_dir_path.join(&icon_filename); tracing::info!( "Install {}@{}: Unpacking {}", pkg_id, version, icon_path.display() ); let icon_buf = progress .track_read_during(ctx.db.clone(), pkg_id, || async { Ok(rdr.icon().await?.to_vec().await?) }) .await?; let mut dst = File::create(&icon_path).await?; dst.write_all(&icon_buf).await?; dst.sync_all().await?; let icon = DataUrl::from_vec( mime(manifest.assets.icon_type()).unwrap_or("image/png"), icon_buf, ); tracing::info!( "Install {}@{}: Unpacked {}", pkg_id, version, icon_filename.display() ); tracing::info!("Install {}@{}: Unpacking Docker Images", pkg_id, version); progress .track_read_during(ctx.db.clone(), pkg_id, || async { Command::new(CONTAINER_TOOL) .arg("load") .input(Some(&mut rdr.docker_images().await?)) .invoke(ErrorKind::Docker) .await }) .await?; tracing::info!("Install {}@{}: Unpacked Docker Images", pkg_id, version,); tracing::info!("Install {}@{}: Unpacking Assets", pkg_id, version); progress .track_read_during(ctx.db.clone(), pkg_id, || async { let asset_dir = asset_dir(&ctx.datadir, pkg_id, version); if tokio::fs::metadata(&asset_dir).await.is_err() { tokio::fs::create_dir_all(&asset_dir).await?; } let mut tar = tokio_tar::Archive::new(rdr.assets().await?); tar.unpack(asset_dir).await?; let script_dir = script_dir(&ctx.datadir, pkg_id, version); if tokio::fs::metadata(&script_dir).await.is_err() { tokio::fs::create_dir_all(&script_dir).await?; } if let Some(mut hdl) = rdr.scripts().await? { tokio::io::copy( &mut hdl, &mut File::create(script_dir.join("embassy.js")).await?, ) .await?; } Ok(()) }) .await?; tracing::info!("Install {}@{}: Unpacked Assets", pkg_id, version); progress.unpack_complete.store(true, Ordering::SeqCst); progress .track_read( ctx.db.clone(), pkg_id.clone(), Arc::new(::std::sync::atomic::AtomicBool::new(true)), ) .await?; let peek = ctx.db.peek().await; let prev = peek .as_package_data() .as_idx(pkg_id) .or_not_found(pkg_id)? .de()?; let mut sql_tx = ctx.secret_store.begin().await?; tracing::info!("Install {}@{}: Creating volumes", pkg_id, version); manifest.volumes.install(&ctx, pkg_id, version).await?; tracing::info!("Install {}@{}: Created volumes", pkg_id, version); tracing::info!("Install {}@{}: Installing interfaces", pkg_id, version); let interface_addresses = manifest.interfaces.install(sql_tx.as_mut(), pkg_id).await?; tracing::info!( "Install {}@{}: Installed interfaces {:?}", pkg_id, version, interface_addresses ); tracing::info!("Install {}@{}: Creating manager", pkg_id, version); let manager = ctx.managers.add(ctx.clone(), manifest.clone()).await?; tracing::info!("Install {}@{}: Created manager", pkg_id, version); let static_files = StaticFiles::local(pkg_id, version, manifest.assets.icon_type()); let current_dependencies: CurrentDependencies = CurrentDependencies( manifest .dependencies .0 .iter() .filter_map(|(id, info)| { if info.requirement.required() { Some((id.clone(), CurrentDependencyInfo::default())) } else { None } }) .collect(), ); let mut dependents_static_dependency_info = BTreeMap::new(); let current_dependents = { let mut deps = BTreeMap::new(); for package in db.as_package_data().keys()? { if db .as_package_data() .as_idx(&package) .or_not_found(&package)? .as_installed() .and_then(|i| i.as_dependency_info().as_idx(&pkg_id)) .is_some() { dependents_static_dependency_info.insert(package.clone(), icon.clone()); } if let Some(dep) = db .as_package_data() .as_idx(&package) .or_not_found(&package)? .as_installed() .and_then(|i| i.as_current_dependencies().as_idx(pkg_id)) { deps.insert(package, dep.de()?); } } CurrentDependents(deps) }; let installed = InstalledPackageInfo { status: Status { configured: manifest.config.is_none(), main: MainStatus::Stopped, dependency_config_errors: compute_dependency_config_errs( &ctx, &peek, &manifest, ¤t_dependencies, &Default::default(), ) .await?, }, marketplace_url, developer_key, manifest: manifest.clone(), last_backup: match prev { PackageDataEntry::Updating(PackageDataEntryUpdating { installed: InstalledPackageInfo { last_backup: Some(time), .. }, .. }) => Some(time), _ => None, }, dependency_info, current_dependents: current_dependents.clone(), current_dependencies: current_dependencies.clone(), interface_addresses, }; let mut next = PackageDataEntryInstalled { installed, manifest: manifest.clone(), static_files, }; let mut auto_start = false; let mut configured = false; if let PackageDataEntry::Updating(PackageDataEntryUpdating { installed: prev, .. }) = &prev { let prev_is_configured = prev.status.configured; let prev_migration = prev .manifest .migrations .to( &ctx, version, pkg_id, &prev.manifest.version, &prev.manifest.volumes, ) .map(futures::future::Either::Left); let migration = manifest .migrations .from( &manifest.containers, &ctx, &prev.manifest.version, pkg_id, version, &manifest.volumes, ) .map(futures::future::Either::Right); let viable_migration = if prev.manifest.version > manifest.version { prev_migration.or(migration) } else { migration.or(prev_migration) }; if let Some(f) = viable_migration { configured = f.await?.configured && prev_is_configured; } if configured || manifest.config.is_none() { auto_start = prev.status.main.running(); } if &prev.manifest.version != version { cleanup(&ctx, &prev.manifest.id, &prev.manifest.version).await?; } } else if let PackageDataEntry::Restoring(PackageDataEntryRestoring { .. }) = prev { next.installed.marketplace_url = manifest .backup .restore(&ctx, pkg_id, version, &manifest.volumes) .await?; } sql_tx.commit().await?; let to_configure = ctx .db .mutate(|db| { for (package, icon) in dependents_static_dependency_info { db.as_package_data_mut() .as_idx_mut(&package) .or_not_found(&package)? .as_installed_mut() .or_not_found(&package)? .as_dependency_info_mut() .insert( &pkg_id, &StaticDependencyInfo { icon, title: manifest.title.clone(), }, )?; } db.as_package_data_mut() .insert(&pkg_id, &PackageDataEntry::Installed(next))?; if let PackageDataEntry::Updating(PackageDataEntryUpdating { installed: prev, .. }) = &prev { remove_from_current_dependents_lists(db, pkg_id, &prev.current_dependencies)?; } add_dependent_to_current_dependents_lists(db, pkg_id, ¤t_dependencies)?; set_dependents_with_live_pointers_to_needs_config(db, pkg_id) }) .await?; if configured && manifest.config.is_some() { let breakages = BTreeMap::new(); let overrides = Default::default(); let configure_context = ConfigureContext { breakages, timeout: None, config: None, dry_run: false, overrides, }; manager.configure(configure_context).await?; } for to_configure in to_configure.into_iter().filter(|(dep, _)| dep != pkg_id) { if let Err(e) = async { ctx.managers .get(&to_configure) .await .or_not_found(format!("manager for {}", to_configure.0))? .configure(ConfigureContext { breakages: BTreeMap::new(), timeout: None, config: None, overrides: BTreeMap::new(), dry_run: false, }) .await } .await { tracing::error!("error configuring dependent: {e}"); tracing::debug!("{e:?}") } } if auto_start { manager.start().await; } tracing::info!("Install {}@{}: Complete", pkg_id, version); Ok(()) } #[instrument(skip_all)] pub fn load_images<'a, P: AsRef + 'a + Send + Sync>( datadir: P, ) -> BoxFuture<'a, Result<(), Error>> { async move { let docker_dir = datadir.as_ref(); if tokio::fs::metadata(&docker_dir).await.is_ok() { ReadDirStream::new(tokio::fs::read_dir(&docker_dir).await?) .map(|r| { r.with_ctx(|_| (crate::ErrorKind::Filesystem, format!("{:?}", &docker_dir))) }) .try_for_each(|entry| async move { let m = entry.metadata().await?; if m.is_file() { let path = entry.path(); let ext = path.extension().and_then(|ext| ext.to_str()); if ext == Some("tar") || ext == Some("s9pk") { if let Err(e) = async { match ext { Some("tar") => { Command::new(CONTAINER_TOOL) .arg("load") .input(Some(&mut File::open(&path).await?)) .invoke(ErrorKind::Docker) .await } Some("s9pk") => { Command::new(CONTAINER_TOOL) .arg("load") .input(Some( &mut S9pkReader::open(&path, true) .await? .docker_images() .await?, )) .invoke(ErrorKind::Docker) .await } _ => unreachable!(), } } .await { tracing::error!("Error loading docker images from s9pk: {e}"); tracing::debug!("{e:?}"); } Ok(()) } else { Ok(()) } } else if m.is_dir() { load_images(entry.path()).await?; Ok(()) } else { Ok(()) } }) .await } else { Ok(()) } } .boxed() } fn ssl_port_status(manifests: &Vec) -> BTreeMap { let mut ret = BTreeMap::new(); for m in manifests { for (_id, iface) in &m.interfaces.0 { match &iface.lan_config { None => {} Some(cfg) => { for (p, lan) in cfg { ret.insert(p.clone(), (lan.ssl, m.id.clone())); } } } } } ret }