mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
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:
81
appmgr/src/config/action.rs
Normal file
81
appmgr/src/config/action.rs
Normal 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>>,
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user