appmgr 0.3.0 rewrite pt 1

appmgr: split bins

update cargo.toml and .gitignore

context

appmgr: refactor error module

appmgr: context

begin new s9pk format

appmgr: add fields to manifest

appmgr: start action abstraction

appmgr: volume abstraction

appmgr: improved volumes

appmgr: install wip

appmgr: health daemon

appmgr: health checks

appmgr: wip

config get

appmgr: secret store

wip

appmgr: config rewritten

appmgr: delete non-reusable code

appmgr: wip

appmgr: please the borrow-checker

appmgr: technically runs now

appmgr: cli

appmgr: clean up cli

appmgr: rpc-toolkit in action

appmgr: wrap up config

appmgr: account for updates during install

appmgr: fix: #308

appmgr: impl Display for Version

appmgr: cleanup

appmgr: set dependents on install

appmgr: dependency health checks
This commit is contained in:
Aiden McClelland
2021-04-08 11:16:25 -06:00
parent fd354b6cbe
commit ad12bf395c
84 changed files with 7510 additions and 9942 deletions

View File

@@ -0,0 +1,81 @@
use anyhow::anyhow;
use indexmap::{IndexMap, IndexSet};
use nix::sys::signal::Signal;
use patch_db::HasModel;
use serde::{Deserialize, Serialize};
use super::{Config, ConfigSpec};
use crate::action::ActionImplementation;
use crate::dependencies::Dependencies;
use crate::net::host::Hosts;
use crate::s9pk::manifest::PackageId;
use crate::status::health_check::HealthCheckId;
use crate::util::Version;
use crate::volume::Volumes;
use crate::Error;
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
pub struct ConfigRes {
pub config: Option<Config>,
pub spec: ConfigSpec,
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel)]
pub struct ConfigActions {
pub get: ActionImplementation,
pub set: ActionImplementation,
}
impl ConfigActions {
pub async fn get(
&self,
pkg_id: &PackageId,
pkg_version: &Version,
volumes: &Volumes,
hosts: &Hosts,
) -> Result<ConfigRes, Error> {
self.get
.execute(pkg_id, pkg_version, volumes, hosts, None::<()>, false)
.await
.and_then(|res| {
res.map_err(|e| Error::new(anyhow!("{}", e.1), crate::ErrorKind::ConfigGen))
})
}
pub async fn set(
&self,
pkg_id: &PackageId,
pkg_version: &Version,
dependencies: &Dependencies,
volumes: &Volumes,
hosts: &Hosts,
input: &Config,
) -> Result<SetResult, Error> {
let res: SetResult = self
.set
.execute(pkg_id, pkg_version, volumes, hosts, Some(input), false)
.await
.and_then(|res| {
res.map_err(|e| {
Error::new(anyhow!("{}", e.1), crate::ErrorKind::ConfigRulesViolation)
})
})?;
Ok(SetResult {
signal: res.signal,
depends_on: res
.depends_on
.into_iter()
.filter(|(pkg, _)| dependencies.0.contains_key(pkg))
.collect(),
})
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct SetResult {
#[serde(deserialize_with = "crate::util::deserialize_from_str_opt")]
#[serde(serialize_with = "crate::util::serialize_display_opt")]
pub signal: Option<Signal>,
pub depends_on: IndexMap<PackageId, IndexSet<HealthCheckId>>,
}

View File

@@ -1,56 +1,81 @@
use std::borrow::Cow;
use std::path::Path;
use std::time::Duration;
use failure::ResultExt as _;
use anyhow::anyhow;
use bollard::container::KillContainerOptions;
use bollard::Docker;
use futures::future::{BoxFuture, FutureExt};
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use linear_map::{set::LinearSet, LinearMap};
use patch_db::DbHandle;
use rand::SeedableRng;
use regex::Regex;
use rpc_toolkit::command;
use serde_json::Value;
use crate::dependencies::{DependencyError, TaggedDependencyError};
use crate::util::PersistencePath;
use crate::util::{from_yaml_async_reader, to_yaml_async_writer};
use crate::ResultExt as _;
use crate::action::docker::DockerAction;
use crate::config::spec::PackagePointerSpecVariant;
use crate::context::{EitherContext, ExtendedContext};
use crate::db::model::{CurrentDependencyInfo, InstalledPackageDataEntryModel};
use crate::db::util::WithRevision;
use crate::dependencies::{BreakageRes, DependencyError, TaggedDependencyError};
use crate::net::host::Hosts;
use crate::s9pk::manifest::PackageId;
use crate::util::{
display_none, display_serializable, parse_duration, parse_stdin_deserializable, IoFormat,
};
use crate::{Error, ResultExt as _};
pub mod rules;
pub mod action;
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)]
use self::action::ConfigRes;
use self::spec::{PackagePointerSpec, ValueSpecPointer};
pub type Config = serde_json::Map<String, Value>;
pub trait TypeOf {
fn type_of(&self) -> &'static str;
}
impl TypeOf for Value {
fn type_of(&self) -> &'static str {
match self {
Value::Array(_) => "list",
Value::Bool(_) => "boolean",
Value::Null => "null",
Value::Number(_) => "number",
Value::Object(_) => "object",
Value::String(_) => "string",
}
}
}
#[derive(Debug, thiserror::Error)]
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),
#[error("Timeout Error")]
TimeoutError(#[from] TimeoutError),
#[error("No Match: {0}")]
NoMatch(#[from] NoMatchWithPath),
#[error("System Error: {0}")]
SystemError(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)
impl From<ConfigurationError> for Error {
fn from(err: ConfigurationError) -> Self {
let kind = match &err {
ConfigurationError::SystemError(e) => e.kind,
_ => crate::ErrorKind::ConfigGen,
};
crate::Error::new(err, kind)
}
}
#[derive(Clone, Copy, Debug, Fail)]
#[fail(display = "Timeout Error")]
#[derive(Clone, Copy, Debug, thiserror::Error)]
#[error("Timeout Error")]
pub struct TimeoutError;
#[derive(Clone, Debug, Fail)]
#[derive(Clone, Debug, thiserror::Error)]
pub struct NoMatchWithPath {
pub path: Vec<String>,
pub error: MatchError,
@@ -72,256 +97,504 @@ impl std::fmt::Display for NoMatchWithPath {
write!(f, "{}: {}", self.path.iter().rev().join("."), self.error)
}
}
impl From<NoMatchWithPath> for Error {
fn from(e: NoMatchWithPath) -> Self {
ConfigurationError::from(e).into()
}
}
#[derive(Clone, Debug, Fail)]
#[derive(Clone, Debug, thiserror::Error)]
pub enum MatchError {
#[fail(display = "String {:?} Does Not Match Pattern {}", _0, _1)]
#[error("String {0:?} Does Not Match Pattern {1}")]
Pattern(String, Regex),
#[fail(display = "String {:?} Is Not In Enum {:?}", _0, _1)]
Enum(String, LinearSet<String>),
#[fail(display = "Field Is Not Nullable")]
#[error("String {0:?} Is Not In Enum {1:?}")]
Enum(String, IndexSet<String>),
#[error("Field Is Not Nullable")]
NotNullable,
#[fail(display = "Length Mismatch: expected {}, actual: {}", _0, _1)]
#[error("Length Mismatch: expected {0}, actual: {1}")]
LengthMismatch(NumRange<usize>, usize),
#[fail(display = "Invalid Type: expected {}, actual: {}", _0, _1)]
#[error("Invalid Type: expected {0}, actual: {1}")]
InvalidType(&'static str, &'static str),
#[fail(display = "Number Out Of Range: expected {}, actual: {}", _0, _1)]
#[error("Number Out Of Range: expected {0}, actual: {1}")]
OutOfRange(NumRange<f64>, f64),
#[fail(display = "Number Is Not Integral: {}", _0)]
#[error("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)]
#[error("Variant {0:?} Is Not In Union {1:?}")]
Union(String, IndexSet<String>),
#[error("Variant Is Missing Tag {0:?}")]
MissingTag(String),
#[fail(
display = "Property {:?} Of Variant {:?} Conflicts With Union Tag",
_0, _1
)]
#[error("Property {0:?} Of Variant {1:?} Conflicts With Union Tag")]
PropertyMatchesUnionTag(String, String),
#[fail(display = "Name of Property {:?} Conflicts With Map Tag Name", _0)]
#[error("Name of Property {0:?} Conflicts With Map Tag Name")]
PropertyNameMatchesMapTag(String),
#[fail(display = "Pointer Is Invalid: {}", _0)]
#[error("Pointer Is Invalid: {0}")]
InvalidPointer(spec::ValueSpecPointer),
#[fail(display = "Object Key Is Invalid: {}", _0)]
#[error("Object Key Is Invalid: {0}")]
InvalidKey(String),
#[fail(display = "Value In List Is Not Unique")]
#[error("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>,
#[command(subcommands(get, set))]
pub fn config(
#[context] ctx: EitherContext,
#[arg] id: PackageId,
) -> Result<ExtendedContext<EitherContext, PackageId>, Error> {
Ok(ExtendedContext::from(ctx).map(|_| id))
}
// 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,
)
#[command(display(display_serializable))]
pub async fn get(
#[context] ctx: ExtendedContext<EitherContext, PackageId>,
#[allow(unused_variables)]
#[arg(long = "format")]
format: Option<IoFormat>,
) -> Result<ConfigRes, Error> {
let mut db = ctx.base().as_rpc().unwrap().db.handle();
let pkg_model = crate::db::DatabaseModel::new()
.package_data()
.idx_model(ctx.extension())
.and_then(|m| m.installed())
.expect(&mut db)
.await
.with_kind(crate::ErrorKind::NotFound)?;
let action = pkg_model
.clone()
.manifest()
.config()
.get(&mut db)
.await?
.to_owned()
.ok_or_else(|| {
Error::new(
anyhow!("{} has no config", ctx.extension()),
crate::ErrorKind::NotFound,
)
})?;
let version = pkg_model.clone().manifest().version().get(&mut db).await?;
let volumes = pkg_model.manifest().volumes().get(&mut db).await?;
let hosts = crate::db::DatabaseModel::new()
.network()
.hosts()
.get(&mut db)
.await?;
if crate::apps::status(&dependent, false).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?)
action
.get(ctx.extension(), &*version, &*volumes, &*hosts)
.await
}
#[command(subcommands(self(set_impl(async)), set_dry), display(display_none))]
pub fn set(
#[context] ctx: ExtendedContext<EitherContext, PackageId>,
#[allow(unused_variables)]
#[arg(long = "format")]
format: Option<IoFormat>,
#[arg(long = "timeout", parse(parse_duration))] timeout: Option<Duration>,
#[arg(stdin, parse(parse_stdin_deserializable))] config: Option<Config>,
#[arg(rename = "expire-id", long = "expire-id")] expire_id: Option<String>,
) -> Result<
ExtendedContext<EitherContext, (PackageId, Option<Config>, Option<Duration>, Option<String>)>,
Error,
> {
Ok(ctx.map(|id| (id, config, timeout, expire_id)))
}
#[command(display(display_serializable))]
pub async fn set_dry(
#[context] ctx: ExtendedContext<
EitherContext,
(PackageId, Option<Config>, Option<Duration>, Option<String>),
>,
) -> Result<BreakageRes, Error> {
let (ctx, (id, config, timeout, _)) = ctx.split();
let rpc_ctx = ctx.as_rpc().unwrap();
let mut db = rpc_ctx.db.handle();
let hosts = crate::db::DatabaseModel::new()
.network()
.hosts()
.get(&mut db)
.await?;
let mut tx = db.begin().await?;
let mut breakages = IndexMap::new();
configure(
&mut tx,
&rpc_ctx.docker,
&*hosts,
&id,
config,
&timeout,
true,
&mut IndexMap::new(),
&mut breakages,
)
.await?;
crate::db::DatabaseModel::new()
.package_data()
.idx_model(&id)
.expect(&mut tx)
.await?
.installed()
.expect(&mut tx)
.await?
.status()
.configured()
.put(&mut tx, &true)
.await?;
Ok(BreakageRes {
patch: tx.abort().await?,
breakages,
})
}
pub async fn set_impl(
ctx: ExtendedContext<
EitherContext,
(PackageId, Option<Config>, Option<Duration>, Option<String>),
>,
) -> Result<WithRevision<()>, Error> {
let (ctx, (id, config, timeout, expire_id)) = ctx.split();
let rpc_ctx = ctx.as_rpc().unwrap();
let mut db = rpc_ctx.db.handle();
let hosts = crate::db::DatabaseModel::new()
.network()
.hosts()
.get(&mut db)
.await?;
let mut tx = db.begin().await?;
let mut breakages = IndexMap::new();
configure(
&mut tx,
&rpc_ctx.docker,
&*hosts,
&id,
config,
&timeout,
false,
&mut IndexMap::new(),
&mut breakages,
)
.await?;
crate::db::DatabaseModel::new()
.package_data()
.idx_model(&id)
.expect(&mut tx)
.await?
.installed()
.expect(&mut tx)
.await?
.status()
.configured()
.put(&mut tx, &true)
.await?;
Ok(WithRevision {
response: (),
revision: tx.commit(expire_id).await?,
})
}
pub fn configure<'a, Db: DbHandle>(
db: &'a mut Db,
docker: &'a Docker,
hosts: &'a Hosts,
id: &'a PackageId,
config: Option<Config>,
timeout: &'a Option<Duration>,
dry_run: bool,
overrides: &'a mut IndexMap<PackageId, Config>,
breakages: &'a mut IndexMap<PackageId, TaggedDependencyError>,
) -> BoxFuture<'a, Result<(), Error>> {
async move {
// fetch data from db
let pkg_model = crate::db::DatabaseModel::new()
.package_data()
.idx_model(id)
.and_then(|m| m.installed())
.expect(db)
.await
.with_kind(crate::ErrorKind::NotFound)?;
let action = pkg_model
.clone()
.manifest()
.config()
.get(db)
.await?
.to_owned()
.ok_or_else(|| {
Error::new(anyhow!("{} has no config", id), crate::ErrorKind::NotFound)
})?;
let version = pkg_model.clone().manifest().version().get(db).await?;
let dependencies = pkg_model.clone().manifest().dependencies().get(db).await?;
let volumes = pkg_model.clone().manifest().volumes().get(db).await?;
// get current config and current spec
let ConfigRes {
config: old_config,
spec,
} = action.get(id, &*version, &*volumes, &*hosts).await?;
// determine new config to use
let mut config = if let Some(config) = config.or_else(|| old_config.clone()) {
config
} else {
spec.gen(&mut rand::rngs::StdRng::from_entropy(), timeout)?
};
spec.matches(&config)?; // check that new config matches spec
spec.update(db, &*overrides, &mut config).await?; // dereference pointers in the new config
// create backreferences to pointers
let mut sys = pkg_model.clone().system_pointers().get_mut(db).await?;
sys.truncate(0);
let mut current_dependencies: IndexMap<PackageId, CurrentDependencyInfo> = dependencies
.0
.iter()
.filter_map(|(id, info)| {
if info.optional.is_none() {
Some((id.clone(), CurrentDependencyInfo::default()))
} 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)?;
})
.collect();
for ptr in spec.pointers(&config)? {
match ptr {
ValueSpecPointer::Package(PackagePointerSpec { package_id, target }) => {
if let Some(current_dependency) = current_dependencies.get_mut(&package_id) {
current_dependency.pointers.push(target);
} else {
current_dependencies.insert(
package_id,
CurrentDependencyInfo {
pointers: vec![target],
health_checks: IndexSet::new(),
},
);
}
}
ValueSpecPointer::System(s) => sys.push(s),
}
match old_config {
Some(old) if &old == &config && info.configured && !info.recoverable => {
return Ok(config)
}
sys.save(db).await?;
let signal = if !dry_run {
// run config action
let res = action
.set(id, &*version, &*dependencies, &*volumes, hosts, &config)
.await?;
// track dependencies with no pointers
for (package_id, health_checks) in res.depends_on.into_iter() {
if let Some(current_dependency) = current_dependencies.get_mut(&package_id) {
current_dependency.health_checks.extend(health_checks);
} else {
current_dependencies.insert(
package_id,
CurrentDependencyInfo {
pointers: Vec::new(),
health_checks,
},
);
}
_ => (),
};
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,
)
}
// track dependency health checks
let mut deps = pkg_model.clone().current_dependencies().get_mut(db).await?;
*deps = current_dependencies.clone();
deps.save(db).await?;
res.signal
} else {
None
};
// update dependencies
for (dependency, dep_info) in current_dependencies {
if let Some(dependency_model) = crate::db::DatabaseModel::new()
.package_data()
.idx_model(&dependency)
.and_then(|pkg| pkg.installed())
.check(db)
.await?
{
dependency_model
.current_dependents()
.idx_model(id)
.put(db, &dep_info)
.await?;
}
}
// cache current config for dependents
overrides.insert(id.clone(), config.clone());
// handle dependents
let dependents = pkg_model.clone().current_dependents().get(db).await?;
let prev = old_config.map(Value::Object).unwrap_or_default();
let next = Value::Object(config.clone());
for (dependent, dep_info) in &*dependents {
fn handle_broken_dependents<'a, Db: DbHandle>(
db: &'a mut Db,
id: &'a PackageId,
dependency: &'a PackageId,
model: InstalledPackageDataEntryModel,
error: DependencyError,
breakages: &'a mut IndexMap<PackageId, TaggedDependencyError>,
) -> BoxFuture<'a, Result<(), Error>> {
async move {
let mut status = model.clone().status().get_mut(db).await?;
let old = status.dependency_errors.0.remove(id);
let newly_broken = old.is_none();
status.dependency_errors.0.insert(
id.clone(),
if let Some(old) = old {
old.merge_with(error.clone())
} else {
error.clone()
},
);
if newly_broken {
breakages.insert(
id.clone(),
TaggedDependencyError {
dependency: dependency.clone(),
error: error.clone(),
},
);
if status.main.running() {
if model
.clone()
.manifest()
.dependencies()
.idx_model(dependency)
.expect(db)
.await?
.get(db)
.await?
.critical
{
Ok(_) => (),
Err(e) => {
handle_broken_dependent(name, dependent, dry_run, res, e)
status.main.stop();
let dependents = model.current_dependents().get(db).await?;
for (dependent, _) in &*dependents {
let dependent_model = crate::db::DatabaseModel::new()
.package_data()
.idx_model(dependent)
.and_then(|pkg| pkg.installed())
.expect(db)
.await?;
handle_broken_dependents(
db,
dependent,
id,
dependent_model,
DependencyError::NotRunning,
breakages,
)
.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?;
status.save(db).await?;
Ok(())
}
.boxed()
}
// check if config passes dependent check
let dependent_model = crate::db::DatabaseModel::new()
.package_data()
.idx_model(dependent)
.and_then(|pkg| pkg.installed())
.expect(db)
.await?;
if let Some(cfg) = &*dependent_model
.clone()
.manifest()
.dependencies()
.idx_model(id)
.expect(db)
.await?
.config()
.get(db)
.await?
{
let version = dependent_model.clone().manifest().version().get(db).await?;
if let Err(error) = cfg.check(dependent, &*version, &config).await? {
let dep_err = DependencyError::ConfigUnsatisfied { error };
handle_broken_dependents(
db,
dependent,
id,
dependent_model,
dep_err,
breakages,
)
.await?;
}
// handle backreferences
for ptr in &dep_info.pointers {
if let PackagePointerSpecVariant::Config { selector, multi } = ptr {
if selector.select(*multi, &next) != selector.select(*multi, &prev) {
if let Err(e) = configure(
db, docker, hosts, dependent, None, timeout, dry_run, overrides,
breakages,
)
.await
{
if e.kind == crate::ErrorKind::ConfigRulesViolation {
let dependent_model = crate::db::DatabaseModel::new()
.package_data()
.idx_model(dependent)
.and_then(|pkg| pkg.installed())
.expect(db)
.await?;
handle_broken_dependents(
db,
dependent,
id,
dependent_model,
DependencyError::ConfigUnsatisfied {
error: format!("{}", e),
},
breakages,
)
.await?;
} else {
return Err(e);
}
}
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, false).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)?;
if let Some(signal) = signal {
docker
.kill_container(
&DockerAction::container_name(id, &*version),
Some(KillContainerOptions {
signal: signal.to_string(),
}),
)
.await
// ignore container is not running https://docs.docker.com/engine/api/v1.41/#operation/ContainerKill
.or_else(|e| {
if matches!(
e,
bollard::errors::Error::DockerResponseConflictError { .. }
) {
Ok(())
} else {
Err(e)
}
})?;
}
Ok(())
}
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(())
.boxed()
}

View File

@@ -1,76 +0,0 @@
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" }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
use std::ops::Bound;
use std::ops::RangeBounds;
use std::ops::RangeInclusive;
use std::ops::{Bound, RangeBounds, RangeInclusive};
use rand::{distributions::Distribution, Rng};
use rand::distributions::Distribution;
use rand::Rng;
use serde_json::Value;
use super::value::Config;
use super::Config;
pub const STATIC_NULL: super::value::Value = super::value::Value::Null;
pub const STATIC_NULL: Value = Value::Null;
#[derive(Clone, Debug)]
pub struct CharSet(pub Vec<(RangeInclusive<char>, usize)>, usize);
@@ -15,7 +15,7 @@ impl CharSet {
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);
let mut idx = rng.gen_range(0..self.1);
for r in &self.0 {
if idx < r.1 {
return std::convert::TryFrom::try_from(
@@ -282,7 +282,7 @@ impl UniqueBy {
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::Exactly(key) => lhs.get(key) == rhs.get(key),
UniqueBy::NotUnique => false,
}
}

View File

@@ -1,66 +0,0 @@
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",
}
}
}