mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 12:33:40 +00:00
0.2.5 initial commit
Makefile incomplete
This commit is contained in:
440
appmgr/src/apps.rs
Normal file
440
appmgr/src/apps.rs
Normal 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
238
appmgr/src/backup.rs
Normal 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
324
appmgr/src/config/mod.rs
Normal 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(())
|
||||
}
|
||||
76
appmgr/src/config/rule_parser.pest
Normal file
76
appmgr/src/config/rule_parser.pest
Normal 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
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
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
367
appmgr/src/config/util.rs
Normal 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(®ex.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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
66
appmgr/src/config/value.rs
Normal file
66
appmgr/src/config/value.rs
Normal 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
194
appmgr/src/control.rs
Normal 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
276
appmgr/src/dependencies.rs
Normal 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
230
appmgr/src/disks.rs
Normal 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
107
appmgr/src/error.rs
Normal 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
130
appmgr/src/index.rs
Normal 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
195
appmgr/src/inspect.rs
Normal 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
564
appmgr/src/install.rs
Normal 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
51
appmgr/src/lib.rs
Normal 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
199
appmgr/src/logs.rs
Normal 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
1645
appmgr/src/main.rs
Normal file
File diff suppressed because it is too large
Load Diff
76
appmgr/src/manifest.rs
Normal file
76
appmgr/src/manifest.rs
Normal 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
398
appmgr/src/pack.rs
Normal 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
66
appmgr/src/registry.rs
Normal 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
117
appmgr/src/remove.rs
Normal 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
414
appmgr/src/tor.rs
Normal 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
80
appmgr/src/update.rs
Normal 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
553
appmgr/src/util.rs
Normal 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
251
appmgr/src/version/mod.rs
Normal 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(())
|
||||
}
|
||||
278
appmgr/src/version/v0_1_0.rs
Normal file
278
appmgr/src/version/v0_1_0.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
202
appmgr/src/version/v0_1_1.rs
Normal file
202
appmgr/src/version/v0_1_1.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
104
appmgr/src/version/v0_1_2.rs
Normal file
104
appmgr/src/version/v0_1_2.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
21
appmgr/src/version/v0_1_3.rs
Normal file
21
appmgr/src/version/v0_1_3.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
21
appmgr/src/version/v0_1_4.rs
Normal file
21
appmgr/src/version/v0_1_4.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
21
appmgr/src/version/v0_1_5.rs
Normal file
21
appmgr/src/version/v0_1_5.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
98
appmgr/src/version/v0_2_0.rs
Normal file
98
appmgr/src/version/v0_2_0.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
21
appmgr/src/version/v0_2_1.rs
Normal file
21
appmgr/src/version/v0_2_1.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
21
appmgr/src/version/v0_2_2.rs
Normal file
21
appmgr/src/version/v0_2_2.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
21
appmgr/src/version/v0_2_3.rs
Normal file
21
appmgr/src/version/v0_2_3.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
21
appmgr/src/version/v0_2_4.rs
Normal file
21
appmgr/src/version/v0_2_4.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
21
appmgr/src/version/v0_2_5.rs
Normal file
21
appmgr/src/version/v0_2_5.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user