0.2.5 initial commit

Makefile incomplete
This commit is contained in:
Aiden McClelland
2020-11-23 13:44:28 -07:00
commit 95d3845906
503 changed files with 53448 additions and 0 deletions

440
appmgr/src/apps.rs Normal file
View File

@@ -0,0 +1,440 @@
use failure::ResultExt as _;
use futures::future::{BoxFuture, FutureExt, OptionFuture};
use linear_map::{set::LinearSet, LinearMap};
use rand::SeedableRng;
use crate::dependencies::AppDependencies;
use crate::manifest::{Manifest, ManifestLatest};
use crate::util::Apply;
use crate::util::{from_yaml_async_reader, PersistencePath, YamlUpdateHandle};
use crate::Error;
use crate::ResultExt as _;
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DockerStatus {
Running,
Stopped, // created || exited
Paused,
Restarting,
Removing,
Dead,
}
fn not(b: &bool) -> bool {
!b
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppInfo {
pub title: String,
pub version: emver::Version,
pub tor_address: Option<String>,
pub configured: bool,
#[serde(default)]
#[serde(skip_serializing_if = "not")]
pub recoverable: bool,
#[serde(default)]
#[serde(skip_serializing_if = "not")]
pub needs_restart: bool,
}
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppStatus {
pub status: DockerStatus,
}
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppConfig {
pub spec: crate::config::ConfigSpec,
pub rules: Vec<crate::config::ConfigRuleEntry>,
pub config: Option<crate::config::Config>,
}
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppInfoFull {
#[serde(flatten)]
pub info: AppInfo,
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<AppStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest: Option<ManifestLatest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<AppConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<AppDependencies>,
}
pub async fn list_info() -> Result<LinearMap<String, AppInfo>, Error> {
let apps_path = PersistencePath::from_ref("apps.yaml");
let mut f = match apps_path.maybe_read(false).await.transpose()? {
Some(a) => a,
None => return Ok(LinearMap::new()),
};
from_yaml_async_reader(&mut *f).await
}
pub async fn list_info_mut() -> Result<YamlUpdateHandle<LinearMap<String, AppInfo>>, Error> {
let apps_path = PersistencePath::from_ref("apps.yaml");
YamlUpdateHandle::new_or_default(apps_path).await
}
pub async fn add(id: &str, info: AppInfo) -> Result<(), failure::Error> {
let mut apps = list_info_mut().await?;
apps.insert(id.to_string(), info);
apps.commit().await?;
Ok(())
}
pub async fn set_configured(id: &str, configured: bool) -> Result<(), Error> {
let mut apps = list_info_mut().await?;
let mut app = apps
.get_mut(id)
.ok_or_else(|| failure::format_err!("App Not Installed: {}", id))
.with_code(crate::error::NOT_FOUND)?;
app.configured = configured;
apps.commit().await?;
Ok(())
}
pub async fn set_needs_restart(id: &str, needs_restart: bool) -> Result<(), Error> {
let mut apps = list_info_mut().await?;
let mut app = apps
.get_mut(id)
.ok_or_else(|| failure::format_err!("App Not Installed: {}", id))
.with_code(crate::error::NOT_FOUND)?;
app.needs_restart = needs_restart;
apps.commit().await?;
Ok(())
}
pub async fn set_recoverable(id: &str, recoverable: bool) -> Result<(), Error> {
let mut apps = list_info_mut().await?;
let mut app = apps
.get_mut(id)
.ok_or_else(|| failure::format_err!("App Not Installed: {}", id))
.with_code(crate::error::NOT_FOUND)?;
app.recoverable = recoverable;
apps.commit().await?;
Ok(())
}
pub async fn remove(id: &str) -> Result<(), failure::Error> {
let mut apps = list_info_mut().await?;
apps.remove(id);
apps.commit().await?;
Ok(())
}
pub async fn status(id: &str) -> Result<AppStatus, Error> {
let output = std::process::Command::new("docker")
.args(&["inspect", id, "--format", "{{.State.Status}}"])
.stdout(std::process::Stdio::piped())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.spawn()?
.wait_with_output()?;
crate::ensure_code!(
output.status.success(),
crate::error::DOCKER_ERROR,
"{}: Docker Error: {}",
id,
std::str::from_utf8(&output.stderr).no_code()?
);
let status = std::str::from_utf8(&output.stdout).no_code()?;
Ok(AppStatus {
status: match status.trim() {
"running" => DockerStatus::Running,
"restarting" => DockerStatus::Restarting,
"removing" => DockerStatus::Removing,
"dead" => DockerStatus::Dead,
"created" | "exited" => DockerStatus::Stopped,
"paused" => DockerStatus::Paused,
_ => Err(format_err!("unknown status: {}", status))?,
},
})
}
pub async fn manifest(id: &str) -> Result<ManifestLatest, Error> {
let manifest: Manifest = from_yaml_async_reader(
&mut *PersistencePath::from_ref("apps")
.join(id)
.join("manifest.yaml")
.read(false)
.await?,
)
.await?;
Ok(manifest.into_latest())
}
pub async fn config(id: &str) -> Result<AppConfig, Error> {
let spec = PersistencePath::from_ref("apps")
.join(id)
.join("config_spec.yaml");
let spec: crate::config::ConfigSpec =
crate::util::from_yaml_async_reader(&mut *spec.read(false).await?)
.await
.no_code()?;
let rules = PersistencePath::from_ref("apps")
.join(id)
.join("config_rules.yaml");
let rules: Vec<crate::config::ConfigRuleEntry> =
crate::util::from_yaml_async_reader(&mut *rules.read(false).await?)
.await
.no_code()?;
let config = PersistencePath::from_ref("apps")
.join(id)
.join("config.yaml");
let config: Option<crate::config::Config> = match config
.maybe_read(false)
.await
.transpose()?
.map(|mut f| async move { from_yaml_async_reader(&mut *f).await })
.apply(OptionFuture::from)
.await
{
Some(Ok(cfg)) => Some(cfg),
#[cfg(not(feature = "production"))]
Some(Err(e)) => return Err(e),
_ => {
let volume_config = std::path::Path::new(crate::VOLUMES)
.join(id)
.join("start9")
.join("config.yaml");
if volume_config.exists() {
let cfg_path = config.path();
tokio::fs::copy(&volume_config, &cfg_path)
.await
.with_context(|e| {
format!(
"{}: {} -> {}",
e,
volume_config.display(),
cfg_path.display()
)
})
.with_code(crate::error::FILESYSTEM_ERROR)?;
let mut f = tokio::fs::File::open(&volume_config)
.await
.with_context(|e| format!("{}: {}", e, volume_config.display()))
.with_code(crate::error::FILESYSTEM_ERROR)?;
match from_yaml_async_reader(&mut f).await {
Ok(a) => Some(a),
#[cfg(not(feature = "production"))]
Err(e) => return Err(e),
#[cfg(feature = "production")]
_ => None,
}
} else {
None
}
}
};
Ok(AppConfig {
spec,
rules,
config,
})
}
pub async fn config_or_default(id: &str) -> Result<crate::config::Config, Error> {
let config = config(id).await?;
Ok(if let Some(config) = config.config {
config
} else {
config
.spec
.gen(&mut rand::rngs::StdRng::from_entropy(), &None)
.with_code(crate::error::CFG_SPEC_VIOLATION)?
})
}
pub async fn info(id: &str) -> Result<AppInfo, Error> {
list_info()
.await
.map_err(Error::from)?
.get(id)
.ok_or_else(|| Error::new(failure::format_err!("{} is not installed", id), Some(6)))
.map(Clone::clone)
}
pub async fn info_full(
id: &str,
with_status: bool,
with_manifest: bool,
with_config: bool,
with_dependencies: bool,
) -> Result<AppInfoFull, Error> {
Ok(AppInfoFull {
info: info(id).await?,
status: if with_status {
Some(status(id).await?)
} else {
None
},
manifest: if with_manifest {
Some(manifest(id).await?)
} else {
None
},
config: if with_config {
Some(config(id).await?)
} else {
None
},
dependencies: if with_dependencies {
Some(dependencies(id, true).await?)
} else {
None
},
})
}
pub async fn dependencies(id_version: &str, local_only: bool) -> Result<AppDependencies, Error> {
let mut id_version_iter = id_version.split("@");
let id = id_version_iter.next().unwrap();
let version_range = id_version_iter
.next()
.map(|a| a.parse::<emver::VersionRange>())
.transpose()
.with_context(|e| format!("Failed to Parse Version Requirement: {}", e))
.no_code()?
.unwrap_or_else(emver::VersionRange::any);
let (manifest, config_info) = match list_info().await?.get(id) {
Some(info) if info.version.satisfies(&version_range) => {
futures::try_join!(manifest(id), config(id))?
}
_ if !local_only => futures::try_join!(
crate::registry::manifest(id, &version_range),
crate::registry::config(id, &version_range)
)?,
_ => {
return Err(failure::format_err!("App Not Installed: {}", id))
.with_code(crate::error::NOT_FOUND)
}
};
let config = if let Some(cfg) = config_info.config {
cfg
} else {
config_info
.spec
.gen(&mut rand::rngs::StdRng::from_entropy(), &None)
.unwrap_or_default()
};
crate::dependencies::check_dependencies(manifest, &config, &config_info.spec).await
}
pub async fn dependents(id: &str, transitive: bool) -> Result<LinearSet<String>, Error> {
pub fn dependents_rec<'a>(
id: &'a str,
transitive: bool,
res: &'a mut LinearSet<String>,
) -> BoxFuture<'a, Result<(), Error>> {
async move {
for (app_id, _) in list_info().await? {
let manifest = manifest(&app_id).await?;
match manifest.dependencies.0.get(id) {
Some(info) if !res.contains(&app_id) => {
let config_info = config(&app_id).await?;
let config = if let Some(cfg) = config_info.config {
cfg
} else {
config_info
.spec
.gen(&mut rand::rngs::StdRng::from_entropy(), &None)
.unwrap_or_default()
};
if info.optional.is_none() || config_info.spec.requires(&id, &config) {
res.insert(app_id.clone());
if transitive {
dependents_rec(&app_id, true, res).await?;
}
}
}
_ => (),
}
}
Ok(())
}
.boxed()
}
let mut res = LinearSet::new();
dependents_rec(id, transitive, &mut res).await?;
Ok(res)
}
pub async fn list(
with_status: bool,
with_manifest: bool,
with_config: bool,
with_dependencies: bool,
) -> Result<LinearMap<String, AppInfoFull>, Error> {
let info = list_info().await?;
futures::future::join_all(info.into_iter().map(move |(id, info)| async move {
let (status, manifest, config, dependencies) = futures::try_join!(
OptionFuture::from(if with_status { Some(status(&id)) } else { None })
.map(Option::transpose),
OptionFuture::from(if with_manifest {
Some(manifest(&id))
} else {
None
})
.map(Option::transpose),
OptionFuture::from(if with_config { Some(config(&id)) } else { None })
.map(Option::transpose),
OptionFuture::from(if with_dependencies {
Some(dependencies(&id, true))
} else {
None
})
.map(Option::transpose)
)?;
Ok((
id,
AppInfoFull {
info,
status,
manifest,
config,
dependencies,
},
))
}))
.await
.into_iter()
.collect()
}
pub async fn print_instructions(id: &str) -> Result<(), Error> {
if let Some(file) = PersistencePath::from_ref("apps")
.join(id)
.join("instructions.md")
.maybe_read(false)
.await
{
use tokio::io::AsyncWriteExt;
let mut stdout = tokio::io::stdout();
tokio::io::copy(&mut *file?, &mut stdout)
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
stdout
.flush()
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
stdout
.shutdown()
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
Ok(())
} else {
Err(failure::format_err!("No Instructions: {}", id)).with_code(crate::error::NOT_FOUND)
}
}

238
appmgr/src/backup.rs Normal file
View File

@@ -0,0 +1,238 @@
use std::path::Path;
use argonautica::{Hasher, Verifier};
use futures::try_join;
use futures::TryStreamExt;
use crate::apps;
use crate::util::from_yaml_async_reader;
use crate::util::Invoke;
use crate::Error;
use crate::ResultExt;
pub async fn create_backup<P: AsRef<Path>>(
path: P,
app_id: &str,
password: &str,
) -> Result<(), Error> {
let path = tokio::fs::canonicalize(path).await?;
crate::ensure_code!(
path.is_dir(),
crate::error::FILESYSTEM_ERROR,
"Backup Path Must Be Directory"
);
let pw_path = path.join("password");
let data_path = path.join("data");
let tor_path = path.join("tor");
let volume_path = Path::new(crate::VOLUMES).join(app_id);
let hidden_service_path =
Path::new(crate::tor::HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", app_id));
if pw_path.exists() {
use tokio::io::AsyncReadExt;
let mut f = tokio::fs::File::open(&pw_path).await?;
let mut hash = String::new();
f.read_to_string(&mut hash).await?;
crate::ensure_code!(
Verifier::new()
.with_password(password)
.with_hash(hash)
.verify()
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
crate::error::INVALID_BACKUP_PASSWORD,
"Invalid Backup Decryption Password"
);
}
{
// save password
use tokio::io::AsyncWriteExt;
let mut hasher = Hasher::default();
hasher.opt_out_of_secret_key(true);
let hash = hasher.with_password(password).hash().no_code()?;
let mut f = tokio::fs::File::create(pw_path).await?;
f.write_all(hash.as_bytes()).await?;
f.flush().await?;
}
let status = crate::apps::status(app_id).await?;
let exclude = if volume_path.is_dir() {
let ignore_path = volume_path.join(".backupignore");
if ignore_path.is_file() {
use tokio::io::AsyncBufReadExt;
tokio::io::BufReader::new(tokio::fs::File::open(ignore_path).await?)
.lines()
.try_filter(|l| futures::future::ready(!l.is_empty()))
.try_collect()
.await?
} else {
Vec::new()
}
} else {
return Err(format_err!("Volume For {} Does Not Exist", app_id))
.with_code(crate::error::NOT_FOUND);
};
let running = status.status == crate::apps::DockerStatus::Running;
if running {
crate::control::pause_app(&app_id).await?;
}
let mut data_cmd = tokio::process::Command::new("duplicity");
for exclude in exclude {
if exclude.starts_with('!') {
data_cmd.arg(format!(
"--include={}",
volume_path.join(exclude.trim_start_matches('!')).display()
));
} else {
data_cmd.arg(format!("--exclude={}", volume_path.join(exclude).display()));
}
}
let data_res = data_cmd
.env("PASSPHRASE", password)
.arg(volume_path)
.arg(format!("file://{}", data_path.display()))
.invoke("Duplicity")
.await;
let tor_res = tokio::process::Command::new("duplicity")
.env("PASSPHRASE", password)
.arg(hidden_service_path)
.arg(format!("file://{}", tor_path.display()))
.invoke("Duplicity")
.await;
if running {
if crate::apps::info(&app_id).await?.needs_restart {
crate::control::restart_app(&app_id).await?;
} else {
crate::control::resume_app(&app_id).await?;
}
}
data_res?;
tor_res?;
Ok(())
}
pub async fn restore_backup<P: AsRef<Path>>(
path: P,
app_id: &str,
password: &str,
) -> Result<(), Error> {
let path = tokio::fs::canonicalize(path).await?;
crate::ensure_code!(
path.is_dir(),
crate::error::FILESYSTEM_ERROR,
"Backup Path Must Be Directory"
);
let pw_path = path.join("password");
let data_path = path.join("data");
let tor_path = path.join("tor");
let volume_path = Path::new(crate::VOLUMES).join(app_id);
let hidden_service_path =
Path::new(crate::tor::HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", app_id));
if pw_path.exists() {
use tokio::io::AsyncReadExt;
let mut f = tokio::fs::File::open(&pw_path).await?;
let mut hash = String::new();
f.read_to_string(&mut hash).await?;
crate::ensure_code!(
Verifier::new()
.with_password(password)
.with_hash(hash)
.verify()
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
crate::error::INVALID_BACKUP_PASSWORD,
"Invalid Backup Decryption Password"
);
}
let status = crate::apps::status(app_id).await?;
let running = status.status == crate::apps::DockerStatus::Running;
if running {
crate::control::stop_app(app_id, true, false).await?;
}
let mut data_cmd = tokio::process::Command::new("duplicity");
data_cmd
.env("PASSPHRASE", password)
.arg("--force")
.arg(format!("file://{}", data_path.display()))
.arg(&volume_path);
let mut tor_cmd = tokio::process::Command::new("duplicity");
tor_cmd
.env("PASSPHRASE", password)
.arg("--force")
.arg(format!("file://{}", tor_path.display()))
.arg(&hidden_service_path);
let (data_output, tor_output) = try_join!(data_cmd.status(), tor_cmd.status())?;
crate::ensure_code!(
data_output.success(),
crate::error::GENERAL_ERROR,
"Duplicity Error"
);
crate::ensure_code!(
tor_output.success(),
crate::error::GENERAL_ERROR,
"Duplicity Error"
);
// Fix the tor address in apps.yaml
let mut yhdl = apps::list_info_mut().await?;
if let Some(app_info) = yhdl.get_mut(app_id) {
app_info.tor_address = Some(crate::tor::read_tor_address(app_id, None).await?);
}
yhdl.commit().await?;
// Attempt to configure the service with the config coming from restoration
let cfg_path = Path::new(crate::VOLUMES)
.join(app_id)
.join("start9")
.join("config.yaml");
if cfg_path.exists() {
let cfg = from_yaml_async_reader(tokio::fs::File::open(cfg_path).await?).await?;
if let Err(e) = crate::config::configure(app_id, cfg, None, false).await {
log::warn!("Could not restore backup configuration: {}", e);
}
}
crate::tor::restart().await?;
Ok(())
}
pub async fn backup_to_partition(
logicalname: &str,
app_id: &str,
password: &str,
) -> Result<(), Error> {
let backup_mount_path = Path::new(crate::BACKUP_MOUNT_POINT);
let guard = crate::disks::MountGuard::new(logicalname, &backup_mount_path).await?;
let backup_dir_path = backup_mount_path.join(crate::BACKUP_DIR).join(app_id);
tokio::fs::create_dir_all(&backup_dir_path).await?;
let res = create_backup(backup_dir_path, app_id, password).await;
guard.unmount().await?;
res
}
pub async fn restore_from_partition(
logicalname: &str,
app_id: &str,
password: &str,
) -> Result<(), Error> {
let backup_mount_path = Path::new(crate::BACKUP_MOUNT_POINT);
let guard = crate::disks::MountGuard::new(logicalname, &backup_mount_path).await?;
let backup_dir_path = backup_mount_path.join(crate::BACKUP_DIR).join(app_id);
let res = restore_backup(backup_dir_path, app_id, password).await;
guard.unmount().await?;
res
}

324
appmgr/src/config/mod.rs Normal file
View File

@@ -0,0 +1,324 @@
use std::borrow::Cow;
use std::path::Path;
use std::time::Duration;
use failure::ResultExt as _;
use futures::future::{BoxFuture, FutureExt};
use itertools::Itertools;
use linear_map::{set::LinearSet, LinearMap};
use rand::SeedableRng;
use regex::Regex;
use crate::dependencies::{DependencyError, TaggedDependencyError};
use crate::util::PersistencePath;
use crate::util::{from_yaml_async_reader, to_yaml_async_writer};
use crate::ResultExt as _;
pub mod rules;
pub mod spec;
pub mod util;
pub mod value;
pub use rules::{ConfigRuleEntry, ConfigRuleEntryWithSuggestions};
pub use spec::{ConfigSpec, Defaultable};
use util::NumRange;
pub use value::Config;
#[derive(Debug, Fail)]
pub enum ConfigurationError {
#[fail(display = "Timeout Error")]
TimeoutError,
#[fail(display = "No Match: {}", _0)]
NoMatch(NoMatchWithPath),
#[fail(display = "Invalid Variant: {}", _0)]
InvalidVariant(String),
#[fail(display = "System Error: {}", _0)]
SystemError(crate::Error),
}
impl From<TimeoutError> for ConfigurationError {
fn from(_: TimeoutError) -> Self {
ConfigurationError::TimeoutError
}
}
impl From<NoMatchWithPath> for ConfigurationError {
fn from(e: NoMatchWithPath) -> Self {
ConfigurationError::NoMatch(e)
}
}
#[derive(Clone, Copy, Debug, Fail)]
#[fail(display = "Timeout Error")]
pub struct TimeoutError;
#[derive(Clone, Debug, Fail)]
pub struct NoMatchWithPath {
pub path: Vec<String>,
pub error: MatchError,
}
impl NoMatchWithPath {
pub fn new(error: MatchError) -> Self {
NoMatchWithPath {
path: Vec::new(),
error,
}
}
pub fn prepend(mut self, seg: String) -> Self {
self.path.push(seg);
self
}
}
impl std::fmt::Display for NoMatchWithPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.path.iter().rev().join("."), self.error)
}
}
#[derive(Clone, Debug, Fail)]
pub enum MatchError {
#[fail(display = "String {:?} Does Not Match Pattern {}", _0, _1)]
Pattern(String, Regex),
#[fail(display = "String {:?} Is Not In Enum {:?}", _0, _1)]
Enum(String, LinearSet<String>),
#[fail(display = "Field Is Not Nullable")]
NotNullable,
#[fail(display = "Length Mismatch: expected {}, actual: {}", _0, _1)]
LengthMismatch(NumRange<usize>, usize),
#[fail(display = "Invalid Type: expected {}, actual: {}", _0, _1)]
InvalidType(&'static str, &'static str),
#[fail(display = "Number Out Of Range: expected {}, actual: {}", _0, _1)]
OutOfRange(NumRange<f64>, f64),
#[fail(display = "Number Is Not Integral: {}", _0)]
NonIntegral(f64),
#[fail(display = "Variant {:?} Is Not In Union {:?}", _0, _1)]
Union(String, LinearSet<String>),
#[fail(display = "Variant Is Missing Tag {:?}", _0)]
MissingTag(String),
#[fail(
display = "Property {:?} Of Variant {:?} Conflicts With Union Tag",
_0, _1
)]
PropertyMatchesUnionTag(String, String),
#[fail(display = "Name of Property {:?} Conflicts With Map Tag Name", _0)]
PropertyNameMatchesMapTag(String),
#[fail(display = "Pointer Is Invalid: {}", _0)]
InvalidPointer(spec::ValueSpecPointer),
#[fail(display = "Object Key Is Invalid: {}", _0)]
InvalidKey(String),
#[fail(display = "Value In List Is Not Unique")]
ListUniquenessViolation,
}
#[derive(Clone, Debug, Default, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ConfigurationRes {
pub changed: LinearMap<String, Config>,
pub needs_restart: LinearSet<String>,
pub stopped: LinearMap<String, TaggedDependencyError>,
}
// returns apps with changed configurations
pub async fn configure(
name: &str,
config: Option<Config>,
timeout: Option<Duration>,
dry_run: bool,
) -> Result<ConfigurationRes, crate::Error> {
async fn handle_broken_dependent(
name: &str,
dependent: String,
dry_run: bool,
res: &mut ConfigurationRes,
error: DependencyError,
) -> Result<(), crate::Error> {
crate::control::stop_dependents(
&dependent,
dry_run,
DependencyError::NotRunning,
&mut res.stopped,
)
.await?;
if crate::apps::status(&dependent).await?.status != crate::apps::DockerStatus::Stopped {
crate::control::stop_app(&dependent, false, dry_run).await?;
res.stopped.insert(
// TODO: maybe don't do this if its not running
dependent,
TaggedDependencyError {
dependency: name.to_owned(),
error,
},
);
}
Ok(())
}
fn configure_rec<'a>(
name: &'a str,
config: Option<Config>,
timeout: Option<Duration>,
dry_run: bool,
res: &'a mut ConfigurationRes,
) -> BoxFuture<'a, Result<Config, crate::Error>> {
async move {
let info = crate::apps::list_info()
.await?
.remove(name)
.ok_or_else(|| failure::format_err!("{} is not installed", name))
.with_code(crate::error::NOT_FOUND)?;
let mut rng = rand::rngs::StdRng::from_entropy();
let spec_path = PersistencePath::from_ref("apps")
.join(name)
.join("config_spec.yaml");
let rules_path = PersistencePath::from_ref("apps")
.join(name)
.join("config_rules.yaml");
let config_path = PersistencePath::from_ref("apps")
.join(name)
.join("config.yaml");
let spec: ConfigSpec =
from_yaml_async_reader(&mut *spec_path.read(false).await?).await?;
let rules: Vec<ConfigRuleEntry> =
from_yaml_async_reader(&mut *rules_path.read(false).await?).await?;
let old_config: Option<Config> =
if let Some(mut f) = config_path.maybe_read(false).await.transpose()? {
Some(from_yaml_async_reader(&mut *f).await?)
} else {
None
};
let mut config = if let Some(cfg) = config {
cfg
} else {
if let Some(old) = &old_config {
old.clone()
} else {
spec.gen(&mut rng, &timeout)
.with_code(crate::error::CFG_SPEC_VIOLATION)?
}
};
spec.matches(&config)
.with_code(crate::error::CFG_SPEC_VIOLATION)?;
spec.update(&mut config)
.await
.with_code(crate::error::CFG_SPEC_VIOLATION)?;
let mut cfgs = LinearMap::new();
cfgs.insert(name, Cow::Borrowed(&config));
for rule in rules {
rule.check(&config, &cfgs)
.with_code(crate::error::CFG_RULES_VIOLATION)?;
}
match old_config {
Some(old) if &old == &config && info.configured && !info.recoverable => {
return Ok(config)
}
_ => (),
};
res.changed.insert(name.to_owned(), config.clone());
for dependent in crate::apps::dependents(name, false).await? {
match configure_rec(&dependent, None, timeout, dry_run, res).await {
Ok(dependent_config) => {
let man = crate::apps::manifest(&dependent).await?;
if let Some(dep_info) = man.dependencies.0.get(name) {
match dep_info
.satisfied(
name,
Some(config.clone()),
&dependent,
&dependent_config,
)
.await?
{
Ok(_) => (),
Err(e) => {
handle_broken_dependent(name, dependent, dry_run, res, e)
.await?;
}
}
}
}
Err(e) => {
if e.code == Some(crate::error::CFG_RULES_VIOLATION)
|| e.code == Some(crate::error::CFG_SPEC_VIOLATION)
{
if !dry_run {
crate::apps::set_configured(&dependent, false).await?;
}
handle_broken_dependent(
name,
dependent,
dry_run,
res,
DependencyError::PointerUpdateError(format!("{}", e)),
)
.await?;
} else {
handle_broken_dependent(
name,
dependent,
dry_run,
res,
DependencyError::Other(format!("{}", e)),
)
.await?;
}
}
}
}
if !dry_run {
let mut file = config_path.write(None).await?;
to_yaml_async_writer(file.as_mut(), &config).await?;
file.commit().await?;
let volume_config = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("config.yaml");
tokio::fs::copy(config_path.path(), &volume_config)
.await
.with_context(|e| {
format!(
"{}: {} -> {}",
e,
config_path.path().display(),
volume_config.display()
)
})
.with_code(crate::error::FILESYSTEM_ERROR)?;
crate::apps::set_configured(name, true).await?;
crate::apps::set_recoverable(name, false).await?;
}
if crate::apps::status(name).await?.status != crate::apps::DockerStatus::Stopped {
if !dry_run {
crate::apps::set_needs_restart(name, true).await?;
}
res.needs_restart.insert(name.to_string());
}
Ok(config)
}
.boxed()
}
let mut res = ConfigurationRes::default();
configure_rec(name, config, timeout, dry_run, &mut res).await?;
Ok(res)
}
pub async fn remove(name: &str) -> Result<(), crate::Error> {
let config_path = PersistencePath::from_ref("apps")
.join(name)
.join("config.yaml")
.path();
if config_path.exists() {
tokio::fs::remove_file(&config_path)
.await
.with_context(|e| format!("{}: {}", e, config_path.display()))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
let volume_config = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("config.yaml");
if volume_config.exists() {
tokio::fs::remove_file(&volume_config)
.await
.with_context(|e| format!("{}: {}", e, volume_config.display()))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
crate::apps::set_configured(name, false).await?;
Ok(())
}

View File

@@ -0,0 +1,76 @@
num = @{ int ~ ("." ~ ASCII_DIGIT*)? ~ (^"e" ~ int)? }
int = @{ ("+" | "-")? ~ ASCII_DIGIT+ }
raw_string = @{ (!("\\" | "\"") ~ ANY)+ }
predefined = @{ "n" | "r" | "t" | "\\" | "0" | "\"" | "'" }
escape = @{ "\\" ~ predefined }
str = @{ "\"" ~ (raw_string | escape)* ~ "\"" }
ident_char = @{ ASCII_ALPHANUMERIC | "-" }
sub_ident = _{ sub_ident_regular | sub_ident_index | sub_ident_any | sub_ident_all | sub_ident_fn }
sub_ident_regular = { sub_ident_regular_base | sub_ident_regular_expr }
sub_ident_regular_base = @{ ASCII_ALPHA ~ ident_char* }
sub_ident_regular_expr = ${ "[" ~ str_expr ~ "]" }
sub_ident_index = { sub_ident_index_base | sub_ident_index_expr }
sub_ident_index_base = @{ ASCII_DIGIT+ }
sub_ident_index_expr = ${ "[" ~ num_expr ~ "]" }
sub_ident_any = @{ "*" }
sub_ident_all = @{ "&" }
sub_ident_fn = ${ "[" ~ list_access_function ~ "]"}
list_access_function = _{ list_access_function_first | list_access_function_last | list_access_function_any | list_access_function_all }
list_access_function_first = !{ "first" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" }
list_access_function_last = !{ "last" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" }
list_access_function_any = !{ "any" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" }
list_access_function_all = !{ "all" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" }
app_id = ${ "[" ~ sub_ident_regular ~ "]" }
ident = _{ (app_id ~ ".")? ~ sub_ident_regular ~ ("." ~ sub_ident)* }
bool_var = ${ ident ~ "?" }
num_var = ${ "#" ~ ident }
str_var = ${ "'" ~ ident }
any_var = ${ ident }
bool_op = _{ and | or | xor }
and = { "AND" }
or = { "OR" }
xor = { "XOR" }
num_cmp_op = _{ lt | lte | eq | neq | gt | gte }
str_cmp_op = _{ lt | lte | eq | neq | gt | gte }
lt = { "<" }
lte = { "<=" }
eq = { "=" }
neq = { "!=" }
gt = { ">" }
gte = { ">=" }
num_op = _{ add | sub | mul | div | pow }
str_op = _{ add }
add = { "+" }
sub = { "-" }
mul = { "*" }
div = { "/" }
pow = { "^" }
num_expr = !{ num_term ~ (num_op ~ num_term)* }
num_term = _{ num | num_var | "(" ~ num_expr ~ ")" }
str_expr = !{ str_term ~ (str_op ~ str_term)* }
str_term = _{ str | str_var | "(" ~ str_expr ~ ")" }
num_cmp_expr = { num_expr ~ num_cmp_op ~ num_expr }
str_cmp_expr = { str_expr ~ str_cmp_op ~ str_expr }
bool_expr = !{ bool_term ~ (bool_op ~ bool_term)* }
inv_bool_expr = { "!(" ~ bool_expr ~ ")" }
bool_term = _{ bool_var | "(" ~ bool_expr ~ ")" | inv_bool_expr | num_cmp_expr | str_cmp_expr }
val_expr = _{ any_var | str_expr | num_expr | bool_expr }
rule = _{ SOI ~ bool_expr ~ EOI }
reference = _{ SOI ~ any_var ~ EOI }
value = _{ SOI ~ val_expr ~ EOI }
del_action = _{ SOI ~ "FROM" ~ any_var ~ "AS" ~ sub_ident_regular ~ "WHERE" ~ bool_expr ~ EOI }
obj_key = _{ SOI ~ sub_ident_regular ~ EOI }
WHITESPACE = _{ " " | "\t" }

1252
appmgr/src/config/rules.rs Normal file

File diff suppressed because it is too large Load Diff

1874
appmgr/src/config/spec.rs Normal file

File diff suppressed because it is too large Load Diff

367
appmgr/src/config/util.rs Normal file
View File

@@ -0,0 +1,367 @@
use std::ops::Bound;
use std::ops::RangeBounds;
use std::ops::RangeInclusive;
use rand::{distributions::Distribution, Rng};
use super::value::Config;
pub const STATIC_NULL: super::value::Value = super::value::Value::Null;
#[derive(Clone, Debug)]
pub struct CharSet(pub Vec<(RangeInclusive<char>, usize)>, usize);
impl CharSet {
pub fn contains(&self, c: &char) -> bool {
self.0.iter().any(|r| r.0.contains(c))
}
pub fn gen<R: Rng>(&self, rng: &mut R) -> char {
let mut idx = rng.gen_range(0, self.1);
for r in &self.0 {
if idx < r.1 {
return std::convert::TryFrom::try_from(
rand::distributions::Uniform::new_inclusive(
u32::from(*r.0.start()),
u32::from(*r.0.end()),
)
.sample(rng),
)
.unwrap();
} else {
idx -= r.1;
}
}
unreachable!()
}
}
impl Default for CharSet {
fn default() -> Self {
CharSet(vec![('!'..='~', 94)], 94)
}
}
impl<'de> serde::de::Deserialize<'de> for CharSet {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let mut res = Vec::new();
let mut len = 0;
let mut a: Option<char> = None;
let mut b: Option<char> = None;
let mut in_range = false;
for c in s.chars() {
match c {
',' => match (a, b, in_range) {
(Some(start), Some(end), _) => {
if !end.is_ascii() {
return Err(serde::de::Error::custom("Invalid Character"));
}
if start >= end {
return Err(serde::de::Error::custom("Invalid Bounds"));
}
let l = u32::from(end) - u32::from(start) + 1;
res.push((start..=end, l as usize));
len += l as usize;
a = None;
b = None;
in_range = false;
}
(Some(start), None, false) => {
len += 1;
res.push((start..=start, 1));
a = None;
}
(Some(_), None, true) => {
b = Some(',');
}
(None, None, false) => {
a = Some(',');
}
_ => {
return Err(serde::de::Error::custom("Syntax Error"));
}
},
'-' => {
if a.is_none() {
a = Some('-');
} else if !in_range {
in_range = true;
} else if b.is_none() {
b = Some('-')
} else {
return Err(serde::de::Error::custom("Syntax Error"));
}
}
_ => {
if a.is_none() {
a = Some(c);
} else if in_range && b.is_none() {
b = Some(c);
} else {
return Err(serde::de::Error::custom("Syntax Error"));
}
}
}
}
match (a, b) {
(Some(start), Some(end)) => {
if !end.is_ascii() {
return Err(serde::de::Error::custom("Invalid Character"));
}
if start >= end {
return Err(serde::de::Error::custom("Invalid Bounds"));
}
let l = u32::from(end) - u32::from(start) + 1;
res.push((start..=end, l as usize));
len += l as usize;
}
(Some(c), None) => {
len += 1;
res.push((c..=c, 1));
}
_ => (),
}
Ok(CharSet(res, len))
}
}
impl serde::ser::Serialize for CharSet {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
<&str>::serialize(
&self
.0
.iter()
.map(|r| match r.1 {
1 => format!("{}", r.0.start()),
_ => format!("{}-{}", r.0.start(), r.0.end()),
})
.collect::<Vec<_>>()
.join(",")
.as_str(),
serializer,
)
}
}
pub mod serde_regex {
use regex::Regex;
use serde::*;
pub fn serialize<S>(regex: &Regex, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
<&str>::serialize(&regex.as_str(), serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Regex::new(&s).map_err(|e| de::Error::custom(e))
}
}
#[derive(Clone, Debug)]
pub struct NumRange<T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd>(
pub (Bound<T>, Bound<T>),
);
impl<T> std::ops::Deref for NumRange<T>
where
T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd,
{
type Target = (Bound<T>, Bound<T>);
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'de, T> serde::de::Deserialize<'de> for NumRange<T>
where
T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let mut split = s.split(",");
let start = split
.next()
.map(|s| match s.get(..1) {
Some("(") => match s.get(1..2) {
Some("*") => Ok(Bound::Unbounded),
_ => s[1..]
.trim()
.parse()
.map(Bound::Excluded)
.map_err(|e| serde::de::Error::custom(e)),
},
Some("[") => s[1..]
.trim()
.parse()
.map(Bound::Included)
.map_err(|e| serde::de::Error::custom(e)),
_ => Err(serde::de::Error::custom(format!(
"Could not parse left bound: {}",
s
))),
})
.transpose()?
.unwrap();
let end = split
.next()
.map(|s| match s.get(s.len() - 1..) {
Some(")") => match s.get(s.len() - 2..s.len() - 1) {
Some("*") => Ok(Bound::Unbounded),
_ => s[..s.len() - 1]
.trim()
.parse()
.map(Bound::Excluded)
.map_err(|e| serde::de::Error::custom(e)),
},
Some("]") => s[..s.len() - 1]
.trim()
.parse()
.map(Bound::Included)
.map_err(|e| serde::de::Error::custom(e)),
_ => Err(serde::de::Error::custom(format!(
"Could not parse right bound: {}",
s
))),
})
.transpose()?
.unwrap_or(Bound::Unbounded);
Ok(NumRange((start, end)))
}
}
impl<T> std::fmt::Display for NumRange<T>
where
T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.start_bound() {
Bound::Excluded(n) => write!(f, "({},", n)?,
Bound::Included(n) => write!(f, "[{},", n)?,
Bound::Unbounded => write!(f, "(*,")?,
};
match self.end_bound() {
Bound::Excluded(n) => write!(f, "{})", n),
Bound::Included(n) => write!(f, "{}]", n),
Bound::Unbounded => write!(f, "*)"),
}
}
}
impl<T> serde::ser::Serialize for NumRange<T>
where
T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
<&str>::serialize(&format!("{}", self).as_str(), serializer)
}
}
#[derive(Clone, Debug)]
pub enum UniqueBy {
Any(Vec<UniqueBy>),
All(Vec<UniqueBy>),
Exactly(String),
NotUnique,
}
impl UniqueBy {
pub fn eq(&self, lhs: &Config, rhs: &Config) -> bool {
match self {
UniqueBy::Any(any) => any.iter().any(|u| u.eq(lhs, rhs)),
UniqueBy::All(all) => all.iter().all(|u| u.eq(lhs, rhs)),
UniqueBy::Exactly(key) => lhs.0.get(key) == rhs.0.get(key),
UniqueBy::NotUnique => false,
}
}
}
impl Default for UniqueBy {
fn default() -> Self {
UniqueBy::NotUnique
}
}
impl<'de> serde::de::Deserialize<'de> for UniqueBy {
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = UniqueBy;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a key, an \"any\" object, or an \"all\" object")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(UniqueBy::Exactly(v.to_owned()))
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(UniqueBy::Exactly(v))
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut variant = None;
while let Some(key) = map.next_key()? {
match key {
"any" => {
return Ok(UniqueBy::Any(map.next_value()?));
}
"all" => {
return Ok(UniqueBy::All(map.next_value()?));
}
_ => {
variant = Some(key);
}
}
}
Err(serde::de::Error::unknown_variant(
variant.unwrap_or_default(),
&["any", "all"],
))
}
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(UniqueBy::NotUnique)
}
fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
Ok(UniqueBy::NotUnique)
}
}
deserializer.deserialize_any(Visitor)
}
}
impl serde::ser::Serialize for UniqueBy {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
use serde::ser::SerializeMap;
match self {
UniqueBy::Any(any) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_key("any")?;
map.serialize_value(any)?;
map.end()
}
UniqueBy::All(all) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_key("all")?;
map.serialize_value(all)?;
map.end()
}
UniqueBy::Exactly(key) => serializer.serialize_str(key),
UniqueBy::NotUnique => serializer.serialize_unit(),
}
}
}

View File

@@ -0,0 +1,66 @@
use linear_map::LinearMap;
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Config(pub LinearMap<String, Value>);
impl Config {
pub fn merge_with(&mut self, other: Config) {
for (key, val) in other.0.into_iter() {
match (self.0.get_mut(&key), &val) {
(Some(Value::Object(l_obj)), Value::Object(_)) => {
// gross, I know. https://github.com/rust-lang/rust/issues/45600
let r_obj = match val {
Value::Object(r_obj) => r_obj,
_ => unreachable!(),
};
l_obj.merge_with(r_obj)
}
(Some(Value::List(l_vec)), Value::List(_)) => {
let mut r_vec = match val {
Value::List(r_vec) => r_vec,
_ => unreachable!(),
};
l_vec.append(&mut r_vec);
}
_ => {
self.0.insert(key, val);
}
}
}
}
}
fn serialize_num<S: serde::Serializer>(num: &f64, serializer: S) -> Result<S::Ok, S::Error> {
if *num < (1_i64 << f64::MANTISSA_DIGITS) as f64
&& *num > -(1_i64 << f64::MANTISSA_DIGITS) as f64
&& num.trunc() == *num
{
serializer.serialize_i64(*num as i64)
} else {
serializer.serialize_f64(*num)
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Value {
String(String),
#[serde(serialize_with = "serialize_num")]
Number(f64),
Bool(bool),
List(Vec<Value>),
Object(Config),
Null,
}
impl Value {
pub fn type_of(&self) -> &'static str {
match self {
Value::String(_) => "string",
Value::Number(_) => "number",
Value::Bool(_) => "boolean",
Value::List(_) => "list",
Value::Object(_) => "object",
Value::Null => "null",
}
}
}

194
appmgr/src/control.rs Normal file
View File

@@ -0,0 +1,194 @@
use std::path::Path;
use futures::future::{BoxFuture, FutureExt};
use linear_map::LinearMap;
use crate::dependencies::{DependencyError, TaggedDependencyError};
use crate::Error;
pub async fn start_app(name: &str, update_metadata: bool) -> Result<(), Error> {
let lock = crate::util::lock_file(
format!(
"{}",
Path::new(crate::PERSISTENCE_DIR)
.join("apps")
.join(name)
.join("control.lock")
.display()
),
true,
)
.await?;
let status = crate::apps::status(name).await?.status;
if status == crate::apps::DockerStatus::Stopped {
if update_metadata {
crate::config::configure(name, None, None, false).await?;
crate::dependencies::update_shared(name).await?;
crate::dependencies::update_binds(name).await?;
}
crate::apps::set_needs_restart(name, false).await?;
let output = tokio::process::Command::new("docker")
.args(&["start", name])
.stdout(std::process::Stdio::null())
.output()
.await?;
crate::ensure_code!(
output.status.success(),
crate::error::DOCKER_ERROR,
"Failed to Start Application: {}",
std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error")
);
} else if status == crate::apps::DockerStatus::Paused {
resume_app(name).await?;
}
crate::util::unlock(lock).await?;
Ok(())
}
pub async fn stop_app(
name: &str,
cascade: bool,
dry_run: bool,
) -> Result<LinearMap<String, TaggedDependencyError>, Error> {
let mut res = LinearMap::new();
if cascade {
stop_dependents(name, dry_run, DependencyError::NotRunning, &mut res).await?;
}
if !dry_run {
let lock = crate::util::lock_file(
format!(
"{}",
Path::new(crate::PERSISTENCE_DIR)
.join("apps")
.join(name)
.join("control.lock")
.display()
),
true,
)
.await?;
log::info!("Stopping {}", name);
let output = tokio::process::Command::new("docker")
.args(&["stop", "-t", "25", name])
.stdout(std::process::Stdio::null())
.output()
.await?;
crate::ensure_code!(
output.status.success(),
crate::error::DOCKER_ERROR,
"Failed to Stop Application: {}",
std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error")
);
crate::util::unlock(lock).await?;
}
Ok(res)
}
pub async fn stop_dependents(
name: &str,
dry_run: bool,
err: DependencyError,
res: &mut LinearMap<String, TaggedDependencyError>,
) -> Result<(), Error> {
fn stop_dependents_rec<'a>(
name: &'a str,
dry_run: bool,
err: DependencyError,
res: &'a mut LinearMap<String, TaggedDependencyError>,
) -> BoxFuture<'a, Result<(), Error>> {
async move {
for dependent in crate::apps::dependents(name, false).await? {
if crate::apps::status(&dependent).await?.status
!= crate::apps::DockerStatus::Stopped
{
stop_dependents_rec(&dependent, dry_run, DependencyError::NotRunning, res)
.await?;
stop_app(&dependent, false, dry_run).await?;
res.insert(
dependent,
TaggedDependencyError {
dependency: name.to_owned(),
error: err.clone(),
},
);
}
}
Ok(())
}
.boxed()
}
stop_dependents_rec(name, dry_run, err, res).await
}
pub async fn restart_app(name: &str) -> Result<(), Error> {
stop_app(name, false, false).await?;
if let Err(e) = start_app(name, true).await {
log::warn!("Stopping dependents");
stop_dependents(
name,
false,
crate::dependencies::DependencyError::NotRunning,
&mut linear_map::LinearMap::new(),
)
.await?;
return Err(e);
}
Ok(())
}
pub async fn pause_app(name: &str) -> Result<(), Error> {
let lock = crate::util::lock_file(
format!(
"{}",
Path::new(crate::PERSISTENCE_DIR)
.join("apps")
.join(name)
.join("control.lock")
.display()
),
true,
)
.await?;
let output = tokio::process::Command::new("docker")
.args(&["pause", name])
.stdout(std::process::Stdio::null())
.output()
.await?;
crate::ensure_code!(
output.status.success(),
crate::error::DOCKER_ERROR,
"Failed to Pause Application: {}",
std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error")
);
crate::util::unlock(lock).await?;
Ok(())
}
pub async fn resume_app(name: &str) -> Result<(), Error> {
let lock = crate::util::lock_file(
format!(
"{}",
Path::new(crate::PERSISTENCE_DIR)
.join("apps")
.join(name)
.join("control.lock")
.display()
),
true,
)
.await?;
let output = tokio::process::Command::new("docker")
.args(&["unpause", name])
.stdout(std::process::Stdio::null())
.output()
.await?;
crate::ensure_code!(
output.status.success(),
crate::error::DOCKER_ERROR,
"Failed to Resume Application: {}",
std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error")
);
crate::util::unlock(lock).await?;
Ok(())
}

276
appmgr/src/dependencies.rs Normal file
View File

@@ -0,0 +1,276 @@
use std::borrow::Cow;
use std::path::Path;
use emver::{Version, VersionRange};
use linear_map::LinearMap;
use rand::SeedableRng;
use crate::config::{Config, ConfigRuleEntryWithSuggestions, ConfigSpec};
use crate::manifest::ManifestLatest;
use crate::Error;
use crate::ResultExt as _;
#[derive(Clone, Debug, Fail, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DependencyError {
NotInstalled, // "not-installed"
NotRunning, // "not-running"
IncorrectVersion {
expected: VersionRange,
received: Version,
}, // { "incorrect-version": { "expected": "0.1.0", "received": "^0.2.0" } }
ConfigUnsatisfied(Vec<String>), // { "config-unsatisfied": ["Bitcoin Core must have pruning set to manual."] }
PointerUpdateError(String), // { "pointer-update-error": "Bitcoin Core RPC Port must not be 18332" }
Other(String), // { "other": "Well fuck." }
}
impl std::fmt::Display for DependencyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use DependencyError::*;
match self {
NotInstalled => write!(f, "Not Installed"),
NotRunning => write!(f, "Not Running"),
IncorrectVersion { expected, received } => write!(
f,
"Incorrect Version: Expected {}, Received {}",
expected, received
),
ConfigUnsatisfied(rules) => {
write!(f, "Configuration Rule(s) Violated: {}", rules.join(", "))
}
PointerUpdateError(e) => write!(f, "Pointer Update Caused {}", e),
Other(e) => write!(f, "System Error: {}", e),
}
}
}
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct TaggedDependencyError {
pub dependency: String,
pub error: DependencyError,
}
impl std::fmt::Display for TaggedDependencyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.dependency, self.error)
}
}
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Dependencies(pub LinearMap<String, DepInfo>);
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DepInfo {
pub version: VersionRange,
pub optional: Option<String>,
pub description: Option<String>,
#[serde(default)]
pub mount_public: bool,
#[serde(default)]
pub mount_shared: bool,
#[serde(default)]
pub config: Vec<ConfigRuleEntryWithSuggestions>,
}
impl DepInfo {
pub async fn satisfied(
&self,
dependency_id: &str,
dependency_config: Option<Config>, // fetch if none
dependent_id: &str,
dependent_config: &Config,
) -> Result<Result<(), DependencyError>, Error> {
let info = if let Some(info) = crate::apps::list_info().await?.remove(dependency_id) {
info
} else {
return Ok(Err(DependencyError::NotInstalled));
};
if !&info.version.satisfies(&self.version) {
return Ok(Err(DependencyError::IncorrectVersion {
expected: self.version.clone(),
received: info.version.clone(),
}));
}
let dependency_config = if let Some(cfg) = dependency_config {
cfg
} else {
let app_config = crate::apps::config(dependency_id).await?;
if let Some(cfg) = app_config.config {
cfg
} else {
app_config
.spec
.gen(&mut rand::rngs::StdRng::from_entropy(), &None)
.unwrap_or_default()
}
};
let mut errors = Vec::new();
let mut cfgs = LinearMap::with_capacity(2);
cfgs.insert(dependency_id, Cow::Borrowed(&dependency_config));
cfgs.insert(dependent_id, Cow::Borrowed(dependent_config));
for rule in self.config.iter() {
if !(rule.entry.rule.compiled)(&dependency_config, &cfgs) {
errors.push(rule.entry.description.clone());
}
}
if !errors.is_empty() {
return Ok(Err(DependencyError::ConfigUnsatisfied(errors)));
}
if crate::apps::status(dependency_id).await?.status != crate::apps::DockerStatus::Running {
return Ok(Err(DependencyError::NotRunning));
}
Ok(Ok(()))
}
}
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppDepInfo {
#[serde(flatten)]
pub info: DepInfo,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<DependencyError>,
}
#[derive(Debug, Default, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppDependencies(pub LinearMap<String, AppDepInfo>);
pub async fn check_dependencies(
manifest: ManifestLatest,
dependent_config: &Config,
dependent_config_spec: &ConfigSpec,
) -> Result<AppDependencies, Error> {
let mut deps = AppDependencies::default();
for (dependency_id, dependency_info) in manifest.dependencies.0.into_iter() {
let required = dependency_info.optional.is_none()
|| dependent_config_spec.requires(&dependency_id, dependent_config);
let error = dependency_info
.satisfied(&dependency_id, None, &manifest.id, dependent_config)
.await?
.err();
let app_dep_info = AppDepInfo {
error,
required,
info: dependency_info,
};
deps.0.insert(dependency_id, app_dep_info);
}
Ok(deps)
}
pub async fn auto_configure(
dependent: &str,
dependency: &str,
dry_run: bool,
) -> Result<crate::config::ConfigurationRes, Error> {
let (dependent_config, mut dependency_config, manifest) = futures::try_join!(
crate::apps::config_or_default(dependent),
crate::apps::config_or_default(dependency),
crate::apps::manifest(dependent)
)?;
let mut cfgs = LinearMap::new();
cfgs.insert(dependent, Cow::Borrowed(&dependent_config));
cfgs.insert(dependency, Cow::Owned(dependency_config.clone()));
let dep_info = manifest
.dependencies
.0
.get(dependency)
.ok_or_else(|| failure::format_err!("{} Does Not Depend On {}", dependent, dependency))
.no_code()?;
for rule in &dep_info.config {
if let Err(e) = rule.apply(dependency, &mut dependency_config, &mut cfgs) {
log::warn!("Rule Unsatisfied After Applying Suggestions: {}", e);
}
}
crate::config::configure(dependency, Some(dependency_config), None, dry_run).await
}
pub async fn update_shared(dependency_id: &str) -> Result<(), Error> {
let dependency_manifest = crate::apps::manifest(dependency_id).await?;
if let Some(shared) = dependency_manifest.shared {
for dependent_id in &crate::apps::dependents(dependency_id, false).await? {
let dependent_manifest = crate::apps::manifest(&dependent_id).await?;
if dependent_manifest
.dependencies
.0
.get(dependency_id)
.ok_or_else(|| failure::format_err!("failed to index dependent: {}", dependent_id))?
.mount_shared
{
tokio::fs::create_dir_all(
Path::new(crate::VOLUMES)
.join(dependency_id)
.join(&shared)
.join(&dependent_id),
)
.await?;
}
}
}
Ok(())
}
pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
let dependent_manifest = crate::apps::manifest(dependent_id).await?;
let dependency_manifests = futures::future::try_join_all(
dependent_manifest
.dependencies
.0
.into_iter()
.filter(|(_, info)| info.mount_public || info.mount_shared)
.map(|(id, info)| async {
crate::apps::manifest(&id).await.map(|man| (id, info, man))
}),
)
.await?;
// i just have a gut feeling this shouldn't be concurrent
for (dependency_id, info, dependency_manifest) in dependency_manifests {
match (dependency_manifest.public, info.mount_public) {
(Some(public), true) => {
let public_path = Path::new(crate::VOLUMES).join(&dependency_id).join(public);
if let Ok(metadata) = tokio::fs::metadata(&public_path).await {
if metadata.is_dir() {
crate::disks::bind(
public_path,
Path::new(crate::VOLUMES)
.join(&dependent_id)
.join("start9")
.join("public")
.join(&dependency_id),
true,
)
.await?
}
}
}
_ => (),
}
match (dependency_manifest.shared, info.mount_shared) {
(Some(shared), true) => {
let shared_path = Path::new(crate::VOLUMES)
.join(&dependency_id)
.join(shared)
.join(dependent_id); // namespaced by dependent
tokio::fs::create_dir_all(&shared_path).await?;
if let Ok(metadata) = tokio::fs::metadata(&shared_path).await {
if metadata.is_dir() {
crate::disks::bind(
shared_path,
Path::new(crate::VOLUMES)
.join(&dependent_id)
.join("start9")
.join("shared")
.join(&dependency_id),
false,
)
.await?
}
}
}
_ => (),
}
}
Ok(())
}

230
appmgr/src/disks.rs Normal file
View File

@@ -0,0 +1,230 @@
use std::path::Path;
use futures::future::try_join_all;
use crate::util::Invoke;
use crate::Error;
use crate::ResultExt;
pub const FSTAB: &'static str = "/etc/fstab";
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DiskInfo {
pub logicalname: String,
pub size: String,
pub description: Option<String>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct PartitionInfo {
pub logicalname: String,
pub is_mounted: bool,
pub size: Option<String>,
pub label: Option<String>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Disk {
#[serde(flatten)]
pub info: DiskInfo,
pub partitions: Vec<PartitionInfo>,
}
pub async fn list() -> Result<Vec<Disk>, Error> {
let output = tokio::process::Command::new("parted")
.arg("-lm")
.invoke("GNU Parted")
.await?;
let output_str = std::str::from_utf8(&output).no_code()?;
let disks = output_str.split("\n\n").filter_map(|s| -> Option<Disk> {
let mut lines = s.split("\n");
let has_size = lines.next()? == "BYT;";
let disk_info_line = lines.next()?;
let mut disk_info_iter = disk_info_line.split(":");
let logicalname = disk_info_iter.next()?.to_owned();
let partition_prefix = if logicalname.ends_with(|c: char| c.is_digit(10)) {
logicalname.clone() + "p"
} else {
logicalname.clone()
};
let size = disk_info_iter.next()?.to_owned();
disk_info_iter.next()?; // transport-type
disk_info_iter.next()?; // logical-sector-size
disk_info_iter.next()?; // physical-sector-size
disk_info_iter.next()?; // partition-table-type
let description = disk_info_iter.next()?;
let description = if description.is_empty() {
None
} else {
Some(description.to_owned())
};
let info = DiskInfo {
logicalname,
size,
description,
};
let partitions = lines
.filter_map(|partition_info_line| -> Option<PartitionInfo> {
let mut partition_info_iter = partition_info_line.split(":");
let partition_idx = partition_info_iter.next()?;
let logicalname = partition_prefix.clone() + partition_idx;
let size = if has_size {
partition_info_iter.next()?; // begin
partition_info_iter.next()?; // end
Some(partition_info_iter.next()?.to_owned())
} else {
None
};
Some(PartitionInfo {
logicalname,
is_mounted: false,
size,
label: None,
})
})
.collect();
Some(Disk { info, partitions })
});
try_join_all(disks.map(|disk| async move {
Ok(Disk {
info: disk.info,
partitions: try_join_all(disk.partitions.into_iter().map(|mut partition| async move {
let mut blkid_command = tokio::process::Command::new("blkid");
let (blkid_res, findmnt_status) = futures::join!(
blkid_command
.arg(&partition.logicalname)
.arg("-s")
.arg("LABEL")
.arg("-o")
.arg("value")
.invoke("BLKID"),
tokio::process::Command::new("findmnt")
.arg(&partition.logicalname)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
);
let blkid_output = blkid_res?;
let label = std::str::from_utf8(&blkid_output).no_code()?.trim();
if !label.is_empty() {
partition.label = Some(label.to_owned());
}
if findmnt_status?.success() {
partition.is_mounted = true;
}
Ok::<_, Error>(partition)
}))
.await?,
})
}))
.await
}
pub async fn mount<P: AsRef<Path>>(logicalname: &str, mount_point: P) -> Result<(), Error> {
let is_mountpoint = tokio::process::Command::new("mountpoint")
.arg(mount_point.as_ref())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await?;
if is_mountpoint.success() {
unmount(mount_point.as_ref()).await?;
}
tokio::fs::create_dir_all(&mount_point).await?;
let mount_output = tokio::process::Command::new("mount")
.arg(logicalname)
.arg(mount_point.as_ref())
.output()
.await?;
crate::ensure_code!(
mount_output.status.success(),
crate::error::FILESYSTEM_ERROR,
"Error Mounting Drive: {}",
std::str::from_utf8(&mount_output.stderr).unwrap_or("Unknown Error")
);
Ok(())
}
pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
src: P0,
dst: P1,
read_only: bool,
) -> Result<(), Error> {
let is_mountpoint = tokio::process::Command::new("mountpoint")
.arg(dst.as_ref())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await?;
if is_mountpoint.success() {
unmount(dst.as_ref()).await?;
}
tokio::fs::create_dir_all(&dst).await?;
let mut mount_cmd = tokio::process::Command::new("mount");
mount_cmd.arg("--bind");
if read_only {
mount_cmd.arg("-o").arg("ro");
}
let mount_output = mount_cmd
.arg(src.as_ref())
.arg(dst.as_ref())
.output()
.await?;
crate::ensure_code!(
mount_output.status.success(),
crate::error::FILESYSTEM_ERROR,
"Error Binding {} to {}: {}",
src.as_ref().display(),
dst.as_ref().display(),
std::str::from_utf8(&mount_output.stderr).unwrap_or("Unknown Error")
);
Ok(())
}
pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
let umount_output = tokio::process::Command::new("umount")
.arg(mount_point.as_ref())
.output()
.await?;
crate::ensure_code!(
umount_output.status.success(),
crate::error::FILESYSTEM_ERROR,
"Error Unmounting Drive: {}",
std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error")
);
tokio::fs::remove_dir_all(mount_point.as_ref()).await?;
Ok(())
}
#[must_use]
pub struct MountGuard<P: AsRef<Path>> {
path: Option<P>,
}
impl<P: AsRef<Path>> MountGuard<P> {
pub async fn new(logicalname: &str, mount_point: P) -> Result<Self, Error> {
mount(logicalname, mount_point.as_ref()).await?;
Ok(Self {
path: Some(mount_point),
})
}
pub async fn unmount(mut self) -> Result<(), Error> {
if let Some(ref path) = self.path {
unmount(path).await?;
self.path = None;
}
Ok(())
}
}
impl<P: AsRef<Path>> Drop for MountGuard<P> {
fn drop(&mut self) {
if let Some(ref path) = self.path {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(unmount(path))
.unwrap()
}
}
}

107
appmgr/src/error.rs Normal file
View File

@@ -0,0 +1,107 @@
use std::fmt::Display;
pub const GENERAL_ERROR: i32 = 1;
pub const FILESYSTEM_ERROR: i32 = 2;
pub const DOCKER_ERROR: i32 = 3;
pub const CFG_SPEC_VIOLATION: i32 = 4;
pub const CFG_RULES_VIOLATION: i32 = 5;
pub const NOT_FOUND: i32 = 6;
pub const INVALID_BACKUP_PASSWORD: i32 = 7;
pub const VERSION_INCOMPATIBLE: i32 = 8;
pub const NETWORK_ERROR: i32 = 9;
pub const REGISTRY_ERROR: i32 = 10;
pub const SERDE_ERROR: i32 = 11;
#[derive(Debug, Fail)]
#[fail(display = "{}", _0)]
pub struct Error {
pub failure: failure::Error,
pub code: Option<i32>,
}
impl Error {
pub fn new<E: Into<failure::Error>>(e: E, code: Option<i32>) -> Self {
Error {
failure: e.into(),
code,
}
}
pub fn from<E: Into<failure::Error>>(e: E) -> Self {
Error {
failure: e.into(),
code: None,
}
}
}
impl From<failure::Error> for Error {
fn from(e: failure::Error) -> Self {
Error {
failure: e,
code: None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error {
failure: e.into(),
code: Some(2),
}
}
}
pub trait ResultExt<T, E>
where
Self: Sized,
{
fn with_code(self, code: i32) -> Result<T, Error>;
fn with_ctx<F: FnOnce(&E) -> (Option<i32>, D), D: Display + Send + Sync + 'static>(
self,
f: F,
) -> Result<T, Error>;
fn no_code(self) -> Result<T, Error>;
}
impl<T, E> ResultExt<T, E> for Result<T, E>
where
failure::Error: From<E>,
{
fn with_code(self, code: i32) -> Result<T, Error> {
#[cfg(not(feature = "production"))]
assert!(code != 0);
self.map_err(|e| Error {
failure: e.into(),
code: Some(code),
})
}
fn with_ctx<F: FnOnce(&E) -> (Option<i32>, D), D: Display + Send + Sync + 'static>(
self,
f: F,
) -> Result<T, Error> {
self.map_err(|e| {
let (code, ctx) = f(&e);
let failure = failure::Error::from(e).context(ctx);
Error {
code,
failure: failure.into(),
}
})
}
fn no_code(self) -> Result<T, Error> {
self.map_err(|e| Error {
failure: e.into(),
code: None,
})
}
}
#[macro_export]
macro_rules! ensure_code {
($x:expr, $c:expr, $fmt:expr $(, $arg:expr)*) => {
if !($x) {
return Err(crate::Error {
failure: format_err!($fmt, $($arg, )*),
code: Some($c),
});
}
};
}

130
appmgr/src/index.rs Normal file
View File

@@ -0,0 +1,130 @@
use std::cmp::Ord;
use std::ffi::OsStr;
use std::iter::FromIterator;
use std::path::Path;
use emver::{Version, VersionRange};
use futures::future::{BoxFuture, FutureExt};
use linear_map::LinearMap;
use crate::inspect::info_full;
use crate::manifest::{Description, ManifestLatest};
use crate::{Error, ResultExt};
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct AppIndex(pub LinearMap<String, IndexInfo>);
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndexInfo {
pub title: String,
pub description: Description,
pub version_info: Vec<VersionInfo>,
pub icon_type: String,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct VersionInfo {
pub version: Version,
pub release_notes: String,
pub os_version_required: VersionRange,
pub os_version_recommended: VersionRange,
}
const NULL_VERSION: Version = Version::new(0, 0, 0, 0);
impl AppIndex {
fn add(&mut self, manifest: ManifestLatest) {
if let Some(ref mut entry) = self.0.get_mut(&manifest.id) {
if entry
.version_info
.get(0)
.map(|i| &i.version)
.unwrap_or(&NULL_VERSION)
<= &manifest.version
{
entry.title = manifest.title;
entry.description = manifest.description;
}
entry.version_info.push(VersionInfo {
version: manifest.version,
release_notes: manifest.release_notes,
os_version_required: manifest.os_version_required,
os_version_recommended: manifest.os_version_recommended,
});
entry
.version_info
.sort_unstable_by(|a, b| b.version.cmp(&a.version));
entry.version_info.dedup_by(|a, b| a.version == b.version);
} else {
self.0.insert(
manifest.id,
IndexInfo {
title: manifest.title,
description: manifest.description,
version_info: vec![VersionInfo {
version: manifest.version,
release_notes: manifest.release_notes,
os_version_required: manifest.os_version_required,
os_version_recommended: manifest.os_version_recommended,
}],
icon_type: "png".to_owned(), // TODO
},
);
}
}
}
impl Extend<ManifestLatest> for AppIndex {
fn extend<I: IntoIterator<Item = ManifestLatest>>(&mut self, iter: I) {
for manifest in iter {
self.add(manifest);
}
}
}
impl FromIterator<ManifestLatest> for AppIndex {
fn from_iter<I: IntoIterator<Item = ManifestLatest>>(iter: I) -> Self {
let mut res = Self::default();
res.extend(iter);
res
}
}
pub async fn index<P: AsRef<Path>>(dir: P) -> Result<AppIndex, Error> {
let dir_path = dir.as_ref();
let mut idx = AppIndex::default();
fn index_rec<'a, P: AsRef<Path> + Send + Sync + 'a>(
idx: &'a mut AppIndex,
dir: P,
) -> BoxFuture<'a, Result<(), Error>> {
async move {
let dir_path = dir.as_ref();
if let Ok(_) = tokio::fs::metadata(dir_path.join(".ignore")).await {
log::info!("Skipping {}", dir_path.display());
return Ok(());
}
let mut entry_stream = tokio::fs::read_dir(dir_path).await?;
while let Some(entry) = entry_stream.next_entry().await? {
let path = entry.path();
let metadata = entry.metadata().await?;
if metadata.is_file() {
let ext = path.extension();
if ext == Some(OsStr::new("s9pk")) {
let info = info_full(&path, true, false)
.await
.with_ctx(|e| (e.code.clone(), format!("{}: {}", path.display(), e)))?;
idx.add(info.manifest.unwrap());
}
} else if metadata.is_dir() {
index_rec(idx, &path).await?;
}
}
Ok(())
}
.boxed()
}
index_rec(&mut idx, dir_path).await?;
Ok(idx)
}

195
appmgr/src/inspect.rs Normal file
View File

@@ -0,0 +1,195 @@
use std::path::Path;
use failure::ResultExt as _;
use futures::stream::StreamExt;
use tokio_tar as tar;
use crate::config::{ConfigRuleEntry, ConfigSpec};
use crate::manifest::{Manifest, ManifestLatest};
use crate::util::from_cbor_async_reader;
use crate::version::VersionT;
use crate::Error;
use crate::ResultExt as _;
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppInfoFull {
#[serde(flatten)]
pub info: AppInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest: Option<ManifestLatest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<AppConfig>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppInfo {
pub title: String,
pub version: emver::Version,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AppConfig {
pub spec: ConfigSpec,
pub rules: Vec<ConfigRuleEntry>,
}
pub async fn info_full<P: AsRef<Path>>(
path: P,
with_manifest: bool,
with_config: bool,
) -> Result<AppInfoFull, Error> {
let p = path.as_ref();
log::info!("Opening file.");
let r = tokio::fs::File::open(p)
.await
.with_context(|e| format!("{}: {}", p.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
log::info!("Extracting archive.");
let mut pkg = tar::Archive::new(r);
let mut entries = pkg.entries()?;
log::info!("Opening manifest from archive.");
let manifest = entries
.next()
.await
.ok_or(crate::install::Error::CorruptedPkgFile("missing manifest"))
.no_code()??;
crate::ensure_code!(
manifest.path()?.to_str() == Some("manifest.cbor"),
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
log::trace!("Deserializing manifest.");
let manifest: Manifest = from_cbor_async_reader(manifest).await?;
let manifest = manifest.into_latest();
crate::ensure_code!(
crate::version::Current::new()
.semver()
.satisfies(&manifest.os_version_required),
crate::error::VERSION_INCOMPATIBLE,
"AppMgr Version Not Compatible: needs {}",
manifest.os_version_required
);
Ok(AppInfoFull {
info: AppInfo {
title: manifest.title.clone(),
version: manifest.version.clone(),
},
manifest: if with_manifest { Some(manifest) } else { None },
config: if with_config {
log::info!("Opening config spec from archive.");
let spec = entries
.next()
.await
.ok_or(crate::install::Error::CorruptedPkgFile(
"missing config spec",
))
.no_code()??;
crate::ensure_code!(
spec.path()?.to_str() == Some("config_spec.cbor"),
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
log::trace!("Deserializing config spec.");
let spec = from_cbor_async_reader(spec).await?;
log::info!("Opening config rules from archive.");
let rules = entries
.next()
.await
.ok_or(crate::install::Error::CorruptedPkgFile(
"missing config rules",
))
.no_code()??;
crate::ensure_code!(
rules.path()?.to_str() == Some("config_rules.cbor"),
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
log::trace!("Deserializing config rules.");
let rules = from_cbor_async_reader(rules).await?;
Some(AppConfig { spec, rules })
} else {
None
},
})
}
pub async fn print_instructions<P: AsRef<Path>>(path: P) -> Result<(), Error> {
let p = path.as_ref();
log::info!("Opening file.");
let r = tokio::fs::File::open(p)
.await
.with_context(|e| format!("{}: {}", p.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
log::info!("Extracting archive.");
let mut pkg = tar::Archive::new(r);
let mut entries = pkg.entries()?;
log::info!("Opening manifest from archive.");
let manifest = entries
.next()
.await
.ok_or(crate::install::Error::CorruptedPkgFile("missing manifest"))
.no_code()??;
crate::ensure_code!(
manifest.path()?.to_str() == Some("manifest.cbor"),
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
log::trace!("Deserializing manifest.");
let manifest: Manifest = from_cbor_async_reader(manifest).await?;
let manifest = manifest.into_latest();
crate::ensure_code!(
crate::version::Current::new()
.semver()
.satisfies(&manifest.os_version_required),
crate::error::VERSION_INCOMPATIBLE,
"AppMgr Version Not Compatible: needs {}",
manifest.os_version_required
);
entries
.next()
.await
.ok_or(crate::install::Error::CorruptedPkgFile(
"missing config spec",
))
.no_code()??;
entries
.next()
.await
.ok_or(crate::install::Error::CorruptedPkgFile(
"missing config rules",
))
.no_code()??;
if manifest.has_instructions {
use tokio::io::AsyncWriteExt;
let mut instructions = entries
.next()
.await
.ok_or(crate::install::Error::CorruptedPkgFile(
"missing instructions",
))
.no_code()??;
let mut stdout = tokio::io::stdout();
tokio::io::copy(&mut instructions, &mut stdout)
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
stdout
.flush()
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
stdout
.shutdown()
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
} else {
return Err(failure::format_err!("No instructions for {}", p.display()))
.with_code(crate::error::NOT_FOUND);
}
Ok(())
}

564
appmgr/src/install.rs Normal file
View File

@@ -0,0 +1,564 @@
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::marker::Unpin;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::{
atomic::{self, AtomicBool, AtomicU64},
Arc,
};
use std::task::Context;
use std::task::Poll;
use std::time::Duration;
use failure::ResultExt as _;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use tokio::io::AsyncRead;
use tokio::io::AsyncWriteExt;
use tokio_tar as tar;
use crate::config::{ConfigRuleEntry, ConfigSpec};
use crate::manifest::{ImageConfig, Manifest, ManifestV0};
use crate::util::{from_cbor_async_reader, to_yaml_async_writer, AsyncCompat, PersistencePath};
use crate::version::VersionT;
use crate::ResultExt as _;
#[derive(Fail, Debug, Clone)]
pub enum Error {
#[fail(display = "Package File Invalid or Corrupted: {}", _0)]
CorruptedPkgFile(&'static str),
#[fail(display = "Invalid File Name")]
InvalidFileName,
}
pub async fn install_name(name_version: &str, use_cache: bool) -> Result<(), crate::Error> {
let name = name_version.split("@").next().unwrap();
let tmp_path = Path::new(crate::TMP_DIR).join(format!("{}.s9pk", name));
if !use_cache || !tmp_path.exists() {
download_name(name_version).await?;
}
install_path(
&tmp_path
.as_os_str()
.to_str()
.ok_or(Error::InvalidFileName)
.with_code(crate::error::FILESYSTEM_ERROR)?,
Some(name),
)
.await?;
tokio::fs::remove_file(&tmp_path)
.await
.with_context(|e| format!("{}: {}", tmp_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
Ok(())
}
struct CountingReader<R: AsyncRead>(pub R, pub Arc<AtomicU64>);
impl<R> AsyncRead for CountingReader<R>
where
R: AsyncRead,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
let atomic = self.as_ref().1.clone(); // TODO: not efficient
match unsafe { self.map_unchecked_mut(|a| &mut a.0) }.poll_read(cx, buf) {
Poll::Ready(Ok(res)) => {
atomic.fetch_add(res as u64, atomic::Ordering::SeqCst);
Poll::Ready(Ok(res))
}
a => a,
}
}
}
pub async fn download_name(name_version: &str) -> Result<PathBuf, crate::Error> {
let mut split = name_version.split("@");
let name = split.next().unwrap();
let req: Option<emver::VersionRange> = split.next().map(|a| a.parse()).transpose().no_code()?;
if let Some(req) = req {
download(
&format!("{}/{}.s9pk?spec={}", &*crate::APP_REGISTRY_URL, name, req),
Some(name),
)
.await
} else {
download(
&format!("{}/{}.s9pk", &*crate::APP_REGISTRY_URL, name),
Some(name),
)
.await
}
}
pub async fn download(url: &str, name: Option<&str>) -> Result<PathBuf, crate::Error> {
let url = reqwest::Url::parse(url).no_code()?;
log::info!("Downloading {}.", url.as_str());
let response = reqwest::get(url)
.await
.with_code(crate::error::NETWORK_ERROR)?
.error_for_status()
.with_code(crate::error::REGISTRY_ERROR)?;
tokio::fs::create_dir_all(crate::TMP_DIR).await?;
let tmp_file_path =
Path::new(crate::TMP_DIR).join(&format!("{}.s9pk", name.unwrap_or("download")));
let mut f = tokio::fs::File::create(&tmp_file_path).await?;
let len: Option<u64> = response.content_length().map(|a| {
log::info!("{}KiB to download.", a / 1024);
a
});
let done = Arc::new(AtomicBool::new(false));
let counter = Arc::new(AtomicU64::new(0));
let mut reader = CountingReader(
AsyncCompat(
response
.bytes_stream()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
.into_async_read(),
),
counter.clone(),
);
let done_handle = done.clone();
let download_handle = tokio::spawn(async move {
let res = tokio::io::copy(&mut reader, &mut f).await;
done_handle.store(true, atomic::Ordering::SeqCst);
res
});
let poll_handle = tokio::spawn(async move {
loop {
let is_done = done.load(atomic::Ordering::SeqCst);
let downloaded_bytes = counter.load(atomic::Ordering::SeqCst);
if !*crate::QUIET.read().await {
if let Some(len) = len {
print!("\rDownloading... {}%", downloaded_bytes * 100 / len);
} else {
print!("\rDownloading... {}KiB", downloaded_bytes / 1024);
}
}
if is_done {
break;
}
tokio::time::delay_for(Duration::from_millis(10)).await;
}
if !*crate::QUIET.read().await {
println!("\rDownloading... 100%");
}
});
download_handle.await.unwrap()?;
poll_handle.await.unwrap();
Ok(tmp_file_path)
}
pub async fn install_url(url: &str, name: Option<&str>) -> Result<(), crate::Error> {
let tmp_file_path = download(url, name).await?;
install_path(&tmp_file_path, name).await?;
tokio::fs::remove_file(&tmp_file_path)
.await
.with_context(|e| format!("{}: {}", tmp_file_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
Ok(())
}
pub async fn install_path<P: AsRef<Path>>(p: P, name: Option<&str>) -> Result<(), crate::Error> {
let path = p.as_ref();
log::info!(
"Starting install of {}.",
path.file_name()
.and_then(|a| a.to_str())
.ok_or(Error::InvalidFileName)
.no_code()?
);
let file = tokio::fs::File::open(&path)
.await
.with_context(|e| format!("{}: {}", path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
let len = file.metadata().await?.len();
let done = Arc::new(AtomicBool::new(false));
let counter = Arc::new(AtomicU64::new(0));
let done_handle = done.clone();
let name_clone = name.map(|a| a.to_owned());
let counter_clone = counter.clone();
let poll_handle = tokio::spawn(async move {
loop {
let is_done = done.load(atomic::Ordering::SeqCst);
let installed_bytes = counter.load(atomic::Ordering::SeqCst);
if !*crate::QUIET.read().await {
print!("\rInstalling... {}%", installed_bytes * 100 / len);
}
if is_done {
break;
}
tokio::time::delay_for(Duration::from_millis(10)).await;
}
if !*crate::QUIET.read().await {
println!("\rInstalling... 100%");
}
});
let reader = CountingReader(file, counter_clone);
let res = install(reader, name_clone.as_ref().map(|a| a.as_str())).await;
done_handle.store(true, atomic::Ordering::SeqCst);
res?;
poll_handle.await.unwrap();
if !*crate::QUIET.read().await {
println!("Complete.");
}
Ok(())
}
pub async fn install<R: AsyncRead + Unpin + Send + Sync>(
r: R,
name: Option<&str>,
) -> Result<(), crate::Error> {
log::info!("Extracting archive.");
let mut pkg = tar::Archive::new(r);
let mut entries = pkg.entries()?;
log::info!("Opening manifest from archive.");
let manifest = entries
.next()
.await
.ok_or(Error::CorruptedPkgFile("missing manifest"))
.no_code()??;
crate::ensure_code!(
manifest.path()?.to_str() == Some("manifest.cbor"),
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
log::trace!("Deserializing manifest.");
let manifest: Manifest = from_cbor_async_reader(manifest).await.no_code()?;
match manifest {
Manifest::V0(m) => install_v0(m, entries, name).await?,
};
Ok(())
}
pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
manifest: ManifestV0,
mut entries: tar::Entries<R>,
name: Option<&str>,
) -> Result<(), crate::Error> {
crate::ensure_code!(
crate::version::Current::new()
.semver()
.satisfies(&manifest.os_version_required),
crate::error::VERSION_INCOMPATIBLE,
"OS Version Not Compatible: need {}",
manifest.os_version_required
);
if let Some(name) = name {
crate::ensure_code!(
manifest.id == name,
crate::error::GENERAL_ERROR,
"Package Name Does Not Match Expected"
);
}
let (ip, tor_addr, tor_key) = crate::tor::set_svc(
&manifest.id,
crate::tor::NewService {
ports: manifest.ports.clone(),
hidden_service_version: manifest.hidden_service_version,
},
)
.await?;
let recoverable = Path::new(crate::VOLUMES).join(&manifest.id).exists();
log::info!("Creating volume {}/{}.", crate::VOLUMES, manifest.id);
tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id)).await?;
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
let app_dir_path = app_dir.path();
if app_dir_path.exists() {
tokio::fs::remove_dir_all(&app_dir_path).await?;
}
tokio::fs::create_dir_all(&app_dir_path).await?;
let _lock = app_dir.lock(true).await?;
log::info!("Saving manifest.");
let mut manifest_out = app_dir.join("manifest.yaml").write(None).await?;
to_yaml_async_writer(&mut *manifest_out, &Manifest::V0(manifest.clone())).await?;
manifest_out.commit().await?;
log::info!("Opening config spec from archive.");
let config_spec = entries
.next()
.await
.ok_or(Error::CorruptedPkgFile("missing config spec"))
.no_code()??;
crate::ensure_code!(
config_spec.path()?.to_str() == Some("config_spec.cbor"),
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
log::trace!("Deserializing config spec.");
let config_spec: ConfigSpec = from_cbor_async_reader(config_spec).await?;
log::info!("Saving config spec.");
let mut config_spec_out = app_dir.join("config_spec.yaml").write(None).await?;
to_yaml_async_writer(&mut *config_spec_out, &config_spec).await?;
config_spec_out.commit().await?;
log::info!("Opening config rules from archive.");
let config_rules = entries
.next()
.await
.ok_or(Error::CorruptedPkgFile("missing config rules"))
.no_code()??;
crate::ensure_code!(
config_rules.path()?.to_str() == Some("config_rules.cbor"),
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
log::trace!("Deserializing config rules.");
let config_rules: Vec<ConfigRuleEntry> = from_cbor_async_reader(config_rules).await?;
log::info!("Saving config rules.");
let mut config_rules_out = app_dir.join("config_rules.yaml").write(None).await?;
to_yaml_async_writer(&mut *config_rules_out, &config_rules).await?;
config_rules_out.commit().await?;
if manifest.has_instructions {
log::info!("Opening instructions from archive.");
let mut instructions = entries
.next()
.await
.ok_or(Error::CorruptedPkgFile("missing config rules"))
.no_code()??;
crate::ensure_code!(
instructions.path()?.to_str() == Some("instructions.md"),
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
log::info!("Saving instructions.");
let mut instructions_out = app_dir.join("instructions.md").write(None).await?;
tokio::io::copy(&mut instructions, &mut *instructions_out)
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
instructions_out.commit().await?;
}
log::info!("Copying over assets.");
for asset in manifest.assets.iter() {
let dst_path = Path::new(crate::VOLUMES)
.join(&manifest.id)
.join(&asset.dst);
log::info!("Copying {} to {}", asset.src.display(), dst_path.display());
let src_path = Path::new(&asset.src);
log::info!("Opening {} from archive.", src_path.display());
let mut src = entries
.next()
.await
.ok_or(Error::CorruptedPkgFile("missing asset"))
.no_code()??;
crate::ensure_code!(
src.path()? == src_path,
crate::error::GENERAL_ERROR,
"Package File Invalid or Corrupted"
);
let dst_path_file = dst_path.join(src_path);
if dst_path_file.exists() && !asset.overwrite {
log::info!("{} already exists, skipping.", dst_path_file.display());
} else {
if dst_path_file.exists() {
if dst_path_file.is_dir() {
tokio::fs::remove_dir_all(&dst_path_file)
.await
.with_context(|e| format!("{}: {}", dst_path_file.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
} else {
tokio::fs::remove_file(&dst_path_file)
.await
.with_context(|e| format!("{}: {}", dst_path_file.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
}
src.unpack_in(&dst_path).await?;
if src.header().entry_type().is_dir() {
loop {
let mut file = entries
.next()
.await
.ok_or(Error::CorruptedPkgFile("missing asset"))
.no_code()??;
if file
.path()?
.starts_with(format!("APPMGR_DIR_END:{}", asset.src.display()))
{
break;
} else {
file.unpack_in(&dst_path).await?;
}
}
}
}
}
let tag = match &manifest.image {
ImageConfig::Tar => {
let image_name = format!("start9/{}", manifest.id);
let tag = format!("{}:latest", image_name);
if tokio::process::Command::new("docker")
.arg("images")
.arg("-q")
.arg(&image_name)
.output()
.await?
.stdout
.len()
> 0
{
tokio::process::Command::new("docker")
.arg("stop")
.arg(&manifest.id)
.spawn()?
.await?;
tokio::process::Command::new("docker")
.arg("rm")
.arg(&manifest.id)
.spawn()?
.await?;
crate::ensure_code!(
tokio::process::Command::new("docker")
.arg("rmi")
.arg(&image_name)
.output()
.await?
.status
.success(),
crate::error::DOCKER_ERROR,
"Failed to Remove Existing Image"
)
}
log::info!("Opening image.tar from archive.");
let mut image = entries
.next()
.await
.ok_or(Error::CorruptedPkgFile("missing image.tar"))
.no_code()??;
let image_path = image.path()?;
if image_path != Path::new("image.tar") {
return Err(crate::Error::from(format_err!(
"Package File Invalid or Corrupted: expected image.tar, got {}",
image_path.display()
)));
}
log::info!(
"Loading docker image start9/{} from image.tar.",
manifest.id
);
let mut child = tokio::process::Command::new("docker")
.arg("load")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::inherit())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.spawn()?;
let mut child_in = child.stdin.take().unwrap();
tokio::io::copy(&mut image, &mut child_in).await?;
child_in.flush().await?;
child_in.shutdown().await?;
drop(child_in);
crate::ensure_code!(
child.await?.success(),
crate::error::DOCKER_ERROR,
"Failed to Load Docker Image From Tar"
);
tag
}
};
log::info!("Creating docker container: {} from {}.", manifest.id, tag);
let volume_arg = format!(
"type=bind,src={}/{},dst={}",
crate::VOLUMES,
manifest.id,
manifest.mount.display()
);
let mut args = vec![
Cow::Borrowed(OsStr::new("create")),
Cow::Borrowed(OsStr::new("--restart")),
Cow::Borrowed(OsStr::new("on-failure")),
Cow::Borrowed(OsStr::new("--name")),
Cow::Borrowed(OsStr::new(&manifest.id)),
Cow::Borrowed(OsStr::new("--mount")),
Cow::Borrowed(OsStr::new(&volume_arg)),
Cow::Borrowed(OsStr::new("--net")),
Cow::Borrowed(OsStr::new("start9")),
Cow::Borrowed(OsStr::new("--ip")),
Cow::Owned(OsString::from(format!("{}", ip))),
];
if let (Some(ref tor_addr), Some(ref tor_key)) = (&tor_addr, &tor_key) {
args.extend(
std::iter::empty()
.chain(std::iter::once(Cow::Borrowed(OsStr::new("--env"))))
.chain(std::iter::once(Cow::Owned(OsString::from(format!(
"TOR_ADDRESS={}",
tor_addr
)))))
.chain(std::iter::once(Cow::Borrowed(OsStr::new("--env"))))
.chain(std::iter::once(Cow::Owned(OsString::from(format!(
"TOR_KEY={}",
tor_key
))))),
);
}
if let Some(shm_size_mb) = manifest.shm_size_mb {
args.push(Cow::Borrowed(OsStr::new("--shm-size")));
args.push(Cow::Owned(OsString::from(format!("{}m", shm_size_mb))));
}
args.push(Cow::Borrowed(OsStr::new(&tag)));
crate::ensure_code!(
std::process::Command::new("docker")
.args(&args)
.stdout(std::process::Stdio::null())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.status()?
.success(),
crate::error::DOCKER_ERROR,
"Failed to Create Docker Container"
);
tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join("start9")).await?;
if let Some(public) = manifest.public {
tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join(public))
.await?;
}
if let Some(shared) = manifest.shared {
tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join(shared))
.await?;
}
log::info!("Updating app list.");
crate::apps::add(
&manifest.id,
crate::apps::AppInfo {
title: manifest.title.clone(),
version: manifest.version.clone(),
tor_address: tor_addr.clone(),
configured: false,
recoverable,
needs_restart: false,
},
)
.await?;
let config = crate::apps::config(&manifest.id).await?;
if let Some(cfg) = config.config {
if config.spec.matches(&cfg).is_ok() {
crate::apps::set_configured(&manifest.id, true).await?;
}
} else {
let empty_config = crate::config::Config::default();
if config.spec.matches(&empty_config).is_ok() {
crate::config::configure(&manifest.id, Some(empty_config), None, false).await?;
}
}
for (dep_id, dep_info) in manifest.dependencies.0 {
if dep_info.mount_shared
&& crate::apps::list_info().await?.get(&dep_id).is_some()
&& crate::apps::manifest(&dep_id).await?.shared.is_some()
&& crate::apps::status(&dep_id).await?.status != crate::apps::DockerStatus::Stopped
{
crate::apps::set_needs_restart(&dep_id, true).await?;
}
}
Ok(())
}

51
appmgr/src/lib.rs Normal file
View File

@@ -0,0 +1,51 @@
#[macro_use]
extern crate failure;
#[macro_use]
extern crate pest_derive;
pub const TOR_RC: &'static str = "/root/appmgr/tor/torrc";
pub const SERVICES_YAML: &'static str = "tor/services.yaml";
pub const VOLUMES: &'static str = "/root/volumes";
pub const PERSISTENCE_DIR: &'static str = "/root/appmgr";
pub const TMP_DIR: &'static str = "/root/tmp/appmgr";
pub const BACKUP_MOUNT_POINT: &'static str = "/mnt/backup_drive";
pub const BACKUP_DIR: &'static str = "Embassy Backups";
pub const BUFFER_SIZE: usize = 1024;
pub const HOST_IP: [u8; 4] = [172, 18, 0, 1];
lazy_static::lazy_static! {
pub static ref REGISTRY_URL: String = std::env::var("REGISTRY_URL").unwrap_or_else(|_| "https://registry.start9labs.com".to_owned());
pub static ref SYS_REGISTRY_URL: String = format!("{}/sys", *REGISTRY_URL);
pub static ref APP_REGISTRY_URL: String = format!("{}/apps", *REGISTRY_URL);
pub static ref QUIET: tokio::sync::RwLock<bool> = tokio::sync::RwLock::new(!std::env::var("APPMGR_QUIET").map(|a| a == "0").unwrap_or(true));
}
pub mod apps;
pub mod backup;
pub mod config;
pub mod control;
pub mod dependencies;
pub mod disks;
pub mod error;
pub mod index;
pub mod inspect;
pub mod install;
pub mod logs;
pub mod manifest;
pub mod pack;
pub mod registry;
pub mod remove;
pub mod tor;
pub mod update;
pub mod util;
pub mod version;
pub use config::{configure, Config};
pub use control::{restart_app, start_app, stop_app, stop_dependents};
pub use error::{Error, ResultExt};
pub use install::{install_name, install_path, install_url};
pub use logs::{logs, notifications, stats, LogOptions};
pub use pack::{pack, verify};
pub use remove::remove;
pub use update::update;
pub use version::{init, self_update};

199
appmgr/src/logs.rs Normal file
View File

@@ -0,0 +1,199 @@
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::path::Path;
use failure::ResultExt as _;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use itertools::Itertools;
use crate::util::PersistencePath;
use crate::Error;
use crate::ResultExt as _;
#[derive(Clone, Copy, Debug, serde::Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Level {
Error,
Warn,
Success,
Info,
}
impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Level::Error => write!(f, "ERROR"),
Level::Warn => write!(f, "WARN"),
Level::Success => write!(f, "SUCCESS"),
Level::Info => write!(f, "INFO"),
}
}
}
impl std::str::FromStr for Level {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ERROR" => Ok(Level::Error),
"WARN" => Ok(Level::Warn),
"SUCCESS" => Ok(Level::Success),
"INFO" => Ok(Level::Info),
_ => Err(Error::from(format_err!("Unknown Notification Level"))),
}
}
}
#[derive(Clone, Debug, serde::Serialize)]
pub struct Notification {
pub time: i64,
pub level: Level,
pub code: usize,
pub title: String,
pub message: String,
}
impl std::fmt::Display for Notification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}:{}:{}",
self.level,
self.code,
self.title.replace(":", "\u{A789}"),
self.message.replace("\n", "\u{2026}")
)
}
}
impl std::str::FromStr for Notification {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut split = s.split(":");
Ok(Notification {
time: split
.next()
.ok_or_else(|| format_err!("missing time"))?
.parse::<f64>()
.map(|a| a as i64)
.no_code()?,
level: split
.next()
.ok_or_else(|| format_err!("missing level"))?
.parse()?,
code: split
.next()
.ok_or_else(|| format_err!("missing code"))?
.parse()
.no_code()?,
title: split
.next()
.ok_or_else(|| format_err!("missing title"))?
.replace("\u{A789}", ":"),
message: split
.intersperse(":")
.collect::<String>()
.replace("\u{2026}", "\n"),
})
}
}
pub struct LogOptions<A: AsRef<str>, B: AsRef<str>> {
pub details: bool,
pub follow: bool,
pub since: Option<A>,
pub until: Option<B>,
pub tail: Option<usize>,
pub timestamps: bool,
}
pub async fn logs<A: AsRef<str>, B: AsRef<str>>(
name: &str,
options: LogOptions<A, B>,
) -> Result<(), Error> {
let mut args = vec![Cow::Borrowed(OsStr::new("logs"))];
if options.details {
args.push(Cow::Borrowed(OsStr::new("--details")));
}
if options.follow {
args.push(Cow::Borrowed(OsStr::new("-f")));
}
if let Some(since) = options.since.as_ref() {
args.push(Cow::Borrowed(OsStr::new("--since")));
args.push(Cow::Borrowed(OsStr::new(since.as_ref())));
}
if let Some(until) = options.until.as_ref() {
args.push(Cow::Borrowed(OsStr::new("--until")));
args.push(Cow::Borrowed(OsStr::new(until.as_ref())));
}
if let Some(tail) = options.tail {
args.push(Cow::Borrowed(OsStr::new("--tail")));
args.push(Cow::Owned(OsString::from(format!("{}", tail))));
}
if options.timestamps {
args.push(Cow::Borrowed(OsStr::new("-t")));
}
args.push(Cow::Borrowed(OsStr::new(name)));
crate::ensure_code!(
std::process::Command::new("docker")
.args(args.into_iter())
.status()?
.success(),
crate::error::DOCKER_ERROR,
"Failed to Collect Logs from Docker"
);
Ok(())
}
pub async fn notifications(id: &str) -> Result<Vec<Notification>, Error> {
let p = PersistencePath::from_ref("notifications").join(id).tmp();
if let Some(parent) = p.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent).await?;
}
}
match tokio::fs::rename(
Path::new(crate::VOLUMES)
.join(id)
.join("start9")
.join("notifications.log"),
&p,
)
.await
{
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
a => a,
}?;
let f = tokio::fs::File::open(&p)
.await
.with_context(|e| format!("{}: {}", p.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
tokio::io::AsyncBufReadExt::lines(tokio::io::BufReader::new(f))
.map(|a| a.map_err(From::from).and_then(|a| a.parse()))
.try_collect()
.await
}
pub async fn stats(id: &str) -> Result<serde_yaml::Value, Error> {
let p = PersistencePath::from_ref("stats").join(id).tmp();
if let Some(parent) = p.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent).await?;
}
}
match tokio::fs::copy(
Path::new(crate::VOLUMES)
.join(id)
.join("start9")
.join("stats.yaml"),
&p,
)
.await
{
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(serde_yaml::Value::Null)
}
a => a,
}?;
let f = tokio::fs::File::open(&p)
.await
.with_context(|e| format!("{}: {}", p.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
crate::util::from_yaml_async_reader(f).await.no_code()
}

1645
appmgr/src/main.rs Normal file

File diff suppressed because it is too large Load Diff

76
appmgr/src/manifest.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::path::PathBuf;
use linear_map::LinearMap;
use crate::dependencies::Dependencies;
use crate::tor::HiddenServiceVersion;
use crate::tor::PortMapping;
pub type ManifestLatest = ManifestV0;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Description {
pub short: String,
pub long: String,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ImageConfig {
Tar,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Asset {
pub src: PathBuf,
pub dst: PathBuf,
pub overwrite: bool,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ManifestV0 {
pub id: String,
pub version: emver::Version,
pub title: String,
pub description: Description,
pub release_notes: String,
#[serde(default)]
pub has_instructions: bool,
#[serde(default = "emver::VersionRange::any")]
pub os_version_required: emver::VersionRange,
#[serde(default = "emver::VersionRange::any")]
pub os_version_recommended: emver::VersionRange,
pub ports: Vec<PortMapping>,
pub image: ImageConfig,
#[serde(default)]
pub shm_size_mb: Option<usize>,
pub mount: PathBuf,
#[serde(default)]
pub public: Option<PathBuf>,
#[serde(default)]
pub shared: Option<PathBuf>,
#[serde(default)]
pub assets: Vec<Asset>,
#[serde(default)]
pub hidden_service_version: HiddenServiceVersion,
#[serde(default)]
pub dependencies: Dependencies,
#[serde(flatten)]
pub extra: LinearMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(tag = "compat")]
#[serde(rename_all = "lowercase")]
pub enum Manifest {
V0(ManifestV0),
}
impl Manifest {
pub fn into_latest(self) -> ManifestLatest {
match self {
Manifest::V0(m) => m,
}
}
}

398
appmgr/src/pack.rs Normal file
View File

@@ -0,0 +1,398 @@
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use failure::ResultExt;
use futures::stream::StreamExt;
use linear_map::LinearMap;
use rand::SeedableRng;
use tokio_tar as tar;
use crate::config::{ConfigRuleEntry, ConfigSpec};
use crate::manifest::{ImageConfig, Manifest};
use crate::util::{from_cbor_async_reader, from_json_async_reader, from_yaml_async_reader};
use crate::version::VersionT;
#[derive(Clone, Debug, Fail)]
pub enum Error {
#[fail(display = "Invalid Directory Name: {}", _0)]
InvalidDirectoryName(String),
#[fail(display = "Invalid File Name: {}", _0)]
InvalidFileName(String),
#[fail(display = "Invalid Output Path: {}", _0)]
InvalidOutputPath(String),
}
pub async fn pack(path: &str, output: &str) -> Result<(), failure::Error> {
let path = Path::new(path.trim_end_matches("/"));
let output = Path::new(output);
ensure!(
output
.extension()
.and_then(|a| a.to_str())
.ok_or_else(|| Error::InvalidOutputPath(format!("{}", output.display())))?
== "s9pk",
"Extension Must Be '.s9pk'"
);
log::info!(
"Starting pack of {} to {}.",
path.file_name()
.and_then(|a| a.to_str())
.ok_or_else(|| Error::InvalidDirectoryName(format!("{}", path.display())))?,
output.display(),
);
let out_file = tokio::fs::File::create(output).await?;
let mut out = tar::Builder::new(out_file);
log::info!("Reading {}/manifest.yaml.", path.display());
let manifest: Manifest = crate::util::from_yaml_async_reader(
tokio::fs::File::open(path.join("manifest.yaml"))
.await
.with_context(|e| format!("{}: manifest.yaml", e))?,
)
.await?;
log::info!("Writing manifest to archive.");
let bin_manifest = serde_cbor::to_vec(&manifest)?;
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_size(bin_manifest.len() as u64);
out.append_data(
&mut manifest_header,
"manifest.cbor",
std::io::Cursor::new(bin_manifest),
)
.await?;
let manifest = manifest.into_latest();
ensure!(
crate::version::Current::new()
.semver()
.satisfies(&manifest.os_version_required),
"Unsupported AppMgr version: expected {}",
manifest.os_version_required
);
log::info!("Reading {}/config_spec.yaml.", path.display());
let config_spec: ConfigSpec = from_yaml_async_reader(
tokio::fs::File::open(path.join("config_spec.yaml"))
.await
.with_context(|e| format!("{}: config_spec.yaml", e))?,
)
.await?;
config_spec.validate(&manifest)?;
let config = config_spec.gen(&mut rand::rngs::StdRng::from_entropy(), &None)?;
config_spec.matches(&config)?;
log::info!("Writing config spec to archive.");
let bin_config_spec = serde_cbor::to_vec(&config_spec)?;
let mut config_spec_header = tar::Header::new_gnu();
config_spec_header.set_size(bin_config_spec.len() as u64);
out.append_data(
&mut config_spec_header,
"config_spec.cbor",
std::io::Cursor::new(bin_config_spec),
)
.await?;
log::info!("Reading {}/config_rules.yaml.", path.display());
let config_rules: Vec<ConfigRuleEntry> = from_yaml_async_reader(
tokio::fs::File::open(path.join("config_rules.yaml"))
.await
.with_context(|e| format!("{}: config_rules.yaml", e))?,
)
.await?;
let mut cfgs = LinearMap::new();
cfgs.insert(manifest.id.as_str(), Cow::Borrowed(&config));
for rule in &config_rules {
rule.check(&config, &cfgs)
.with_context(|e| format!("Default Config does not satisfy: {}", e))?;
}
log::info!("Writing config rules to archive.");
let bin_config_rules = serde_cbor::to_vec(&config_rules)?;
let mut config_rules_header = tar::Header::new_gnu();
config_rules_header.set_size(bin_config_rules.len() as u64);
out.append_data(
&mut config_rules_header,
"config_rules.cbor",
std::io::Cursor::new(bin_config_rules),
)
.await?;
if manifest.has_instructions {
log::info!("Packing instructions.md");
out.append_path_with_name(path.join("instructions.md"), "instructions.md")
.await?;
}
log::info!("Copying over assets.");
for asset in &manifest.assets {
let src_path = Path::new("assets").join(&asset.src);
log::info!("Reading {}/{}.", path.display(), src_path.display());
let file_path = path.join(&src_path);
let src = tokio::fs::File::open(&file_path)
.await
.with_context(|e| format!("{}: {}", e, src_path.display()))?;
log::info!("Writing {} to archive.", src_path.display());
if src.metadata().await?.is_dir() {
out.append_dir_all(&asset.src, &file_path).await?;
let mut h = tar::Header::new_gnu();
h.set_size(0);
h.set_path(format!("APPMGR_DIR_END:{}", asset.src.display()))?;
h.set_cksum();
out.append(&h, tokio::io::empty()).await?;
} else {
out.append_path_with_name(&file_path, &asset.src).await?;
}
}
match manifest.image {
ImageConfig::Tar => {
log::info!("Reading {}/image.tar.", path.display());
let image = tokio::fs::File::open(path.join("image.tar"))
.await
.with_context(|e| format!("{}: image.tar", e))?;
log::info!("Writing image.tar to archive.");
let mut header = tar::Header::new_gnu();
header.set_size(image.metadata().await?.len());
out.append_data(&mut header, "image.tar", image).await?;
}
}
out.into_inner().await?;
Ok(())
}
pub fn validate_path<P: AsRef<Path>>(p: P) -> Result<(), Error> {
let path = p.as_ref();
if path.is_absolute() {
return Err(Error::InvalidFileName(format!("{}", path.display())));
}
for seg in path {
if seg == ".." {
return Err(Error::InvalidFileName(format!("{}", path.display())));
}
}
Ok(())
}
pub async fn verify(path: &str) -> Result<(), failure::Error> {
let path = Path::new(path.trim_end_matches("/"));
ensure!(
path.extension()
.and_then(|a| a.to_str())
.ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?
== "s9pk",
"Extension Must Be '.s9pk'"
);
let name = path
.file_stem()
.and_then(|a| a.to_str())
.ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?;
ensure!(
!name.starts_with("start9")
&& name
.chars()
.filter(|c| !c.is_alphanumeric() && c != &'-')
.next()
.is_none(),
"Invalid Application ID"
);
log::info!(
"Starting verification of {}.",
path.file_name()
.and_then(|a| a.to_str())
.ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?,
);
{}
log::info!("Opening file.");
let r = tokio::fs::File::open(&path)
.await
.with_context(|e| format!("{}: {}", path.display(), e))?;
log::info!("Extracting archive.");
let mut pkg = tar::Archive::new(r);
let mut entries = pkg.entries()?;
log::info!("Opening manifest from archive.");
let manifest = entries
.next()
.await
.ok_or_else(|| format_err!("missing manifest"))??;
ensure!(
manifest.path()?.to_str() == Some("manifest.cbor"),
"Package File Invalid or Corrupted: expected manifest.cbor, got {}",
manifest.path()?.display()
);
log::trace!("Deserializing manifest.");
let manifest: Manifest = from_cbor_async_reader(manifest).await?;
let manifest = manifest.into_latest();
ensure!(
crate::version::Current::new()
.semver()
.satisfies(&manifest.os_version_required),
"Unsupported AppMgr Version: expected {}",
manifest.os_version_required
);
ensure!(manifest.id == name, "Package Name Does Not Match Expected",);
if let (Some(public), Some(shared)) = (&manifest.public, &manifest.shared) {
ensure!(
!public.starts_with(shared) && !shared.starts_with(public),
"Public Directory Conflicts With Shared Directory"
)
}
if let Some(public) = &manifest.public {
validate_path(public)?;
}
if let Some(shared) = &manifest.shared {
validate_path(shared)?;
}
log::info!("Opening config spec from archive.");
let config_spec = entries
.next()
.await
.ok_or_else(|| format_err!("missing config spec"))??;
ensure!(
config_spec.path()?.to_str() == Some("config_spec.cbor"),
"Package File Invalid or Corrupted: expected config_rules.cbor, got {}",
config_spec.path()?.display()
);
log::trace!("Deserializing config spec.");
let config_spec: ConfigSpec = from_cbor_async_reader(config_spec).await?;
log::trace!("Validating config spec.");
config_spec.validate(&manifest)?;
let config = config_spec.gen(&mut rand::rngs::StdRng::from_entropy(), &None)?;
config_spec.matches(&config)?;
log::info!("Opening config rules from archive.");
let config_rules = entries
.next()
.await
.ok_or_else(|| format_err!("missing config rules"))??;
ensure!(
config_rules.path()?.to_str() == Some("config_rules.cbor"),
"Package File Invalid or Corrupted: expected config_rules.cbor, got {}",
config_rules.path()?.display()
);
log::trace!("Deserializing config rules.");
let config_rules: Vec<ConfigRuleEntry> = from_cbor_async_reader(config_rules).await?;
log::trace!("Validating config rules against config spec.");
let mut cfgs = LinearMap::new();
cfgs.insert(name, Cow::Borrowed(&config));
for rule in &config_rules {
rule.check(&config, &cfgs)
.with_context(|e| format!("Default Config does not satisfy: {}", e))?;
}
if manifest.has_instructions {
let instructions = entries
.next()
.await
.ok_or_else(|| format_err!("missing instructions"))??;
ensure!(
instructions.path()?.to_str() == Some("instructions.md"),
"Package File Invalid or Corrupted: expected instructions.md, got {}",
instructions.path()?.display()
);
}
for asset_info in manifest.assets {
validate_path(&asset_info.src)?;
validate_path(&asset_info.dst)?;
let asset = entries
.next()
.await
.ok_or_else(|| format_err!("missing asset: {}", asset_info.src.display()))??;
if asset.header().entry_type().is_file() {
ensure!(
asset.path()?.to_str() == Some(&format!("{}", asset_info.src.display())),
"Package File Invalid or Corrupted: expected {}, got {}",
asset_info.src.display(),
asset.path()?.display()
);
} else if asset.header().entry_type().is_dir() {
ensure!(
asset.path()?.to_str() == Some(&format!("{}/", asset_info.src.display())),
"Package File Invalid or Corrupted: expected {}, got {}",
asset_info.src.display(),
asset.path()?.display()
);
loop {
let file = entries.next().await.ok_or_else(|| {
format_err!(
"missing directory end marker: APPMGR_DIR_END:{}",
asset_info.src.display()
)
})??;
if file
.path()?
.starts_with(format!("APPMGR_DIR_END:{}", asset_info.src.display()))
{
break;
} else {
ensure!(
file.path()?
.to_str()
.map(|p| p.starts_with(&format!("{}/", asset_info.src.display())))
.unwrap_or(false),
"Package File Invalid or Corrupted: expected {}, got {}",
asset_info.src.display(),
asset.path()?.display()
);
}
}
} else {
bail!("Asset Not Regular File: {}", asset_info.src.display());
}
}
match &manifest.image {
ImageConfig::Tar => {
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
struct DockerManifest {
config: PathBuf,
repo_tags: Vec<String>,
layers: Vec<PathBuf>,
}
let image_name = format!("start9/{}", manifest.id);
log::debug!("Opening image.tar from archive.");
let image = entries
.next()
.await
.ok_or_else(|| format_err!("missing image.tar"))??;
let image_path = image.path()?;
if image_path != Path::new("image.tar") {
return Err(format_err!(
"Package File Invalid or Corrupted: expected image.tar, got {}",
image_path.display()
));
}
log::info!("Verifying image.tar.");
let mut image_tar = tar::Archive::new(image);
let image_manifest = image_tar
.entries()?
.map(|e| {
let e = e?;
Ok((e.path()?.to_path_buf(), e))
})
.filter_map(|res: Result<(PathBuf, tar::Entry<_>), std::io::Error>| {
futures::future::ready(match res {
Ok((path, e)) => {
if path == Path::new("manifest.json") {
Some(Ok(e))
} else {
None
}
}
Err(e) => Some(Err(e)),
})
})
.next()
.await
.ok_or_else(|| format_err!("image.tar is missing manifest.json"))??;
let image_manifest: Vec<DockerManifest> =
from_json_async_reader(image_manifest).await?;
image_manifest
.into_iter()
.flat_map(|a| a.repo_tags)
.map(|t| {
if t.starts_with("start9/") {
if t.split(":").next().unwrap() != image_name {
Err(format_err!("Contains prohibited image tag: {}", t))
} else {
Ok(())
}
} else {
Ok(())
}
})
.collect::<Result<_, _>>()?;
}
};
Ok(())
}

66
appmgr/src/registry.rs Normal file
View File

@@ -0,0 +1,66 @@
use emver::VersionRange;
use crate::apps::AppConfig;
use crate::manifest::ManifestLatest;
use crate::Error;
use crate::ResultExt as _;
pub async fn manifest(id: &str, version: &VersionRange) -> Result<ManifestLatest, Error> {
let manifest: ManifestLatest = reqwest::get(&format!(
"{}/manifest/{}?spec={}",
&*crate::APP_REGISTRY_URL,
id,
version
))
.await
.with_code(crate::error::NETWORK_ERROR)?
.error_for_status()
.with_code(crate::error::REGISTRY_ERROR)?
.json()
.await
.with_code(crate::error::SERDE_ERROR)?;
Ok(manifest)
}
pub async fn version(id: &str, version: &VersionRange) -> Result<emver::Version, Error> {
#[derive(serde::Deserialize)]
struct VersionRes {
version: emver::Version,
}
let version: VersionRes = reqwest::get(&format!(
"{}/version/{}?spec={}",
&*crate::APP_REGISTRY_URL,
id,
version
))
.await
.with_code(crate::error::NETWORK_ERROR)?
.error_for_status()
.with_code(crate::error::REGISTRY_ERROR)?
.json()
.await
.with_code(crate::error::SERDE_ERROR)?;
Ok(version.version)
}
pub async fn config(id: &str, version: &VersionRange) -> Result<AppConfig, Error> {
let config: crate::inspect::AppConfig = reqwest::get(&format!(
"{}/config/{}?spec={}",
&*crate::APP_REGISTRY_URL,
id,
version
))
.await
.with_code(crate::error::NETWORK_ERROR)?
.error_for_status()
.with_code(crate::error::REGISTRY_ERROR)?
.json()
.await
.with_code(crate::error::SERDE_ERROR)?;
Ok(AppConfig {
config: None,
spec: config.spec,
rules: config.rules,
})
}

117
appmgr/src/remove.rs Normal file
View File

@@ -0,0 +1,117 @@
use std::path::Path;
use linear_map::LinearMap;
use crate::dependencies::{DependencyError, TaggedDependencyError};
use crate::Error;
pub async fn remove(
name: &str,
purge: bool,
dry_run: bool,
) -> Result<LinearMap<String, TaggedDependencyError>, Error> {
let manifest = crate::apps::manifest(name).await?;
let mut res = LinearMap::new();
crate::stop_dependents(name, dry_run, DependencyError::NotInstalled, &mut res).await?;
if dry_run {
return Ok(res);
}
let image_name = format!("start9/{}", name);
log::info!("Removing app from manifest.");
crate::apps::remove(name).await?;
log::info!("Stopping docker container.");
let res = crate::control::stop_app(name, false, false)
.await
.unwrap_or_else(|e| {
log::error!("Error stopping app: {}", e);
LinearMap::new()
});
log::info!("Removing docker container.");
if !std::process::Command::new("docker")
.args(&["rm", name])
.stdout(std::process::Stdio::null())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.status()?
.success()
{
log::error!("Failed to Remove Docker Container");
};
if !std::process::Command::new("docker")
.args(&["rmi", &image_name])
.stdout(std::process::Stdio::null())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.status()?
.success()
{
log::error!("Failed to Remove Docker Image");
};
if purge {
log::info!("Removing tor hidden service.");
crate::tor::rm_svc(name).await?;
log::info!("Removing app metadata.");
tokio::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name))
.await?;
log::info!("Destroying mounted volume.");
log::info!("Unbinding shared filesystem.");
for (dep, info) in manifest.dependencies.0.iter() {
if info.mount_public {
crate::disks::unmount(
Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("public")
.join(&dep),
)
.await?;
}
if info.mount_shared {
if let Some(shared) = match crate::apps::manifest(dep).await {
Ok(man) => man.shared,
Err(e) => {
log::error!("Failed to Fetch Dependency Manifest: {}", e);
None
}
} {
let path = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("shared")
.join(&dep);
if path.exists() {
crate::disks::unmount(&path).await?;
}
let path = Path::new(crate::VOLUMES).join(dep).join(&shared).join(name);
if path.exists() {
tokio::fs::remove_dir_all(
Path::new(crate::VOLUMES).join(dep).join(&shared).join(name),
)
.await?;
}
}
}
}
tokio::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name)).await?;
log::info!("Pruning unused docker images.");
crate::ensure_code!(
std::process::Command::new("docker")
.args(&["image", "prune", "-a", "-f"])
.stdout(std::process::Stdio::null())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.status()?
.success(),
crate::error::DOCKER_ERROR,
"Failed to Prune Docker Images"
);
};
Ok(res)
}

414
appmgr/src/tor.rs Normal file
View File

@@ -0,0 +1,414 @@
use std::collections::{BTreeSet, HashMap};
use std::net::Ipv4Addr;
use std::os::unix::process::ExitStatusExt;
use std::path::Path;
use std::time::{Duration, Instant};
use failure::ResultExt as _;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use crate::util::{PersistencePath, YamlUpdateHandle};
use crate::{Error, ResultExt as _};
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
pub struct PortMapping {
pub internal: u16,
pub tor: u16,
}
pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc";
pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor";
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum HiddenServiceVersion {
V1,
V2,
V3,
}
impl From<HiddenServiceVersion> for usize {
fn from(v: HiddenServiceVersion) -> Self {
match v {
HiddenServiceVersion::V1 => 1,
HiddenServiceVersion::V2 => 2,
HiddenServiceVersion::V3 => 3,
}
}
}
impl std::convert::TryFrom<usize> for HiddenServiceVersion {
type Error = failure::Error;
fn try_from(v: usize) -> Result<Self, Self::Error> {
Ok(match v {
1 => HiddenServiceVersion::V1,
2 => HiddenServiceVersion::V2,
3 => HiddenServiceVersion::V3,
n => bail!("Invalid HiddenServiceVersion {}", n),
})
}
}
impl Default for HiddenServiceVersion {
fn default() -> Self {
HiddenServiceVersion::V3
}
}
impl std::fmt::Display for HiddenServiceVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "HiddenServiceVersion {}", usize::from(*self))
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Service {
pub ip: Ipv4Addr,
pub ports: Vec<PortMapping>,
#[serde(default)]
pub hidden_service_version: HiddenServiceVersion,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct NewService {
pub ports: Vec<PortMapping>,
#[serde(default)]
pub hidden_service_version: HiddenServiceVersion,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct ServicesMap {
pub map: HashMap<String, Service>,
pub ips: BTreeSet<Ipv4Addr>,
}
impl Default for ServicesMap {
fn default() -> Self {
ServicesMap {
map: Default::default(),
ips: Default::default(),
}
}
}
impl ServicesMap {
pub fn add(&mut self, name: String, service: NewService) -> Ipv4Addr {
let ip = self
.map
.get(&name)
.map(|a| a.ip.clone())
.unwrap_or_else(|| {
Ipv4Addr::from(
u32::from(
self.ips
.range(..)
.next_back()
.cloned()
.unwrap_or_else(|| crate::HOST_IP.into()),
) + 1,
)
});
self.ips.insert(ip);
self.map.insert(
name,
Service {
ip,
ports: service.ports,
hidden_service_version: service.hidden_service_version,
},
);
ip
}
pub fn remove(&mut self, name: &str) {
let s = self.map.remove(name);
if let Some(s) = s {
self.ips.remove(&s.ip);
}
}
}
pub async fn services_map(path: &PersistencePath) -> Result<ServicesMap, Error> {
let f = path.maybe_read(false).await.transpose()?;
if let Some(mut f) = f {
crate::util::from_yaml_async_reader(&mut *f).await
} else {
Ok(Default::default())
}
}
pub async fn services_map_mut(
path: PersistencePath,
) -> Result<YamlUpdateHandle<ServicesMap>, Error> {
YamlUpdateHandle::new_or_default(path).await
}
pub async fn write_services(hidden_services: &ServicesMap) -> Result<(), Error> {
tokio::fs::copy(crate::TOR_RC, ETC_TOR_RC)
.await
.with_context(|e| format!("{} -> {}: {}", crate::TOR_RC, ETC_TOR_RC, e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
let mut f = tokio::fs::OpenOptions::new()
.append(true)
.open(ETC_TOR_RC)
.await?;
f.write_all(b"\n").await?;
for (name, service) in &hidden_services.map {
if service.ports.is_empty() {
continue;
}
f.write_all(b"\n").await?;
f.write_all(format!("# HIDDEN SERVICE FOR {}\n", name).as_bytes())
.await?;
f.write_all(
format!(
"HiddenServiceDir {}/app-{}/\n",
HIDDEN_SERVICE_DIR_ROOT, name
)
.as_bytes(),
)
.await?;
f.write_all(format!("{}\n", service.hidden_service_version).as_bytes())
.await?;
for port in &service.ports {
f.write_all(
format!(
"HiddenServicePort {} {}:{}\n",
port.tor, service.ip, port.internal
)
.as_bytes(),
)
.await?;
}
f.write_all(b"\n").await?;
}
Ok(())
}
pub async fn read_tor_address(name: &str, timeout: Option<Duration>) -> Result<String, Error> {
log::info!("Retrieving Tor hidden service address for {}.", name);
let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT)
.join(format!("app-{}", name))
.join("hostname");
if let Some(timeout) = timeout {
let start = Instant::now();
while {
if addr_path.exists() {
false
} else {
if start.elapsed() >= timeout {
log::warn!("Timed out waiting for tor to start.");
false
} else {
true
}
}
} {
tokio::time::delay_for(Duration::from_millis(100)).await;
}
}
let tor_addr = match tokio::fs::read_to_string(&addr_path).await {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(e)
.with_context(|e| format!("{}: {}", addr_path.display(), e))
.with_code(crate::error::NOT_FOUND),
a => a
.with_context(|e| format!("{}: {}", addr_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR),
}?;
Ok(tor_addr.trim().to_owned())
}
pub async fn read_tor_key(
name: &str,
version: HiddenServiceVersion,
timeout: Option<Duration>,
) -> Result<String, Error> {
log::info!("Retrieving Tor hidden service address for {}.", name);
let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT)
.join(format!("app-{}", name))
.join(match version {
HiddenServiceVersion::V3 => "hs_ed25519_secret_key",
_ => "private_key",
});
if let Some(timeout) = timeout {
let start = Instant::now();
while {
if addr_path.exists() {
false
} else {
if start.elapsed() >= timeout {
log::warn!("Timed out waiting for tor to start.");
false
} else {
true
}
}
} {
tokio::time::delay_for(Duration::from_millis(100)).await;
}
}
let tor_key = match version {
HiddenServiceVersion::V3 => {
let mut f = tokio::fs::File::open(&addr_path)
.await
.with_context(|e| format!("{}: {}", e, addr_path.display()))
.with_code(crate::error::FILESYSTEM_ERROR)?;
let mut buf = [0; 96];
f.read_exact(&mut buf).await?;
base32::encode(base32::Alphabet::RFC4648 { padding: false }, &buf[32..]).to_lowercase()
}
_ => tokio::fs::read_to_string(&addr_path)
.await
.with_context(|e| format!("{}: {}", e, addr_path.display()))
.with_code(crate::error::FILESYSTEM_ERROR)?
.trim_end_matches("\u{0}")
.to_string(),
};
Ok(tor_key.trim().to_owned())
}
pub async fn set_svc(
name: &str,
service: NewService,
) -> Result<(Ipv4Addr, Option<String>, Option<String>), Error> {
log::info!(
"Adding Tor hidden service {} to {}.",
name,
crate::SERVICES_YAML
);
let is_listening = !service.ports.is_empty();
let path = PersistencePath::from_ref(crate::SERVICES_YAML);
let mut hidden_services = services_map_mut(path).await?;
let ver = service.hidden_service_version;
let ip = hidden_services.add(name.to_owned(), service);
log::info!("Adding Tor hidden service {} to {}.", name, ETC_TOR_RC);
write_services(&hidden_services).await?;
hidden_services.commit().await?;
log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service")
.args(&["tor", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Tor: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok((
ip,
if is_listening {
Some(read_tor_address(name, Some(Duration::from_secs(30))).await?)
} else {
None
},
if is_listening {
Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?)
} else {
None
},
))
}
pub async fn rm_svc(name: &str) -> Result<(), Error> {
log::info!(
"Removing Tor hidden service {} from {}.",
name,
crate::SERVICES_YAML
);
let path = PersistencePath::from_ref(crate::SERVICES_YAML);
let mut hidden_services = services_map_mut(path).await?;
hidden_services.remove(name);
let hidden_service_path = Path::new(HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", name));
log::info!("Removing {}", hidden_service_path.display());
if hidden_service_path.exists() {
tokio::fs::remove_dir_all(hidden_service_path).await?;
}
log::info!("Removing Tor hidden service {} from {}.", name, ETC_TOR_RC);
write_services(&hidden_services).await?;
hidden_services.commit().await?;
log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service")
.args(&["tor", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Tor: {}",
svc_exit.code().unwrap_or(0)
);
Ok(())
}
pub async fn change_key(
name: &str,
key: Option<&ed25519_dalek::ExpandedSecretKey>,
) -> Result<(), Error> {
let hidden_service_path = Path::new(HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", name));
log::info!("Removing {}", hidden_service_path.display());
if hidden_service_path.exists() {
tokio::fs::remove_dir_all(&hidden_service_path)
.await
.with_context(|e| format!("{}: {}", hidden_service_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
if let Some(key) = key {
tokio::fs::create_dir_all(&hidden_service_path).await?;
let key_path = hidden_service_path.join("hs_ed25519_secret_key");
let mut key_data = b"== ed25519v1-secret: type0 ==".to_vec();
key_data.extend_from_slice(&key.to_bytes());
tokio::fs::write(&key_path, key_data)
.await
.with_context(|e| format!("{}: {}", key_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service")
.args(&["tor", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Tor: {}",
svc_exit.code().unwrap_or(0)
);
let mut info = crate::apps::list_info_mut().await?;
if let Some(mut i) = info.get_mut(name) {
if i.tor_address.is_some() {
i.tor_address = Some(read_tor_address(name, Some(Duration::from_secs(30))).await?);
}
}
Ok(())
}
pub async fn reload() -> Result<(), Error> {
let path = PersistencePath::from_ref(crate::SERVICES_YAML);
let hidden_services = services_map(&path).await?;
log::info!("Syncing Tor hidden services to {}.", ETC_TOR_RC);
write_services(&hidden_services).await?;
log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service")
.args(&["tor", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Tor: {}",
svc_exit.code().unwrap_or(0)
);
Ok(())
}
pub async fn restart() -> Result<(), Error> {
let path = PersistencePath::from_ref(crate::SERVICES_YAML);
let hidden_services = services_map(&path).await?;
log::info!("Syncing Tor hidden services to {}.", ETC_TOR_RC);
write_services(&hidden_services).await?;
log::info!("Restarting Tor.");
let svc_exit = std::process::Command::new("service")
.args(&["tor", "restart"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Restart Tor: {}",
svc_exit.code().unwrap_or(0)
);
Ok(())
}

80
appmgr/src/update.rs Normal file
View File

@@ -0,0 +1,80 @@
use linear_map::LinearMap;
use crate::dependencies::{DependencyError, TaggedDependencyError};
use crate::Error;
use crate::ResultExt as _;
pub async fn update(
name_version: &str,
dry_run: bool,
) -> Result<LinearMap<String, TaggedDependencyError>, Error> {
let mut name_version_iter = name_version.split("@");
let name = name_version_iter.next().unwrap();
let version_req = name_version_iter
.next()
.map(|v| v.parse())
.transpose()
.no_code()?
.unwrap_or_else(emver::VersionRange::any);
let version = crate::registry::version(name, &version_req).await?;
let mut res = LinearMap::new();
for dependent in crate::apps::dependents(name, false).await? {
if crate::apps::status(&dependent).await?.status != crate::apps::DockerStatus::Stopped {
let manifest = crate::apps::manifest(&dependent).await?;
match manifest.dependencies.0.get(name) {
Some(dep) if !version.satisfies(&dep.version) => {
crate::control::stop_dependents(
&dependent,
dry_run,
DependencyError::NotRunning,
&mut res,
)
.await?;
if crate::apps::status(name).await?.status != crate::apps::DockerStatus::Stopped
{
crate::control::stop_app(&dependent, false, dry_run).await?;
res.insert(
dependent,
TaggedDependencyError {
dependency: name.to_owned(),
error: DependencyError::IncorrectVersion {
expected: version_req.clone(),
received: version.clone(),
},
},
);
}
}
_ => {
crate::control::stop_dependents(
&dependent,
dry_run,
DependencyError::NotRunning,
&mut res,
)
.await?;
if crate::apps::status(name).await?.status != crate::apps::DockerStatus::Stopped
{
crate::control::stop_app(&dependent, false, dry_run).await?;
res.insert(
dependent,
TaggedDependencyError {
dependency: name.to_owned(),
error: DependencyError::NotRunning,
},
);
}
}
}
}
}
if dry_run {
return Ok(res);
}
let download_path = crate::install::download_name(name_version).await?;
crate::remove::remove(name, false, false).await?;
crate::install::install_path(download_path, Some(name)).await?;
crate::apps::set_recoverable(name, false).await?;
Ok(res)
}

553
appmgr/src/util.rs Normal file
View File

@@ -0,0 +1,553 @@
use std::fmt;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use failure::ResultExt as _;
use file_lock::FileLock;
use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::Error;
use crate::ResultExt as _;
#[derive(Debug, Clone)]
pub struct PersistencePath(PathBuf);
impl PersistencePath {
pub fn from_ref<P: AsRef<Path>>(p: P) -> Self {
let path = p.as_ref();
PersistencePath(if path.has_root() {
path.strip_prefix("/").unwrap().to_owned()
} else {
path.to_owned()
})
}
pub fn new(path: PathBuf) -> Self {
PersistencePath(if path.has_root() {
path.strip_prefix("/").unwrap().to_owned()
} else {
path.to_owned()
})
}
pub fn join<P: AsRef<Path>>(&self, path: P) -> Self {
PersistencePath::new(self.0.join(path))
}
pub fn tmp(&self) -> PathBuf {
Path::new(crate::TMP_DIR).join(&self.0)
}
pub fn path(&self) -> PathBuf {
Path::new(crate::PERSISTENCE_DIR).join(&self.0)
}
pub async fn lock(&self, for_update: bool) -> Result<FileLock, Error> {
let path = self.path();
let lock_path = format!("{}.lock", path.display());
if tokio::fs::metadata(Path::new(&lock_path)).await.is_err() {
// !exists
tokio::fs::File::create(&lock_path)
.await
.with_context(|e| format!("{}: {}", lock_path, e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
let lock = lock_file(lock_path.clone(), for_update)
.await
.with_context(|e| format!("{}: {}", lock_path, e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
Ok(lock)
}
pub async fn exists(&self) -> bool {
tokio::fs::metadata(self.path()).await.is_ok()
}
pub async fn maybe_read(&self, for_update: bool) -> Option<Result<PersistenceFile, Error>> {
if self.exists().await {
// exists
Some(self.read(for_update).await)
} else {
None
}
}
pub async fn read(&self, for_update: bool) -> Result<PersistenceFile, Error> {
let path = self.path();
let lock = self.lock(for_update).await?;
let file = File::open(&path)
.await
.with_context(|e| format!("{}: {}", path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
Ok(PersistenceFile::new(file, lock, None))
}
pub async fn write(&self, lock: Option<FileLock>) -> Result<PersistenceFile, Error> {
let path = self.path();
if let Some(parent) = path.parent() {
if tokio::fs::metadata(parent).await.is_err() {
// !exists
tokio::fs::create_dir_all(parent).await?;
}
}
let lock = if let Some(lock) = lock {
lock
} else {
self.lock(true).await?
};
Ok({
let path = self.tmp();
if let Some(parent) = path.parent() {
if tokio::fs::metadata(parent).await.is_err() {
// !exists
tokio::fs::create_dir_all(parent).await?;
}
}
PersistenceFile::new(File::create(path).await?, lock, Some(self.clone()))
})
}
pub async fn for_update(self) -> Result<UpdateHandle<ForRead>, Error> {
UpdateHandle::new(self).await
}
}
#[derive(Debug)]
pub struct PersistenceFile {
file: Option<File>,
lock: Option<FileLock>,
needs_commit: Option<PersistencePath>,
}
impl PersistenceFile {
pub fn new(file: File, lock: FileLock, needs_commit: Option<PersistencePath>) -> Self {
PersistenceFile {
file: Some(file),
lock: Some(lock),
needs_commit,
}
}
pub fn take_lock(&mut self) -> Option<FileLock> {
self.lock.take()
}
/// Commits the file to the persistence directory.
/// If this fails, the file was not saved.
pub async fn commit(mut self) -> Result<(), Error> {
if let Some(mut file) = self.file.take() {
file.flush().await?;
file.shutdown().await?;
drop(file);
}
if let Some(path) = self.needs_commit.take() {
tokio::fs::rename(path.tmp(), path.path())
.await
.with_context(|e| {
format!(
"{} -> {}: {}",
path.tmp().display(),
path.path().display(),
e
)
})
.with_code(crate::error::FILESYSTEM_ERROR)?;
if let Some(lock) = self.lock.take() {
unlock(lock)
.await
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
tokio::fs::remove_file(format!("{}.lock", path.path().display()))
.await
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
Ok(())
} else {
Ok(())
}
}
}
impl std::ops::Deref for PersistenceFile {
type Target = File;
fn deref(&self) -> &Self::Target {
self.file.as_ref().unwrap()
}
}
impl std::ops::DerefMut for PersistenceFile {
fn deref_mut(&mut self) -> &mut Self::Target {
self.file.as_mut().unwrap()
}
}
impl AsRef<File> for PersistenceFile {
fn as_ref(&self) -> &File {
&*self
}
}
impl AsMut<File> for PersistenceFile {
fn as_mut(&mut self) -> &mut File {
&mut *self
}
}
impl Drop for PersistenceFile {
fn drop(&mut self) {
if let Some(path) = &self.needs_commit {
log::warn!(
"{} was dropped without being committed.",
path.path().display()
);
}
}
}
pub trait UpdateHandleMode {}
pub struct ForRead;
impl UpdateHandleMode for ForRead {}
pub struct ForWrite;
impl UpdateHandleMode for ForWrite {}
pub struct UpdateHandle<Mode: UpdateHandleMode> {
path: PersistencePath,
file: PersistenceFile,
mode: PhantomData<Mode>,
}
impl UpdateHandle<ForRead> {
pub async fn new(path: PersistencePath) -> Result<Self, Error> {
if !path.path().exists() {
tokio::fs::File::create(path.path()).await?;
}
Ok(UpdateHandle {
file: path.read(true).await?,
path,
mode: PhantomData,
})
}
pub async fn into_writer(mut self) -> Result<UpdateHandle<ForWrite>, Error> {
let lock = self.file.take_lock();
Ok(UpdateHandle {
file: self.path.write(lock).await?,
path: self.path,
mode: PhantomData,
})
}
}
impl UpdateHandle<ForWrite> {
pub async fn commit(self) -> Result<(), Error> {
self.file.commit().await
}
}
impl tokio::io::AsyncRead for UpdateHandle<ForRead> {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut [u8],
) -> std::task::Poll<std::io::Result<usize>> {
unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }.poll_read(cx, buf)
}
}
impl tokio::io::AsyncWrite for UpdateHandle<ForWrite> {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
tokio::io::AsyncWrite::poll_write(
unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) },
cx,
buf,
)
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
tokio::io::AsyncWrite::poll_flush(
unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) },
cx,
)
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
tokio::io::AsyncWrite::poll_shutdown(
unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) },
cx,
)
}
}
pub struct YamlUpdateHandle<T: serde::Serialize + for<'de> serde::Deserialize<'de>> {
inner: T,
handle: UpdateHandle<ForRead>,
committed: bool,
}
impl<T> YamlUpdateHandle<T>
where
T: serde::Serialize + for<'de> serde::Deserialize<'de>,
{
pub async fn new(path: PersistencePath) -> Result<Self, Error> {
let mut handle = path.for_update().await?;
let inner = from_yaml_async_reader(&mut handle).await?;
Ok(YamlUpdateHandle {
inner,
handle,
committed: false,
})
}
pub async fn commit(mut self) -> Result<(), Error> {
let mut file = self.handle.into_writer().await?;
to_yaml_async_writer(&mut file, &self.inner)
.await
.no_code()?;
file.commit().await?;
self.committed = true;
Ok(())
}
}
impl<T> YamlUpdateHandle<T>
where
T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default,
{
pub async fn new_or_default(path: PersistencePath) -> Result<Self, Error> {
if !path.path().exists() {
Ok(YamlUpdateHandle {
inner: Default::default(),
handle: path.for_update().await?,
committed: false,
})
} else {
Self::new(path).await
}
}
}
impl<T> std::ops::Deref for YamlUpdateHandle<T>
where
T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> std::ops::DerefMut for YamlUpdateHandle<T>
where
T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
#[derive(Clone, Debug)]
pub enum Never {}
pub fn absurd<T>(lol: Never) -> T {
match lol {}
}
impl fmt::Display for Never {
fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
absurd(self.clone())
}
}
impl failure::Fail for Never {}
#[derive(Clone, Debug)]
pub struct AsyncCompat<T>(pub T);
impl<T> futures::io::AsyncRead for AsyncCompat<T>
where
T: tokio::io::AsyncRead,
{
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut [u8],
) -> std::task::Poll<std::io::Result<usize>> {
tokio::io::AsyncRead::poll_read(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx, buf)
}
}
impl<T> tokio::io::AsyncRead for AsyncCompat<T>
where
T: futures::io::AsyncRead,
{
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut [u8],
) -> std::task::Poll<std::io::Result<usize>> {
futures::io::AsyncRead::poll_read(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx, buf)
}
}
impl<T> futures::io::AsyncWrite for AsyncCompat<T>
where
T: tokio::io::AsyncWrite,
{
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
tokio::io::AsyncWrite::poll_write(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx, buf)
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
tokio::io::AsyncWrite::poll_flush(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx)
}
fn poll_close(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
tokio::io::AsyncWrite::poll_shutdown(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx)
}
}
impl<T> tokio::io::AsyncWrite for AsyncCompat<T>
where
T: futures::io::AsyncWrite,
{
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
futures::io::AsyncWrite::poll_write(
unsafe { self.map_unchecked_mut(|a| &mut a.0) },
cx,
buf,
)
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
futures::io::AsyncWrite::poll_flush(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx)
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
futures::io::AsyncWrite::poll_close(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx)
}
}
pub async fn lock_file(filename: String, for_write: bool) -> std::io::Result<FileLock> {
tokio::task::spawn_blocking(move || FileLock::lock(&filename, true, for_write)).await?
}
pub async fn unlock(lock: FileLock) -> std::io::Result<()> {
tokio::task::spawn_blocking(move || lock.unlock()).await?
}
pub async fn from_yaml_async_reader<T, R>(mut reader: R) -> Result<T, crate::Error>
where
T: for<'de> serde::Deserialize<'de>,
R: AsyncRead + Unpin,
{
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer).await?;
serde_yaml::from_slice(&buffer)
.map_err(failure::Error::from)
.with_code(crate::error::SERDE_ERROR)
}
pub async fn to_yaml_async_writer<T, W>(mut writer: W, value: &T) -> Result<(), crate::Error>
where
T: serde::Serialize,
W: AsyncWrite + Unpin,
{
let mut buffer = serde_yaml::to_vec(value).with_code(crate::error::SERDE_ERROR)?;
buffer.extend_from_slice(b"\n");
writer.write_all(&buffer).await?;
Ok(())
}
pub async fn from_cbor_async_reader<T, R>(mut reader: R) -> Result<T, crate::Error>
where
T: for<'de> serde::Deserialize<'de>,
R: AsyncRead + Unpin,
{
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer).await?;
serde_cbor::from_slice(&buffer)
.map_err(failure::Error::from)
.with_code(crate::error::SERDE_ERROR)
}
pub async fn from_json_async_reader<T, R>(mut reader: R) -> Result<T, crate::Error>
where
T: for<'de> serde::Deserialize<'de>,
R: AsyncRead + Unpin,
{
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer).await?;
serde_json::from_slice(&buffer)
.map_err(failure::Error::from)
.with_code(crate::error::SERDE_ERROR)
}
pub async fn to_json_async_writer<T, W>(mut writer: W, value: &T) -> Result<(), crate::Error>
where
T: serde::Serialize,
W: AsyncWrite + Unpin,
{
let buffer = serde_json::to_string(value).with_code(crate::error::SERDE_ERROR)?;
writer.write_all(&buffer.as_bytes()).await?;
Ok(())
}
pub async fn to_json_pretty_async_writer<T, W>(mut writer: W, value: &T) -> Result<(), crate::Error>
where
T: serde::Serialize,
W: AsyncWrite + Unpin,
{
let mut buffer = serde_json::to_string_pretty(value).with_code(crate::error::SERDE_ERROR)?;
buffer.push_str("\n");
writer.write_all(&buffer.as_bytes()).await?;
Ok(())
}
#[async_trait::async_trait]
pub trait Invoke {
async fn invoke(&mut self, name: &str) -> Result<Vec<u8>, failure::Error>;
}
#[async_trait::async_trait]
impl Invoke for tokio::process::Command {
async fn invoke(&mut self, name: &str) -> Result<Vec<u8>, failure::Error> {
let res = self.output().await?;
ensure!(
res.status.success(),
"{} Error: {}",
name,
std::str::from_utf8(&res.stderr).unwrap_or("Unknown Error")
);
Ok(res.stdout)
}
}
pub trait Apply: Sized {
fn apply<O, F: FnOnce(Self) -> O>(self, func: F) -> O {
func(self)
}
}
pub trait ApplyRef {
fn apply_ref<O, F: FnOnce(&Self) -> O>(&self, func: F) -> O {
func(&self)
}
fn apply_mut<O, F: FnOnce(&mut Self) -> O>(&mut self, func: F) -> O {
func(self)
}
}
impl<T> Apply for T {}
impl<T> ApplyRef for T {}

251
appmgr/src/version/mod.rs Normal file
View File

@@ -0,0 +1,251 @@
use std::cmp::Ordering;
use async_trait::async_trait;
use failure::ResultExt as _;
use futures::stream::TryStreamExt;
use crate::util::{to_yaml_async_writer, AsyncCompat, PersistencePath};
use crate::Error;
use crate::ResultExt as _;
mod v0_1_0;
mod v0_1_1;
mod v0_1_2;
mod v0_1_3;
mod v0_1_4;
mod v0_1_5;
mod v0_2_0;
mod v0_2_1;
mod v0_2_2;
mod v0_2_3;
mod v0_2_4;
mod v0_2_5;
pub use v0_2_5::Version as Current;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
enum Version {
V0_0_0(Wrapper<()>),
V0_1_0(Wrapper<v0_1_0::Version>),
V0_1_1(Wrapper<v0_1_1::Version>),
V0_1_2(Wrapper<v0_1_2::Version>),
V0_1_3(Wrapper<v0_1_3::Version>),
V0_1_4(Wrapper<v0_1_4::Version>),
V0_1_5(Wrapper<v0_1_5::Version>),
V0_2_0(Wrapper<v0_2_0::Version>),
V0_2_1(Wrapper<v0_2_1::Version>),
V0_2_2(Wrapper<v0_2_2::Version>),
V0_2_3(Wrapper<v0_2_3::Version>),
V0_2_4(Wrapper<v0_2_4::Version>),
V0_2_5(Wrapper<v0_2_5::Version>),
Other(emver::Version),
}
#[async_trait]
pub trait VersionT
where
Self: Sized + Send + Sync,
{
type Previous: VersionT;
fn new() -> Self;
fn semver(&self) -> &'static emver::Version;
async fn up(&self) -> Result<(), Error>;
async fn down(&self) -> Result<(), Error>;
async fn commit(&self) -> Result<(), Error> {
let mut out = PersistencePath::from_ref("version").write(None).await?;
to_yaml_async_writer(out.as_mut(), &self.semver()).await?;
out.commit().await?;
Ok(())
}
async fn migrate_to<V: VersionT>(&self, version: &V) -> Result<(), Error> {
match self.semver().cmp(version.semver()) {
Ordering::Greater => self.rollback_to_unchecked(version).await,
Ordering::Less => version.migrate_from_unchecked(self).await,
Ordering::Equal => Ok(()),
}
}
async fn migrate_from_unchecked<V: VersionT>(&self, version: &V) -> Result<(), Error> {
let previous = Self::Previous::new();
if version.semver() != previous.semver() {
previous.migrate_from_unchecked(version).await?;
}
log::info!("{} -> {}", previous.semver(), self.semver());
self.up().await?;
self.commit().await?;
Ok(())
}
async fn rollback_to_unchecked<V: VersionT>(&self, version: &V) -> Result<(), Error> {
let previous = Self::Previous::new();
log::info!("{} -> {}", self.semver(), previous.semver());
self.down().await?;
previous.commit().await?;
if version.semver() != previous.semver() {
previous.rollback_to_unchecked(version).await?;
}
Ok(())
}
}
struct Wrapper<T>(T);
impl<T> serde::Serialize for Wrapper<T>
where
T: VersionT,
{
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.semver().serialize(serializer)
}
}
impl<'de, T> serde::Deserialize<'de> for Wrapper<T>
where
T: VersionT,
{
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = emver::Version::deserialize(deserializer)?;
let version = T::new();
if &v == version.semver() {
Ok(Wrapper(version))
} else {
Err(serde::de::Error::custom("Mismatched Version"))
}
}
}
const V0_0_0: emver::Version = emver::Version::new(0, 0, 0, 0);
#[async_trait]
impl VersionT for () {
type Previous = ();
fn new() -> Self {
()
}
fn semver(&self) -> &'static emver::Version {
&V0_0_0
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}
pub async fn init() -> Result<(), failure::Error> {
let _lock = PersistencePath::from_ref("").lock(true).await?;
let vpath = PersistencePath::from_ref("version");
if let Some(mut f) = vpath.maybe_read(false).await.transpose()? {
let v: Version = crate::util::from_yaml_async_reader(&mut *f).await?;
match v {
Version::V0_0_0(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_1_0(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_1_1(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_1_2(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_1_3(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_1_4(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_1_5(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_0(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_1(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_2(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_3(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_4(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_5(v) => v.0.migrate_to(&Current::new()).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
}
} else {
().migrate_to(&Current::new()).await?;
}
Ok(())
}
pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error> {
let req_str: String = format!("{}", requirement)
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let url = format!("{}/appmgr?spec={}", &*crate::SYS_REGISTRY_URL, req_str);
log::info!("Fetching new version from {}", url);
let response = reqwest::get(&url)
.await
.with_code(crate::error::NETWORK_ERROR)?
.error_for_status()
.with_code(crate::error::REGISTRY_ERROR)?;
let tmp_appmgr_path = PersistencePath::from_ref("appmgr").tmp();
if let Some(parent) = tmp_appmgr_path.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent)
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
}
let mut f = tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.open(&tmp_appmgr_path)
.await
.with_context(|e| format!("{}: {}", tmp_appmgr_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
tokio::io::copy(
&mut AsyncCompat(
response
.bytes_stream()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
.into_async_read(),
),
&mut f,
)
.await
.no_code()?;
drop(f);
crate::ensure_code!(
tokio::process::Command::new("chmod")
.arg("700")
.arg(&tmp_appmgr_path)
.output()
.await?
.status
.success(),
crate::error::FILESYSTEM_ERROR,
"chmod failed"
);
let out = std::process::Command::new(&tmp_appmgr_path)
.arg("semver")
.stdout(std::process::Stdio::piped())
.spawn()?
.wait_with_output()
.with_context(|e| format!("{} semver: {}", tmp_appmgr_path.display(), e))
.no_code()?;
let out_str = std::str::from_utf8(&out.stdout).no_code()?;
log::info!("Migrating to version {}", out_str);
let v: Version = serde_yaml::from_str(out_str)
.with_context(|e| format!("{}: {:?}", e, out_str))
.with_code(crate::error::SERDE_ERROR)?;
match v {
Version::V0_0_0(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_1_0(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_1_1(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_1_2(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_1_3(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_1_4(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_1_5(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_0(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_1(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_2(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_3(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_4(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_5(v) => Current::new().migrate_to(&v.0).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
};
let cur_path = std::path::Path::new("/usr/local/bin/appmgr");
tokio::fs::rename(&tmp_appmgr_path, &cur_path)
.await
.with_context(|e| {
format!(
"{} -> {}: {}",
tmp_appmgr_path.display(),
cur_path.display(),
e
)
})
.with_code(crate::error::FILESYSTEM_ERROR)?;
Ok(())
}

View File

@@ -0,0 +1,278 @@
use std::path::Path;
use super::*;
const V0_1_0: emver::Version = emver::Version::new(0, 1, 0, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = ();
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_1_0
}
async fn up(&self) -> Result<(), Error> {
tokio::fs::create_dir_all(Path::new(crate::PERSISTENCE_DIR).join("tor")).await?;
tokio::fs::create_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps")).await?;
tokio::fs::create_dir_all(Path::new(crate::TMP_DIR).join("tor")).await?;
tokio::fs::create_dir_all(Path::new(crate::TMP_DIR).join("apps")).await?;
let mut outfile = legacy::util::PersistencePath::from_ref("tor/torrc")
.write()
.await?;
tokio::io::copy(
&mut AsyncCompat(
reqwest::get(&format!("{}/torrc?spec==0.0.0", &*crate::SYS_REGISTRY_URL))
.await
.with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e))
.with_code(crate::error::NETWORK_ERROR)?
.error_for_status()
.with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e))
.with_code(crate::error::REGISTRY_ERROR)?
.bytes_stream()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
.into_async_read(),
),
outfile.as_mut(),
)
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
outfile.commit().await?;
legacy::tor::set_svc(
"start9-agent",
legacy::tor::Service {
ports: vec![5959],
hidden_service_version: Default::default(),
},
)
.await
.no_code()?;
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}
mod legacy {
pub mod tor {
use failure::{Error, ResultExt};
use linear_map::LinearMap;
use tokio::io::AsyncWriteExt;
use crate::tor::HiddenServiceVersion;
use super::util::PersistencePath;
pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc";
pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor";
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Service {
pub ports: Vec<u16>,
pub hidden_service_version: HiddenServiceVersion,
}
async fn services_map(path: &PersistencePath) -> Result<LinearMap<String, Service>, Error> {
use crate::util::Apply;
Ok(path
.maybe_read()
.await
.transpose()?
.map(crate::util::from_yaml_async_reader)
.apply(futures::future::OptionFuture::from)
.await
.transpose()?
.unwrap_or_else(LinearMap::new))
}
pub async fn write_services(
hidden_services: &LinearMap<String, Service>,
) -> Result<(), Error> {
tokio::fs::copy(crate::TOR_RC, ETC_TOR_RC)
.await
.with_context(|e| format!("{} -> {}: {}", crate::TOR_RC, ETC_TOR_RC, e))?;
let mut f = tokio::fs::OpenOptions::new()
.append(true)
.open(ETC_TOR_RC)
.await?;
f.write("\n".as_bytes()).await?;
for (name, service) in hidden_services {
f.write("\n".as_bytes()).await?;
f.write(format!("# HIDDEN SERVICE FOR {}\n", name).as_bytes())
.await?;
f.write(
format!(
"HiddenServiceDir {}/app-{}/\n",
HIDDEN_SERVICE_DIR_ROOT, name
)
.as_bytes(),
)
.await?;
f.write(format!("{}\n", service.hidden_service_version).as_bytes())
.await?;
for port in &service.ports {
f.write(format!("HiddenServicePort {} 127.0.0.1:{}\n", port, port).as_bytes())
.await?;
}
f.write("\n".as_bytes()).await?;
}
Ok(())
}
pub async fn set_svc(name: &str, service: Service) -> Result<(), Error> {
log::info!(
"Adding Tor hidden service {} to {}.",
name,
crate::SERVICES_YAML
);
let path = PersistencePath::from_ref(crate::SERVICES_YAML);
let mut hidden_services = services_map(&path).await?;
hidden_services.insert(name.to_owned(), service);
let mut services_yaml = path.write().await?;
crate::util::to_yaml_async_writer(services_yaml.as_mut(), &hidden_services).await?;
services_yaml.write_all("\n".as_bytes()).await?;
services_yaml.commit().await?;
log::info!("Adding Tor hidden service {} to {}.", name, ETC_TOR_RC);
write_services(&hidden_services).await?;
log::info!("Restarting Tor.");
let svc_exit = std::process::Command::new("service")
.args(&["tor", "restart"])
.status()?;
ensure!(
svc_exit.success(),
"Failed to Restart Tor: {}",
svc_exit.code().unwrap_or(0)
);
Ok(())
}
}
pub mod util {
use std::path::{Path, PathBuf};
use tokio::fs::File;
use crate::Error;
use crate::ResultExt as _;
use failure::ResultExt as _;
#[derive(Clone, Debug)]
pub struct PersistencePath(PathBuf);
impl PersistencePath {
pub fn from_ref<P: AsRef<Path>>(p: P) -> Self {
let path = p.as_ref();
PersistencePath(if path.has_root() {
path.strip_prefix("/").unwrap().to_owned()
} else {
path.to_owned()
})
}
pub fn tmp(&self) -> PathBuf {
Path::new(crate::TMP_DIR).join(&self.0)
}
pub fn path(&self) -> PathBuf {
Path::new(crate::PERSISTENCE_DIR).join(&self.0)
}
pub async fn maybe_read(&self) -> Option<Result<File, Error>> {
let path = self.path();
if path.exists() {
Some(
File::open(&path)
.await
.with_context(|e| format!("{}: {}", path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR),
)
} else {
None
}
}
pub async fn write(&self) -> Result<PersistenceFile, Error> {
let path = self.path();
if let Some(parent) = path.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent).await?;
}
}
Ok(if path.exists() {
let path = self.tmp();
if let Some(parent) = path.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent).await?;
}
}
PersistenceFile::new(File::create(path).await?, Some(self.clone()))
} else {
PersistenceFile::new(File::create(path).await?, None)
})
}
}
#[derive(Debug)]
pub struct PersistenceFile {
file: File,
needs_commit: Option<PersistencePath>,
}
impl PersistenceFile {
pub fn new(file: File, needs_commit: Option<PersistencePath>) -> Self {
PersistenceFile { file, needs_commit }
}
/// Commits the file to the persistence directory.
/// If this fails, the file was not saved.
pub async fn commit(mut self) -> Result<(), Error> {
if let Some(path) = self.needs_commit.take() {
tokio::fs::rename(path.tmp(), path.path())
.await
.with_context(|e| {
format!(
"{} -> {}: {}",
path.tmp().display(),
path.path().display(),
e
)
})
.with_code(crate::error::FILESYSTEM_ERROR)
} else {
Ok(())
}
}
}
impl std::ops::Deref for PersistenceFile {
type Target = File;
fn deref(&self) -> &Self::Target {
&self.file
}
}
impl std::ops::DerefMut for PersistenceFile {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.file
}
}
impl AsRef<File> for PersistenceFile {
fn as_ref(&self) -> &File {
&*self
}
}
impl AsMut<File> for PersistenceFile {
fn as_mut(&mut self) -> &mut File {
&mut *self
}
}
impl Drop for PersistenceFile {
fn drop(&mut self) {
if let Some(path) = &self.needs_commit {
log::warn!(
"{} was dropped without being committed.",
path.path().display()
);
}
}
}
}
}

View File

@@ -0,0 +1,202 @@
use std::path::Path;
use super::*;
const V0_1_1: emver::Version = emver::Version::new(0, 1, 1, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_1_0::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_1_1
}
async fn up(&self) -> Result<(), Error> {
log::info!("Update torrc");
let mut outfile = crate::util::PersistencePath::from_ref("tor/torrc")
.write(None)
.await?;
tokio::io::copy(
&mut AsyncCompat(
reqwest::get(&format!("{}/torrc?spec==0.1.1", &*crate::SYS_REGISTRY_URL))
.await
.with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e))
.with_code(crate::error::NETWORK_ERROR)?
.error_for_status()
.with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e))
.with_code(crate::error::REGISTRY_ERROR)?
.bytes_stream()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
.into_async_read(),
),
outfile.as_mut(),
)
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
outfile.commit().await?;
if !std::process::Command::new("docker")
.arg("network")
.arg("create")
.arg("-d")
.arg("bridge")
.arg("--subnet=172.18.0.0/16")
.arg("start9")
.stdout(std::process::Stdio::null())
.status()?
.success()
{
log::warn!("Failed to Create Network")
}
match tokio::fs::remove_file(Path::new(crate::PERSISTENCE_DIR).join(crate::SERVICES_YAML))
.await
{
Ok(_) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
.with_context(|e| format!("{}/{}: {}", crate::PERSISTENCE_DIR, crate::SERVICES_YAML, e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
crate::tor::reload().await?;
for app in crate::apps::list_info().await? {
legacy::update::update(&app.0).await?;
}
Ok(())
}
async fn down(&self) -> Result<(), Error> {
let mut outfile = crate::util::PersistencePath::from_ref("tor/torrc")
.write(None)
.await?;
tokio::io::copy(
&mut AsyncCompat(
reqwest::get(&format!("{}/torrc?spec==0.1.0", &*crate::SYS_REGISTRY_URL))
.await
.with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e))
.no_code()?
.error_for_status()
.with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e))
.no_code()?
.bytes_stream()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
.into_async_read(),
),
outfile.as_mut(),
)
.await
.with_code(crate::error::FILESYSTEM_ERROR)?;
outfile.commit().await?;
for app in crate::apps::list_info().await? {
legacy::remove::remove(&app.0, false).await?;
}
let tor_svcs = crate::util::PersistencePath::from_ref(crate::SERVICES_YAML).path();
if tor_svcs.exists() {
tokio::fs::remove_file(&tor_svcs)
.await
.with_context(|e| format!("{}: {}", tor_svcs.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
if !std::process::Command::new("docker")
.arg("network")
.arg("rm")
.arg("start9")
.stdout(std::process::Stdio::null())
.status()?
.success()
{
log::warn!("Failed to Remove Network");
}
Ok(())
}
}
mod legacy {
pub mod remove {
use std::path::Path;
use crate::Error;
pub async fn remove(name: &str, purge: bool) -> Result<(), Error> {
log::info!("Removing app from manifest.");
crate::apps::remove(name).await?;
log::info!("Stopping docker container.");
if !tokio::process::Command::new("docker")
.args(&["stop", name])
.stdout(std::process::Stdio::null())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.status()
.await?
.success()
{
log::error!("Failed to Stop Docker Container");
};
log::info!("Removing docker container.");
if !tokio::process::Command::new("docker")
.args(&["rm", name])
.stdout(std::process::Stdio::null())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.status()
.await?
.success()
{
log::error!("Failed to Remove Docker Container");
};
if purge {
log::info!("Removing tor hidden service.");
crate::tor::rm_svc(name).await?;
log::info!("Removing app metadata.");
std::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name))?;
log::info!("Destroying mounted volume.");
std::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name))?;
log::info!("Pruning unused docker images.");
crate::ensure_code!(
std::process::Command::new("docker")
.args(&["image", "prune", "-a", "-f"])
.stdout(std::process::Stdio::null())
.stderr(match log::max_level() {
log::LevelFilter::Error => std::process::Stdio::null(),
_ => std::process::Stdio::inherit(),
})
.status()?
.success(),
3,
"Failed to Prune Docker Images"
);
};
Ok(())
}
}
pub mod update {
use crate::Error;
pub async fn update(name_version: &str) -> Result<(), Error> {
let name = name_version
.split("@")
.next()
.ok_or_else(|| failure::format_err!("invalid app id"))?;
crate::install::download_name(name_version).await?;
super::remove::remove(name, false).await?;
crate::install::install_name(name_version, true).await?;
let config = crate::apps::config(name).await?;
if let Some(cfg) = config.config {
if config.spec.matches(&cfg).is_ok() {
crate::apps::set_configured(name, true).await?;
}
}
Ok(())
}
}
}

View File

@@ -0,0 +1,104 @@
use futures::StreamExt;
use futures::TryStreamExt;
use linear_map::LinearMap;
use super::*;
const V0_1_2: emver::Version = emver::Version::new(0, 1, 2, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_1_1::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_1_2
}
async fn up(&self) -> Result<(), Error> {
let app_info = legacy::apps::list_info().await?;
for (name, _) in &app_info {
let p = PersistencePath::from_ref("apps")
.join(name)
.join("manifest.yaml");
let mut f = p.for_update().await?;
let manifest: crate::manifest::ManifestV0 = crate::util::from_yaml_async_reader(&mut f)
.await
.no_code()?;
let mut f = f.into_writer().await?;
crate::util::to_yaml_async_writer(&mut f, &crate::manifest::Manifest::V0(manifest))
.await
.no_code()?;
f.commit().await?;
}
let p = PersistencePath::from_ref("apps.yaml");
let exists = p.path().exists();
let mut f = p.for_update().await?;
let info: LinearMap<String, legacy::apps::AppInfo> = if exists {
crate::util::from_yaml_async_reader(&mut f)
.await
.no_code()?
} else {
LinearMap::new()
};
let new_info: LinearMap<String, crate::apps::AppInfo> = futures::stream::iter(info)
.then(|(name, i)| async move {
let title = crate::apps::manifest(&name).await?.title;
Ok::<_, Error>((
name,
crate::apps::AppInfo {
title,
version: i.version,
tor_address: i.tor_address,
configured: i.configured,
recoverable: false,
needs_restart: false,
},
))
})
.try_collect()
.await?;
let mut f = f.into_writer().await?;
crate::util::to_yaml_async_writer(&mut f, &new_info)
.await
.no_code()?;
f.commit().await?;
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}
mod legacy {
pub mod apps {
use linear_map::LinearMap;
use crate::util::from_yaml_async_reader;
use crate::util::Apply;
use crate::util::PersistencePath;
use crate::Error;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct AppInfo {
pub version: emver::Version,
pub tor_address: Option<String>,
pub configured: bool,
}
pub async fn list_info() -> Result<LinearMap<String, AppInfo>, Error> {
let apps_path = PersistencePath::from_ref("apps.yaml");
Ok(apps_path
.maybe_read(false)
.await
.transpose()?
.map(|mut f| async move { from_yaml_async_reader(&mut *f).await })
.apply(futures::future::OptionFuture::from)
.await
.transpose()?
.unwrap_or_else(LinearMap::new))
}
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_1_3: emver::Version = emver::Version::new(0, 1, 3, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_1_2::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_1_3
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_1_4: emver::Version = emver::Version::new(0, 1, 4, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_1_3::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_1_4
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_1_5: emver::Version = emver::Version::new(0, 1, 5, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_1_4::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_1_5
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,98 @@
use linear_map::LinearMap;
use super::*;
use crate::util::{to_yaml_async_writer, PersistencePath};
const V0_2_0: emver::Version = emver::Version::new(0, 2, 0, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_1_5::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_0
}
async fn up(&self) -> Result<(), Error> {
let app_info: LinearMap<String, crate::apps::AppInfo> = legacy::apps::list_info()
.await?
.into_iter()
.map(|(id, ai)| {
(
id,
crate::apps::AppInfo {
title: ai.title,
version: ai.version,
tor_address: ai.tor_address,
configured: ai.configured,
recoverable: ai.recoverable,
needs_restart: false,
},
)
})
.collect();
let mut apps_file = PersistencePath::from_ref("apps.yaml").write(None).await?;
to_yaml_async_writer(&mut *apps_file, &app_info).await?;
apps_file.commit().await?;
Ok(())
}
async fn down(&self) -> Result<(), Error> {
let app_info: LinearMap<String, legacy::apps::AppInfo> = crate::apps::list_info()
.await?
.into_iter()
.map(|(id, ai)| {
(
id,
legacy::apps::AppInfo {
title: ai.title,
version: ai.version,
tor_address: ai.tor_address,
configured: ai.configured,
recoverable: ai.recoverable,
},
)
})
.collect();
let mut apps_file = PersistencePath::from_ref("apps.yaml").write(None).await?;
to_yaml_async_writer(&mut *apps_file, &app_info).await?;
apps_file.commit().await?;
Ok(())
}
}
mod legacy {
pub mod apps {
use linear_map::LinearMap;
use crate::util::{from_yaml_async_reader, PersistencePath};
use crate::Error;
fn not(b: &bool) -> bool {
!b
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct AppInfo {
pub title: String,
pub version: emver::Version,
pub tor_address: Option<String>,
pub configured: bool,
#[serde(default)]
#[serde(skip_serializing_if = "not")]
pub recoverable: bool,
}
pub async fn list_info() -> Result<LinearMap<String, AppInfo>, Error> {
let apps_path = PersistencePath::from_ref("apps.yaml");
let mut f = match apps_path.maybe_read(false).await.transpose()? {
Some(a) => a,
None => return Ok(LinearMap::new()),
};
from_yaml_async_reader(&mut *f).await
}
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_1: emver::Version = emver::Version::new(0, 2, 1, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_0::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_1
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_2: emver::Version = emver::Version::new(0, 2, 2, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_1::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_2
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_3: emver::Version = emver::Version::new(0, 2, 3, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_2::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_3
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_4: emver::Version = emver::Version::new(0, 2, 4, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_3::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_4
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_5: emver::Version = emver::Version::new(0, 2, 5, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_4::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_5
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}