mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
v0.3.4
remove health checks from manifest remove "restarting" bool on "starting" status remove restarting attr
This commit is contained in:
@@ -308,14 +308,12 @@ impl RpcContext {
|
||||
let main = match status.main {
|
||||
MainStatus::BackingUp { started, .. } => {
|
||||
if let Some(_) = started {
|
||||
MainStatus::Starting { restarting: false }
|
||||
MainStatus::Starting
|
||||
} else {
|
||||
MainStatus::Stopped
|
||||
}
|
||||
}
|
||||
MainStatus::Running { .. } => {
|
||||
MainStatus::Starting { restarting: false }
|
||||
}
|
||||
MainStatus::Running { .. } => MainStatus::Starting,
|
||||
a => a.clone(),
|
||||
};
|
||||
let new_package = PackageDataEntry::Installed {
|
||||
|
||||
@@ -67,10 +67,7 @@ pub async fn start(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(
|
||||
let mut tx = db.begin().await?;
|
||||
let receipts = StartReceipts::new(&mut tx, &id).await?;
|
||||
let version = receipts.version.get(&mut tx).await?;
|
||||
receipts
|
||||
.status
|
||||
.set(&mut tx, MainStatus::Starting { restarting: false })
|
||||
.await?;
|
||||
receipts.status.set(&mut tx, MainStatus::Starting).await?;
|
||||
heal_all_dependents_transitive(&ctx, &mut tx, &id, &receipts.dependency_receipt).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -44,7 +44,7 @@ impl Database {
|
||||
server_info: ServerInfo {
|
||||
id: account.server_id.clone(),
|
||||
version: Current::new().semver().into(),
|
||||
hostname: Some(account.hostname.no_dot_host_name()),
|
||||
hostname: account.hostname.no_dot_host_name(),
|
||||
last_backup: None,
|
||||
last_wifi_region: None,
|
||||
eos_version_compat: Current::new().compat().clone(),
|
||||
@@ -98,7 +98,7 @@ impl DatabaseModel {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ServerInfo {
|
||||
pub id: String,
|
||||
pub hostname: Option<String>,
|
||||
pub hostname: String,
|
||||
pub version: Version,
|
||||
pub last_backup: Option<DateTime<Utc>>,
|
||||
/// Used in the wifi to determine the region to set the system to
|
||||
|
||||
@@ -161,12 +161,7 @@ async fn save_state(
|
||||
set_status(&mut db, &seed.manifest, &MainStatus::Stopping).await
|
||||
}
|
||||
(None, StartStop::Stop, StartStop::Start) => {
|
||||
set_status(
|
||||
&mut db,
|
||||
&seed.manifest,
|
||||
&MainStatus::Starting { restarting: false },
|
||||
)
|
||||
.await
|
||||
set_status(&mut db, &seed.manifest, &MainStatus::Starting).await
|
||||
}
|
||||
(None, StartStop::Stop, StartStop::Stop) => {
|
||||
set_status(&mut db, &seed.manifest, &MainStatus::Stopped).await
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use super::{pause, resume, start, stop, ManagerSharedState, Status};
|
||||
use crate::status::MainStatus;
|
||||
use crate::Error;
|
||||
|
||||
/// Allocates a db handle. DO NOT CALL with a db handle already in scope
|
||||
async fn synchronize_once(shared: &ManagerSharedState) -> Result<Status, Error> {
|
||||
let mut db = shared.seed.ctx.db.handle();
|
||||
let mut status = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.idx_model(&shared.seed.manifest.id)
|
||||
.expect(&mut db)
|
||||
.await?
|
||||
.installed()
|
||||
.expect(&mut db)
|
||||
.await?
|
||||
.status()
|
||||
.main()
|
||||
.get_mut(&mut db)
|
||||
.await?;
|
||||
let manager_status = *shared.status.1.borrow();
|
||||
match manager_status {
|
||||
Status::Stopped => match &mut *status {
|
||||
MainStatus::Stopped => (),
|
||||
MainStatus::Stopping => {
|
||||
*status = MainStatus::Stopped;
|
||||
}
|
||||
MainStatus::Restarting => {
|
||||
*status = MainStatus::Starting { restarting: true };
|
||||
}
|
||||
MainStatus::Starting { .. } => {
|
||||
start(shared).await?;
|
||||
}
|
||||
MainStatus::Running { started, .. } => {
|
||||
*started = Utc::now();
|
||||
start(shared).await?;
|
||||
}
|
||||
MainStatus::BackingUp { .. } => (),
|
||||
},
|
||||
Status::Starting => match *status {
|
||||
MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => {
|
||||
stop(shared).await?;
|
||||
}
|
||||
MainStatus::Starting { .. } | MainStatus::Running { .. } => (),
|
||||
MainStatus::BackingUp { .. } => {
|
||||
pause(shared).await?;
|
||||
}
|
||||
},
|
||||
Status::Running => match *status {
|
||||
MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => {
|
||||
stop(shared).await?;
|
||||
}
|
||||
MainStatus::Starting { .. } => {
|
||||
*status = MainStatus::Running {
|
||||
started: Utc::now(),
|
||||
health: BTreeMap::new(),
|
||||
};
|
||||
}
|
||||
MainStatus::Running { .. } => (),
|
||||
MainStatus::BackingUp { .. } => {
|
||||
pause(shared).await?;
|
||||
}
|
||||
},
|
||||
Status::Paused => match *status {
|
||||
MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => {
|
||||
stop(shared).await?;
|
||||
}
|
||||
MainStatus::Starting { .. } | MainStatus::Running { .. } => {
|
||||
resume(shared).await?;
|
||||
}
|
||||
MainStatus::BackingUp { .. } => (),
|
||||
},
|
||||
Status::Shutdown => (),
|
||||
}
|
||||
status.save(&mut db).await?;
|
||||
Ok(manager_status)
|
||||
}
|
||||
|
||||
pub async fn synchronizer(shared: &ManagerSharedState) {
|
||||
let mut status_recv = shared.status.0.subscribe();
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => (),
|
||||
_ = shared.synchronize_now.notified() => (),
|
||||
_ = status_recv.changed() => (),
|
||||
}
|
||||
let status = match synchronize_once(shared).await {
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Synchronizer for {}@{} failed: {}",
|
||||
shared.seed.manifest.id,
|
||||
shared.seed.manifest.version,
|
||||
e
|
||||
);
|
||||
tracing::debug!("{:?}", e);
|
||||
continue;
|
||||
}
|
||||
Ok(status) => status,
|
||||
};
|
||||
tracing::trace!("{} status synchronized", shared.seed.manifest.id);
|
||||
shared.synchronized.notify_waiters();
|
||||
match status {
|
||||
Status::Shutdown => {
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,181 +554,182 @@ mod tests {
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let package_id = "test-package".parse().unwrap();
|
||||
let package_version: Version = "0.3.0.3".parse().unwrap();
|
||||
let name = ProcedureName::Action("test-deep-dir".parse().unwrap());
|
||||
let volumes: Volumes = serde_json::from_value(serde_json::json!({
|
||||
"main": {
|
||||
"type": "data"
|
||||
},
|
||||
"compat": {
|
||||
"type": "assets"
|
||||
},
|
||||
"filebrowser" :{
|
||||
"package-id": "filebrowser",
|
||||
"path": "data",
|
||||
"readonly": true,
|
||||
"type": "pointer",
|
||||
"volume-id": "main",
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let input: Option<serde_json::Value> = None;
|
||||
let timeout = Some(Duration::from_secs(10));
|
||||
js_action
|
||||
.execute::<serde_json::Value, serde_json::Value>(
|
||||
&path,
|
||||
&package_id,
|
||||
&package_version,
|
||||
name,
|
||||
&volumes,
|
||||
input,
|
||||
timeout,
|
||||
ProcessGroupId(0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
let package_id = "test-package".parse().unwrap();
|
||||
let package_version: Version = "0.3.0.3".parse().unwrap();
|
||||
let name = ProcedureName::Action("test-deep-dir".parse().unwrap());
|
||||
let volumes: Volumes = serde_json::from_value(serde_json::json!({
|
||||
"main": {
|
||||
"type": "data"
|
||||
},
|
||||
"compat": {
|
||||
"type": "assets"
|
||||
},
|
||||
"filebrowser" :{
|
||||
"package-id": "filebrowser",
|
||||
"path": "data",
|
||||
"readonly": true,
|
||||
"type": "pointer",
|
||||
"volume-id": "main",
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn js_action_test_deep_dir_escape() {
|
||||
let js_action = JsProcedure { args: vec![] };
|
||||
let path: PathBuf = "test/js_action_execute/"
|
||||
.parse::<PathBuf>()
|
||||
.unwrap()
|
||||
.canonicalize()
|
||||
let input: Option<serde_json::Value> = None;
|
||||
let timeout = Some(Duration::from_secs(10));
|
||||
js_action
|
||||
.execute::<serde_json::Value, serde_json::Value>(
|
||||
&path,
|
||||
&package_id,
|
||||
&package_version,
|
||||
name,
|
||||
&volumes,
|
||||
input,
|
||||
timeout,
|
||||
ProcessGroupId(0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn js_action_test_deep_dir_escape() {
|
||||
let js_action = JsProcedure { args: vec![] };
|
||||
let path: PathBuf = "test/js_action_execute/"
|
||||
.parse::<PathBuf>()
|
||||
.unwrap()
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let package_id = "test-package".parse().unwrap();
|
||||
let package_version: Version = "0.3.0.3".parse().unwrap();
|
||||
let name = ProcedureName::Action("test-deep-dir-escape".parse().unwrap());
|
||||
let volumes: Volumes = serde_json::from_value(serde_json::json!({
|
||||
"main": {
|
||||
"type": "data"
|
||||
},
|
||||
"compat": {
|
||||
"type": "assets"
|
||||
},
|
||||
"filebrowser" :{
|
||||
"package-id": "filebrowser",
|
||||
"path": "data",
|
||||
"readonly": true,
|
||||
"type": "pointer",
|
||||
"volume-id": "main",
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let package_id = "test-package".parse().unwrap();
|
||||
let package_version: Version = "0.3.0.3".parse().unwrap();
|
||||
let name = ProcedureName::Action("test-deep-dir-escape".parse().unwrap());
|
||||
let volumes: Volumes = serde_json::from_value(serde_json::json!({
|
||||
"main": {
|
||||
"type": "data"
|
||||
},
|
||||
"compat": {
|
||||
"type": "assets"
|
||||
},
|
||||
"filebrowser" :{
|
||||
"package-id": "filebrowser",
|
||||
"path": "data",
|
||||
"readonly": true,
|
||||
"type": "pointer",
|
||||
"volume-id": "main",
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let input: Option<serde_json::Value> = None;
|
||||
let timeout = Some(Duration::from_secs(10));
|
||||
js_action
|
||||
.execute::<serde_json::Value, serde_json::Value>(
|
||||
&path,
|
||||
&package_id,
|
||||
&package_version,
|
||||
name,
|
||||
&volumes,
|
||||
input,
|
||||
timeout,
|
||||
ProcessGroupId(0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
let input: Option<serde_json::Value> = None;
|
||||
let timeout = Some(Duration::from_secs(10));
|
||||
js_action
|
||||
.execute::<serde_json::Value, serde_json::Value>(
|
||||
&path,
|
||||
&package_id,
|
||||
&package_version,
|
||||
name,
|
||||
&volumes,
|
||||
input,
|
||||
timeout,
|
||||
ProcessGroupId(0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn js_action_test_zero_dir() {
|
||||
let js_action = JsProcedure { args: vec![] };
|
||||
let path: PathBuf = "test/js_action_execute/"
|
||||
.parse::<PathBuf>()
|
||||
.unwrap()
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let package_id = "test-package".parse().unwrap();
|
||||
let package_version: Version = "0.3.0.3".parse().unwrap();
|
||||
let name = ProcedureName::Action("test-zero-dir".parse().unwrap());
|
||||
let volumes: Volumes = serde_json::from_value(serde_json::json!({
|
||||
"main": {
|
||||
"type": "data"
|
||||
},
|
||||
"compat": {
|
||||
"type": "assets"
|
||||
},
|
||||
"filebrowser" :{
|
||||
"package-id": "filebrowser",
|
||||
"path": "data",
|
||||
"readonly": true,
|
||||
"type": "pointer",
|
||||
"volume-id": "main",
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn js_action_test_zero_dir() {
|
||||
let js_action = JsProcedure { args: vec![] };
|
||||
let path: PathBuf = "test/js_action_execute/"
|
||||
.parse::<PathBuf>()
|
||||
.unwrap()
|
||||
.canonicalize()
|
||||
let input: Option<serde_json::Value> = None;
|
||||
let timeout = Some(Duration::from_secs(10));
|
||||
js_action
|
||||
.execute::<serde_json::Value, serde_json::Value>(
|
||||
&path,
|
||||
&package_id,
|
||||
&package_version,
|
||||
name,
|
||||
&volumes,
|
||||
input,
|
||||
timeout,
|
||||
ProcessGroupId(0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn js_action_test_read_dir() {
|
||||
let js_action = JsProcedure { args: vec![] };
|
||||
let path: PathBuf = "test/js_action_execute/"
|
||||
.parse::<PathBuf>()
|
||||
.unwrap()
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let package_id = "test-package".parse().unwrap();
|
||||
let package_version: Version = "0.3.0.3".parse().unwrap();
|
||||
let name = ProcedureName::Action("test-read-dir".parse().unwrap());
|
||||
let volumes: Volumes = serde_json::from_value(serde_json::json!({
|
||||
"main": {
|
||||
"type": "data"
|
||||
},
|
||||
"compat": {
|
||||
"type": "assets"
|
||||
},
|
||||
"filebrowser" :{
|
||||
"package-id": "filebrowser",
|
||||
"path": "data",
|
||||
"readonly": true,
|
||||
"type": "pointer",
|
||||
"volume-id": "main",
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let package_id = "test-package".parse().unwrap();
|
||||
let package_version: Version = "0.3.0.3".parse().unwrap();
|
||||
let name = ProcedureName::Action("test-zero-dir".parse().unwrap());
|
||||
let volumes: Volumes = serde_json::from_value(serde_json::json!({
|
||||
"main": {
|
||||
"type": "data"
|
||||
},
|
||||
"compat": {
|
||||
"type": "assets"
|
||||
},
|
||||
"filebrowser" :{
|
||||
"package-id": "filebrowser",
|
||||
"path": "data",
|
||||
"readonly": true,
|
||||
"type": "pointer",
|
||||
"volume-id": "main",
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let input: Option<serde_json::Value> = None;
|
||||
let timeout = Some(Duration::from_secs(10));
|
||||
js_action
|
||||
.execute::<serde_json::Value, serde_json::Value>(
|
||||
&path,
|
||||
&package_id,
|
||||
&package_version,
|
||||
name,
|
||||
&volumes,
|
||||
input,
|
||||
timeout,
|
||||
ProcessGroupId(0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn js_action_test_read_dir() {
|
||||
let js_action = JsProcedure { args: vec![] };
|
||||
let path: PathBuf = "test/js_action_execute/"
|
||||
.parse::<PathBuf>()
|
||||
.unwrap()
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let package_id = "test-package".parse().unwrap();
|
||||
let package_version: Version = "0.3.0.3".parse().unwrap();
|
||||
let name = ProcedureName::Action("test-read-dir".parse().unwrap());
|
||||
let volumes: Volumes = serde_json::from_value(serde_json::json!({
|
||||
"main": {
|
||||
"type": "data"
|
||||
},
|
||||
"compat": {
|
||||
"type": "assets"
|
||||
},
|
||||
"filebrowser" :{
|
||||
"package-id": "filebrowser",
|
||||
"path": "data",
|
||||
"readonly": true,
|
||||
"type": "pointer",
|
||||
"volume-id": "main",
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let input: Option<serde_json::Value> = None;
|
||||
let timeout = Some(Duration::from_secs(10));
|
||||
js_action
|
||||
.execute::<serde_json::Value, serde_json::Value>(
|
||||
&path,
|
||||
&package_id,
|
||||
&package_version,
|
||||
name,
|
||||
&volumes,
|
||||
input,
|
||||
timeout,
|
||||
ProcessGroupId(0),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
let input: Option<serde_json::Value> = None;
|
||||
let timeout = Some(Duration::from_secs(10));
|
||||
js_action
|
||||
.execute::<serde_json::Value, serde_json::Value>(
|
||||
&path,
|
||||
&package_id,
|
||||
&package_version,
|
||||
name,
|
||||
&volumes,
|
||||
input,
|
||||
timeout,
|
||||
ProcessGroupId(0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn js_action_test_deep_dir() {
|
||||
|
||||
@@ -26,9 +26,7 @@ pub enum MainStatus {
|
||||
Stopped,
|
||||
Restarting,
|
||||
Stopping,
|
||||
Starting {
|
||||
restarting: bool,
|
||||
},
|
||||
Starting,
|
||||
Running {
|
||||
started: DateTime<Utc>,
|
||||
health: BTreeMap<HealthCheckId, HealthCheckResult>,
|
||||
|
||||
@@ -9,40 +9,17 @@ use sqlx::PgPool;
|
||||
use crate::init::InitReceipts;
|
||||
use crate::Error;
|
||||
|
||||
mod v0_3_0;
|
||||
mod v0_3_0_1;
|
||||
mod v0_3_0_2;
|
||||
mod v0_3_0_3;
|
||||
mod v0_3_1;
|
||||
mod v0_3_1_1;
|
||||
mod v0_3_1_2;
|
||||
mod v0_3_2;
|
||||
mod v0_3_2_1;
|
||||
mod v0_3_3;
|
||||
mod v0_3_4;
|
||||
mod v0_3_4_1;
|
||||
mod v0_3_4_2;
|
||||
mod v0_3_4_3;
|
||||
mod v0_4_0;
|
||||
|
||||
pub type Current = v0_3_4_3::Version;
|
||||
pub type Current = v0_4_0::Version;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
enum Version {
|
||||
V0_3_0(Wrapper<v0_3_0::Version>),
|
||||
V0_3_0_1(Wrapper<v0_3_0_1::Version>),
|
||||
V0_3_0_2(Wrapper<v0_3_0_2::Version>),
|
||||
V0_3_0_3(Wrapper<v0_3_0_3::Version>),
|
||||
V0_3_1(Wrapper<v0_3_1::Version>),
|
||||
V0_3_1_1(Wrapper<v0_3_1_1::Version>),
|
||||
V0_3_1_2(Wrapper<v0_3_1_2::Version>),
|
||||
V0_3_2(Wrapper<v0_3_2::Version>),
|
||||
V0_3_2_1(Wrapper<v0_3_2_1::Version>),
|
||||
V0_3_3(Wrapper<v0_3_3::Version>),
|
||||
V0_3_4(Wrapper<v0_3_4::Version>),
|
||||
V0_3_4_1(Wrapper<v0_3_4_1::Version>),
|
||||
V0_3_4_2(Wrapper<v0_3_4_2::Version>),
|
||||
LT0_3_4_3(LTWrapper<v0_3_4_3::Version>),
|
||||
V0_3_4_3(Wrapper<v0_3_4_3::Version>),
|
||||
V0_4_0(Wrapper<v0_4_0::Version>),
|
||||
Other(emver::Version),
|
||||
}
|
||||
|
||||
@@ -58,20 +35,9 @@ impl Version {
|
||||
#[cfg(test)]
|
||||
fn as_sem_ver(&self) -> emver::Version {
|
||||
match self {
|
||||
Version::V0_3_0(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_0_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_0_2(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_0_3(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_1_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_1_2(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_2(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_2_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_3(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_4(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_4_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_4_2(Wrapper(x)) => x.semver(),
|
||||
Version::LT0_3_4_3(LTWrapper(x)) => x.semver(),
|
||||
Version::V0_3_4_3(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0(Wrapper(x)) => x.semver(),
|
||||
Version::Other(x) => x.clone(),
|
||||
}
|
||||
}
|
||||
@@ -177,6 +143,32 @@ where
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LTWrapper<T>(T, emver::Version);
|
||||
impl<T> serde::Serialize for LTWrapper<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 LTWrapper<T>
|
||||
where
|
||||
T: VersionT,
|
||||
{
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let v = crate::util::Version::deserialize(deserializer)?;
|
||||
let version = T::new();
|
||||
if *v < version.semver() {
|
||||
Ok(Self(version, v.into_version()))
|
||||
} else {
|
||||
Err(serde::de::Error::custom("Mismatched Version"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Wrapper<T>(T);
|
||||
impl<T> serde::Serialize for Wrapper<T>
|
||||
@@ -209,62 +201,20 @@ pub async fn init<Db: DbHandle>(
|
||||
) -> Result<(), Error> {
|
||||
let version = Version::from_util_version(receipts.server_version.get(db).await?);
|
||||
match version {
|
||||
Version::V0_3_0(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_0_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_0_2(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_0_3(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_1_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_1_2(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_2(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_2_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_3(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_4(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_4_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_4_2(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
Version::LT0_3_4_3(_) => {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot migrate from pre-0.3.4. Please update to v0.3.4 first."),
|
||||
crate::ErrorKind::MigrationFailed,
|
||||
));
|
||||
}
|
||||
Version::V0_3_4_3(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_4_0(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::Other(_) => {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot downgrade"),
|
||||
@@ -297,19 +247,6 @@ mod tests {
|
||||
|
||||
fn versions() -> impl Strategy<Value = Version> {
|
||||
prop_oneof![
|
||||
Just(Version::V0_3_0(Wrapper(v0_3_0::Version::new()))),
|
||||
Just(Version::V0_3_0_1(Wrapper(v0_3_0_1::Version::new()))),
|
||||
Just(Version::V0_3_0_2(Wrapper(v0_3_0_2::Version::new()))),
|
||||
Just(Version::V0_3_0_3(Wrapper(v0_3_0_3::Version::new()))),
|
||||
Just(Version::V0_3_1(Wrapper(v0_3_1::Version::new()))),
|
||||
Just(Version::V0_3_1_1(Wrapper(v0_3_1_1::Version::new()))),
|
||||
Just(Version::V0_3_1_2(Wrapper(v0_3_1_2::Version::new()))),
|
||||
Just(Version::V0_3_2(Wrapper(v0_3_2::Version::new()))),
|
||||
Just(Version::V0_3_2_1(Wrapper(v0_3_2_1::Version::new()))),
|
||||
Just(Version::V0_3_3(Wrapper(v0_3_3::Version::new()))),
|
||||
Just(Version::V0_3_4(Wrapper(v0_3_4::Version::new()))),
|
||||
Just(Version::V0_3_4_1(Wrapper(v0_3_4_1::Version::new()))),
|
||||
Just(Version::V0_3_4_2(Wrapper(v0_3_4_2::Version::new()))),
|
||||
Just(Version::V0_3_4_3(Wrapper(v0_3_4_3::Version::new()))),
|
||||
em_version().prop_map(Version::Other),
|
||||
]
|
||||
|
||||
36
backend/src/version/v0_4_0.rs
Normal file
36
backend/src/version/v0_4_0.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use async_trait::async_trait;
|
||||
use emver::VersionRange;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use super::*;
|
||||
|
||||
const V0_4_0: emver::Version = emver::Version::new(0, 4, 0, 0);
|
||||
lazy_static! {
|
||||
pub static ref V0_4_0_COMPAT: VersionRange = VersionRange::Conj(
|
||||
Box::new(VersionRange::Anchor(emver::GTE, V0_4_0)),
|
||||
Box::new(VersionRange::Anchor(emver::LTE, Current::new().semver())),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Version;
|
||||
|
||||
#[async_trait]
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_3_4::Version;
|
||||
fn new() -> Self {
|
||||
Version
|
||||
}
|
||||
fn semver(&self) -> emver::Version {
|
||||
V0_4_0
|
||||
}
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_4_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db, secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, db: &mut Db, secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": null,
|
||||
"ack-welcome": "0.3.4.3",
|
||||
"ack-welcome": "0.4.0",
|
||||
"marketplace": {
|
||||
"selected-url": "https://registry.start9.com/",
|
||||
"known-hosts": {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppShowPage } from './app-show.page'
|
||||
import { EmverPipesModule, ResponsiveColModule } from '@start9labs/shared'
|
||||
import { EmverPipesModule, ResponsiveColModule, SharedPipesModule } from '@start9labs/shared'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
|
||||
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
|
||||
@@ -16,7 +16,6 @@ import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.c
|
||||
import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component'
|
||||
import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component'
|
||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||
@@ -34,7 +33,6 @@ const routes: Routes = [
|
||||
AppShowPage,
|
||||
HealthColorPipe,
|
||||
ProgressDataPipe,
|
||||
ToHealthChecksPipe,
|
||||
ToButtonsPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
@@ -56,6 +54,7 @@ const routes: Routes = [
|
||||
LaunchablePipeModule,
|
||||
UiPipeModule,
|
||||
ResponsiveColModule,
|
||||
SharedPipesModule,
|
||||
],
|
||||
})
|
||||
export class AppShowPageModule {}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkg]="pkg"
|
||||
[pkgId]="pkgId"
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<app-show-dependencies
|
||||
|
||||
@@ -31,7 +31,7 @@ const STATES = [
|
||||
export class AppShowPage {
|
||||
readonly secure = this.config.isSecure()
|
||||
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
||||
tap(pkg => {
|
||||
|
||||
@@ -1,92 +1,82 @@
|
||||
<ng-container
|
||||
*ngIf="pkg | toHealthChecks | async | keyvalue: asIsOrder as checks"
|
||||
>
|
||||
<ng-container *ngIf="checks.length">
|
||||
<ion-item-divider>Health Checks</ion-item-divider>
|
||||
<!-- connected -->
|
||||
<ng-container *ngIf="connected$ | async; else disconnected">
|
||||
<ion-item *ngFor="let health of checks">
|
||||
<!-- result -->
|
||||
<ng-container *ngIf="health.value?.result as result; else noResult">
|
||||
<ion-spinner
|
||||
*ngIf="isLoading(result)"
|
||||
class="icon-spinner"
|
||||
color="primary"
|
||||
slot="start"
|
||||
></ion-spinner>
|
||||
<ion-icon
|
||||
*ngIf="result === HealthResult.Success"
|
||||
slot="start"
|
||||
name="checkmark"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="result === HealthResult.Failure"
|
||||
slot="start"
|
||||
name="warning-outline"
|
||||
color="warning"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="result === HealthResult.Disabled"
|
||||
slot="start"
|
||||
name="remove"
|
||||
color="dark"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h2 class="bold">
|
||||
{{ pkg.manifest['health-checks'][health.key].name }}
|
||||
</h2>
|
||||
<ion-text [color]="result | healthColor">
|
||||
<p>
|
||||
<span *ngIf="isReady(result)">{{ result | titlecase }}</span>
|
||||
<span *ngIf="result === HealthResult.Starting">...</span>
|
||||
<span *ngIf="result === HealthResult.Failure">
|
||||
{{ $any(health.value).error }}
|
||||
</span>
|
||||
<span *ngIf="result === HealthResult.Loading">
|
||||
{{ $any(health.value).message }}
|
||||
</span>
|
||||
<span
|
||||
*ngIf="
|
||||
result === HealthResult.Success &&
|
||||
pkg.manifest['health-checks'][health.key]['success-message']
|
||||
"
|
||||
>:
|
||||
{{
|
||||
pkg.manifest['health-checks'][health.key]['success-message']
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ng-container>
|
||||
<!-- no result -->
|
||||
<ng-template #noResult>
|
||||
<ion-spinner
|
||||
class="icon-spinner"
|
||||
color="dark"
|
||||
slot="start"
|
||||
></ion-spinner>
|
||||
<ion-label>
|
||||
<h2 class="bold">
|
||||
{{ pkg.manifest['health-checks'][health.key].name }}
|
||||
</h2>
|
||||
<p class="primary">Awaiting result...</p>
|
||||
</ion-label>
|
||||
</ng-template>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<!-- disconnected -->
|
||||
<ng-template #disconnected>
|
||||
<ion-item *ngFor="let health of checks">
|
||||
<ion-avatar slot="start">
|
||||
<ion-skeleton-text class="avatar"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ng-container *ngIf="healthChecks$ | async as checks">
|
||||
<ion-item-divider>Health Checks</ion-item-divider>
|
||||
<!-- connected -->
|
||||
<ng-container *ngIf="connected$ | async; else disconnected">
|
||||
<ion-item *ngFor="let check of checks">
|
||||
<!-- result -->
|
||||
<ng-container *ngIf="check.result as result; else noResult">
|
||||
<ion-spinner
|
||||
*ngIf="isLoading(result)"
|
||||
class="icon-spinner"
|
||||
color="primary"
|
||||
slot="start"
|
||||
></ion-spinner>
|
||||
<ion-icon
|
||||
*ngIf="result === 'success'"
|
||||
slot="start"
|
||||
name="checkmark"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="result === 'failure'"
|
||||
slot="start"
|
||||
name="warning-outline"
|
||||
color="warning"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="result === 'disabled'"
|
||||
slot="start"
|
||||
name="remove"
|
||||
color="dark"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<ion-skeleton-text class="label"></ion-skeleton-text>
|
||||
<ion-skeleton-text class="description"></ion-skeleton-text>
|
||||
<h2 class="bold">
|
||||
{{ check.name }}
|
||||
</h2>
|
||||
<ion-text [color]="result | healthColor">
|
||||
<p>
|
||||
<span *ngIf="isReady(result)">{{ result | titlecase }}</span>
|
||||
<span *ngIf="result === 'starting'">...</span>
|
||||
<span *ngIf="result === 'failure'">
|
||||
{{ $any(check).error }}
|
||||
</span>
|
||||
<span *ngIf="result === 'loading'">
|
||||
{{ $any(check).message }}
|
||||
</span>
|
||||
<span *ngIf="result === 'success'"
|
||||
>:
|
||||
{{ $any(check).message }}
|
||||
</span>
|
||||
</p>
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<!-- no result -->
|
||||
<ng-template #noResult>
|
||||
<ion-spinner
|
||||
class="icon-spinner"
|
||||
color="dark"
|
||||
slot="start"
|
||||
></ion-spinner>
|
||||
<ion-label>
|
||||
<h2 class="bold">
|
||||
{{ check.name }}
|
||||
</h2>
|
||||
<p class="primary">Awaiting result...</p>
|
||||
</ion-label>
|
||||
</ng-template>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<!-- disconnected -->
|
||||
<ng-template #disconnected>
|
||||
<ion-item *ngFor="let check of checks">
|
||||
<ion-avatar slot="start">
|
||||
<ion-skeleton-text class="avatar"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text class="label"></ion-skeleton-text>
|
||||
<ion-skeleton-text class="description"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import {
|
||||
HealthResult,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel, HealthResult } from 'src/app/services/patch-db/data-model'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-health-checks',
|
||||
@@ -12,14 +12,26 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowHealthChecksComponent {
|
||||
@Input()
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
HealthResult = HealthResult
|
||||
@Input() pkgId!: string
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
get healthChecks$() {
|
||||
return this.patch
|
||||
.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
||||
.pipe(
|
||||
map(main => {
|
||||
if (main.status !== 'running' || isEmptyObject(main.health))
|
||||
return null
|
||||
return Object.values(main.health)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
isLoading(result: HealthResult): boolean {
|
||||
return result === HealthResult.Starting || result === HealthResult.Loading
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import {
|
||||
DataModel,
|
||||
HealthCheckResult,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import { map, startWith } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@Pipe({
|
||||
name: 'toHealthChecks',
|
||||
})
|
||||
export class ToHealthChecksPipe implements PipeTransform {
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
|
||||
transform(
|
||||
pkg: PackageDataEntry,
|
||||
): Observable<Record<string, HealthCheckResult | null>> | null {
|
||||
const healthChecks = Object.keys(pkg.manifest['health-checks']).reduce(
|
||||
(obj, key) => ({ ...obj, [key]: null }),
|
||||
{},
|
||||
)
|
||||
|
||||
const healthChecks$ = this.patch
|
||||
.watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main')
|
||||
.pipe(
|
||||
map(main => {
|
||||
// Question: is this ok or do we have to use Object.keys
|
||||
// to maintain order and the keys initially present in pkg?
|
||||
return main.status === PackageMainStatus.Running &&
|
||||
!isEmptyObject(main.health)
|
||||
? main.health
|
||||
: healthChecks
|
||||
}),
|
||||
startWith(healthChecks),
|
||||
)
|
||||
|
||||
return isEmptyObject(healthChecks) ? null : healthChecks$
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,6 @@ export module Mock {
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': '1ms',
|
||||
},
|
||||
'health-checks': {},
|
||||
config: {
|
||||
get: null,
|
||||
set: null,
|
||||
@@ -382,7 +381,6 @@ export module Mock {
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': '10000µs',
|
||||
},
|
||||
'health-checks': {},
|
||||
config: {
|
||||
get: null,
|
||||
set: null,
|
||||
@@ -535,7 +533,6 @@ export module Mock {
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': '1m',
|
||||
},
|
||||
'health-checks': {},
|
||||
config: { get: {} as any, set: {} as any },
|
||||
volumes: {},
|
||||
'min-os-version': '0.2.12',
|
||||
|
||||
@@ -727,7 +727,18 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2 = [
|
||||
if (params.id !== 'bitcoind') {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: {},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch2)
|
||||
}
|
||||
|
||||
const patch3 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/status',
|
||||
@@ -739,52 +750,7 @@ export class MockApiService extends ApiService {
|
||||
value: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch2)
|
||||
|
||||
const patch3 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: {
|
||||
'ephemeral-health-check': {
|
||||
result: 'starting',
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
result: 'disabled',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch3)
|
||||
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch4 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: {
|
||||
'ephemeral-health-check': {
|
||||
result: 'starting',
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
result: 'disabled',
|
||||
},
|
||||
'chain-state': {
|
||||
result: 'loading',
|
||||
message: 'Bitcoin is syncing from genesis',
|
||||
},
|
||||
'p2p-interface': {
|
||||
result: 'success',
|
||||
},
|
||||
'rpc-interface': {
|
||||
result: 'failure',
|
||||
error: 'RPC interface unreachable.',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch4)
|
||||
}, 2000)
|
||||
|
||||
const originalPatch = [
|
||||
@@ -896,11 +862,6 @@ export class MockApiService extends ApiService {
|
||||
path: path + '/status',
|
||||
value: PackageMainStatus.Stopping,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path + '/health',
|
||||
value: {},
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch)
|
||||
|
||||
@@ -127,24 +127,6 @@ export const mockPatchData: DataModel = {
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': '.49m',
|
||||
},
|
||||
'health-checks': {
|
||||
'chain-state': {
|
||||
name: 'Chain State',
|
||||
},
|
||||
'ephemeral-health-check': {
|
||||
name: 'Ephemeral Health Check',
|
||||
},
|
||||
'p2p-interface': {
|
||||
name: 'P2P Interface',
|
||||
'success-message': 'the health check ran succesfully',
|
||||
},
|
||||
'rpc-interface': {
|
||||
name: 'RPC Interface',
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
name: 'Unneccessary Health Check',
|
||||
},
|
||||
} as any,
|
||||
config: {
|
||||
get: {},
|
||||
set: {},
|
||||
@@ -243,9 +225,12 @@ export const mockPatchData: DataModel = {
|
||||
'Your reason for re-syncing. Why are you doing this?',
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
pattern: '^[a-zA-Z]+$',
|
||||
'pattern-description': 'Must contain only letters.',
|
||||
placeholder: null,
|
||||
textarea: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
@@ -253,14 +238,19 @@ export const mockPatchData: DataModel = {
|
||||
description: 'Tell the class your name.',
|
||||
nullable: true,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
warning: 'You may loose all your money by providing your name.',
|
||||
placeholder: null,
|
||||
pattern: null,
|
||||
'pattern-description': null,
|
||||
textarea: false,
|
||||
default: null,
|
||||
},
|
||||
notifications: {
|
||||
name: 'Notification Preferences',
|
||||
type: 'list',
|
||||
subtype: 'enum',
|
||||
description: 'how you want to be notified',
|
||||
warning: null,
|
||||
range: '[1,3]',
|
||||
default: ['email'],
|
||||
spec: {
|
||||
@@ -282,6 +272,9 @@ export const mockPatchData: DataModel = {
|
||||
default: 100,
|
||||
range: '[0, 9999]',
|
||||
integral: true,
|
||||
units: null,
|
||||
placeholder: null,
|
||||
warning: null,
|
||||
},
|
||||
'top-speed': {
|
||||
type: 'number',
|
||||
@@ -291,6 +284,9 @@ export const mockPatchData: DataModel = {
|
||||
range: '[-1000, 1000]',
|
||||
integral: false,
|
||||
units: 'm/s',
|
||||
placeholder: null,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
testnet: {
|
||||
name: 'Testnet',
|
||||
@@ -318,22 +314,33 @@ export const mockPatchData: DataModel = {
|
||||
name: 'Emergency Contact',
|
||||
type: 'object',
|
||||
description: 'The person to contact in case of emergency.',
|
||||
warning: null,
|
||||
spec: {
|
||||
name: {
|
||||
type: 'string',
|
||||
name: 'Name',
|
||||
description: null,
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
pattern: '^[a-zA-Z]+$',
|
||||
'pattern-description': 'Must contain only letters.',
|
||||
placeholder: null,
|
||||
textarea: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
name: 'Email',
|
||||
description: null,
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
placeholder: null,
|
||||
pattern: null,
|
||||
'pattern-description': null,
|
||||
textarea: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -351,7 +358,7 @@ export const mockPatchData: DataModel = {
|
||||
pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$',
|
||||
'pattern-description': 'Must be a valid IP address',
|
||||
masked: false,
|
||||
copyable: false,
|
||||
placeholder: null,
|
||||
},
|
||||
},
|
||||
bitcoinNode: {
|
||||
@@ -375,7 +382,12 @@ export const mockPatchData: DataModel = {
|
||||
description: 'the lan address',
|
||||
nullable: true,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
placeholder: null,
|
||||
pattern: null,
|
||||
'pattern-description': null,
|
||||
textarea: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
external: {
|
||||
@@ -388,7 +400,9 @@ export const mockPatchData: DataModel = {
|
||||
pattern: '.*',
|
||||
'pattern-description': 'anything',
|
||||
masked: false,
|
||||
copyable: true,
|
||||
placeholder: null,
|
||||
textarea: false,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -411,20 +425,26 @@ export const mockPatchData: DataModel = {
|
||||
started: '2021-06-14T20:49:17.774Z',
|
||||
health: {
|
||||
'ephemeral-health-check': {
|
||||
name: 'Ephemeral Health Check',
|
||||
result: HealthResult.Starting,
|
||||
},
|
||||
'chain-state': {
|
||||
name: 'Chain State',
|
||||
result: HealthResult.Loading,
|
||||
message: 'Bitcoin is syncing from genesis',
|
||||
},
|
||||
'p2p-interface': {
|
||||
name: 'P2P Interface',
|
||||
result: HealthResult.Success,
|
||||
message: 'the health check ran successfully',
|
||||
},
|
||||
'rpc-interface': {
|
||||
name: 'RPC Interface',
|
||||
result: HealthResult.Failure,
|
||||
error: 'RPC interface unreachable.',
|
||||
},
|
||||
'unnecessary-health-check': {
|
||||
name: 'Totally Unnecessary',
|
||||
result: HealthResult.Disabled,
|
||||
},
|
||||
},
|
||||
@@ -461,7 +481,7 @@ export const mockPatchData: DataModel = {
|
||||
manifest: {
|
||||
id: 'lnd',
|
||||
title: 'Lightning Network Daemon',
|
||||
version: '0.11.0',
|
||||
version: '0.11.1',
|
||||
description: {
|
||||
short: 'A bolt spec compliant client.',
|
||||
long: 'More info about LND. More info about LND. More info about LND.',
|
||||
@@ -501,7 +521,6 @@ export const mockPatchData: DataModel = {
|
||||
'shm-size': '',
|
||||
'sigterm-timeout': '0.5s',
|
||||
},
|
||||
'health-checks': {},
|
||||
config: {
|
||||
get: null,
|
||||
set: null,
|
||||
|
||||
@@ -155,10 +155,6 @@ export interface Manifest extends MarketplaceManifest<DependencyConfig | null> {
|
||||
scripts: string // path to scripts folder
|
||||
}
|
||||
main: ActionImpl
|
||||
'health-checks': Record<
|
||||
string,
|
||||
ActionImpl & { name: string; 'success-message': string | null }
|
||||
>
|
||||
config: ConfigActions | null
|
||||
volumes: Record<string, Volume>
|
||||
'min-os-version': string
|
||||
@@ -295,7 +291,6 @@ export interface MainStatusStopping {
|
||||
|
||||
export interface MainStatusStarting {
|
||||
status: PackageMainStatus.Starting
|
||||
restarting: boolean
|
||||
}
|
||||
|
||||
export interface MainStatusRunning {
|
||||
@@ -322,12 +317,13 @@ export enum PackageMainStatus {
|
||||
Restarting = 'restarting',
|
||||
}
|
||||
|
||||
export type HealthCheckResult =
|
||||
export type HealthCheckResult = { name: string } & (
|
||||
| HealthCheckResultStarting
|
||||
| HealthCheckResultLoading
|
||||
| HealthCheckResultDisabled
|
||||
| HealthCheckResultSuccess
|
||||
| HealthCheckResultFailure
|
||||
)
|
||||
|
||||
export enum HealthResult {
|
||||
Starting = 'starting',
|
||||
@@ -347,6 +343,7 @@ export interface HealthCheckResultDisabled {
|
||||
|
||||
export interface HealthCheckResultSuccess {
|
||||
result: HealthResult.Success
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface HealthCheckResultLoading {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import {
|
||||
InstalledPackageDataEntry,
|
||||
MainStatusStarting,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
@@ -8,42 +9,39 @@ import {
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
|
||||
export interface PackageStatus {
|
||||
primary: PrimaryStatus
|
||||
primary: PrimaryStatus | PackageState | PackageMainStatus
|
||||
dependency: DependencyStatus | null
|
||||
health: HealthStatus | null
|
||||
}
|
||||
|
||||
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
||||
let primary: PrimaryStatus
|
||||
let primary: PrimaryStatus | PackageState | PackageMainStatus
|
||||
let dependency: DependencyStatus | null = null
|
||||
let health: HealthStatus | null = null
|
||||
const hasHealthChecks = !isEmptyObject(pkg.manifest['health-checks'])
|
||||
|
||||
if (pkg.state === PackageState.Installed && pkg.installed) {
|
||||
primary = getPrimaryStatus(pkg.installed.status)
|
||||
dependency = getDependencyStatus(pkg)
|
||||
health = getHealthStatus(pkg.installed.status, hasHealthChecks)
|
||||
dependency = getDependencyStatus(pkg.installed)
|
||||
health = getHealthStatus(pkg.installed.status)
|
||||
} else {
|
||||
primary = pkg.state as string as PrimaryStatus
|
||||
primary = pkg.state
|
||||
}
|
||||
|
||||
return { primary, dependency, health }
|
||||
}
|
||||
|
||||
function getPrimaryStatus(status: Status): PrimaryStatus {
|
||||
function getPrimaryStatus(status: Status): PrimaryStatus | PackageMainStatus {
|
||||
if (!status.configured) {
|
||||
return PrimaryStatus.NeedsConfig
|
||||
} else if ((status.main as MainStatusStarting).restarting) {
|
||||
return PrimaryStatus.Restarting
|
||||
} else {
|
||||
return status.main.status as any as PrimaryStatus
|
||||
return status.main.status
|
||||
}
|
||||
}
|
||||
|
||||
function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null {
|
||||
const installed = pkg.installed
|
||||
if (!installed || isEmptyObject(installed['current-dependencies']))
|
||||
return null
|
||||
function getDependencyStatus(
|
||||
installed: InstalledPackageDataEntry,
|
||||
): DependencyStatus | null {
|
||||
if (isEmptyObject(installed['current-dependencies'])) return null
|
||||
|
||||
const depErrors = installed.status['dependency-errors']
|
||||
const depIds = Object.keys(depErrors).filter(key => !!depErrors[key])
|
||||
@@ -51,11 +49,8 @@ function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null {
|
||||
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
||||
}
|
||||
|
||||
function getHealthStatus(
|
||||
status: Status,
|
||||
hasHealthChecks: boolean,
|
||||
): HealthStatus | null {
|
||||
if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
|
||||
function getHealthStatus(status: Status): HealthStatus | null {
|
||||
if (status.main.status !== PackageMainStatus.Running) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -65,10 +60,6 @@ function getHealthStatus(
|
||||
return HealthStatus.Failure
|
||||
}
|
||||
|
||||
if (!values.length && hasHealthChecks) {
|
||||
return HealthStatus.Waiting
|
||||
}
|
||||
|
||||
if (values.some(h => h.result === 'loading')) {
|
||||
return HealthStatus.Loading
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user