diff --git a/Cargo.lock b/Cargo.lock index 058004ef8..dddba558d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4765,7 +4765,6 @@ dependencies = [ "tonic", "tonic-build", "tracing", - "uuid", ] [[package]] @@ -4807,6 +4806,7 @@ dependencies = [ "futures", "hyper", "portpicker", + "prost-types", "rmp-serde", "serde_json", "shuttle-common", @@ -4820,7 +4820,6 @@ dependencies = [ "tower", "tracing", "tracing-subscriber", - "uuid", "wasi-common", "wasmtime", "wasmtime-wasi", diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index 058a0eca5..ee118d331 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -37,7 +37,7 @@ use shuttle_service::builder::{build_crate, Runtime}; use std::fmt::Write; use strum::IntoEnumIterator; use tar::Builder; -use tracing::trace; +use tracing::{trace, warn}; use uuid::Uuid; use crate::args::{DeploymentCommand, ProjectCommand}; @@ -477,6 +477,8 @@ impl Shuttle { let path = std::fs::canonicalize(format!("{MANIFEST_DIR}/../runtime")) .expect("path to shuttle-runtime does not exist or is invalid"); + trace!(?path, "installing runtime from local filesystem"); + // TODO: Add --features next here when https://github.com/shuttle-hq/shuttle/pull/688 is merged std::process::Command::new("cargo") .arg("install") @@ -494,7 +496,7 @@ impl Shuttle { // or it isn't installed, try to install shuttle-runtime from the production // branch. if let Err(err) = check_version(&runtime_path) { - trace!("{}", err); + warn!("{}", err); trace!("installing shuttle-runtime"); // TODO: Add --features next here when https://github.com/shuttle-hq/shuttle/pull/688 is merged @@ -516,6 +518,7 @@ impl Shuttle { runtime_path } else { + trace!(path = ?executable_path, "using alpha runtime"); executable_path.clone() } }; @@ -580,7 +583,6 @@ impl Shuttle { let addr = SocketAddr::new(addr, run_args.port); let start_request = StartRequest { - deployment_id: Uuid::default().as_bytes().to_vec(), ip: addr.to_string(), }; diff --git a/common/src/storage_manager.rs b/common/src/storage_manager.rs index 22ac50c4e..51a0fe892 100644 --- a/common/src/storage_manager.rs +++ b/common/src/storage_manager.rs @@ -6,12 +6,8 @@ pub trait StorageManager: Sync + Send { /// Path for a specific service build files fn service_build_path(&self, service_name: &str) -> Result; - /// Path to folder for storing deployment files - fn deployment_storage_path( - &self, - service_name: &str, - deployment_id: &Uuid, - ) -> Result; + /// Path to folder for storing service files + fn service_storage_path(&self, service_name: &str) -> Result; } /// Manager to take care of directories for storing project, services and deployment files for deployer @@ -65,15 +61,8 @@ impl StorageManager for ArtifactsStorageManager { Ok(builds_path) } - fn deployment_storage_path( - &self, - service_name: &str, - deployment_id: &Uuid, - ) -> Result { - let storage_path = self - .storage_path()? - .join(service_name) - .join(deployment_id.to_string()); + fn service_storage_path(&self, service_name: &str) -> Result { + let storage_path = self.storage_path()?.join(service_name); fs::create_dir_all(&storage_path)?; Ok(storage_path) @@ -97,11 +86,7 @@ impl StorageManager for WorkingDirStorageManager { Ok(self.working_dir.clone()) } - fn deployment_storage_path( - &self, - _service_name: &str, - _deployment_id: &Uuid, - ) -> Result { + fn service_storage_path(&self, _service_name: &str) -> Result { Ok(self.working_dir.clone()) } } diff --git a/deployer/src/deployment/deploy_layer.rs b/deployer/src/deployment/deploy_layer.rs index f756a56de..a46441950 100644 --- a/deployer/src/deployment/deploy_layer.rs +++ b/deployer/src/deployment/deploy_layer.rs @@ -117,10 +117,8 @@ impl TryFrom for Log { fn try_from(log: runtime::LogItem) -> Result { Ok(Self { - id: Uuid::from_slice(&log.id)?, - state: runtime::LogState::from_i32(log.state) - .unwrap_or_default() - .into(), + id: Default::default(), + state: State::Running, level: runtime::LogLevel::from_i32(log.level) .unwrap_or_default() .into(), @@ -134,22 +132,6 @@ impl TryFrom for Log { } } -impl From for State { - fn from(state: runtime::LogState) -> Self { - match state { - runtime::LogState::Queued => Self::Queued, - runtime::LogState::Building => Self::Building, - runtime::LogState::Built => Self::Built, - runtime::LogState::Loading => Self::Loading, - runtime::LogState::Running => Self::Running, - runtime::LogState::Completed => Self::Completed, - runtime::LogState::Stopped => Self::Stopped, - runtime::LogState::Crashed => Self::Crashed, - runtime::LogState::Unknown => Self::Unknown, - } - } -} - impl From for LogLevel { fn from(level: runtime::LogLevel) -> Self { match level { diff --git a/deployer/src/deployment/run.rs b/deployer/src/deployment/run.rs index b98d59416..766cfe923 100644 --- a/deployer/src/deployment/run.rs +++ b/deployer/src/deployment/run.rs @@ -308,7 +308,6 @@ async fn run( .expect("to set deployment address"); let start_request = tonic::Request::new(StartRequest { - deployment_id: id.as_bytes().to_vec(), ip: address.to_string(), }); diff --git a/deployer/src/runtime_manager.rs b/deployer/src/runtime_manager.rs index 5eaed5c6e..825a4ade5 100644 --- a/deployer/src/runtime_manager.rs +++ b/deployer/src/runtime_manager.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, convert::TryInto, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, path::PathBuf, sync::Arc}; use anyhow::Context; use shuttle_proto::runtime::{ @@ -13,11 +13,13 @@ use crate::deployment::deploy_layer; const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); +type Runtimes = Arc)>>>; + /// Manager that can start up mutliple runtimes. This is needed so that two runtimes can be up when a new deployment is made: /// One runtime for the new deployment being loaded; another for the currently active deployment #[derive(Clone)] pub struct RuntimeManager { - runtimes: Arc)>>>, + runtimes: Runtimes, artifacts_path: PathBuf, provisioner_address: String, log_sender: crossbeam_channel::Sender, @@ -30,7 +32,7 @@ impl RuntimeManager { log_sender: crossbeam_channel::Sender, ) -> Arc> { Arc::new(Mutex::new(Self { - runtimes: Arc::new(std::sync::Mutex::new(HashMap::new())), + runtimes: Default::default(), artifacts_path, provisioner_address, log_sender, @@ -111,7 +113,9 @@ impl RuntimeManager { tokio::spawn(async move { while let Ok(Some(log)) = stream.message().await { - if let Ok(log) = log.try_into() { + if let Ok(mut log) = deploy_layer::Log::try_from(log) { + log.id = id; + sender.send(log).expect("to send log to persistence"); } } diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 44e2d990c..eb633ae31 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -16,7 +16,6 @@ prost-types = { workspace = true } tokio = { version = "1.22.0", features = ["process"] } tonic = { workspace = true } tracing = { workspace = true } -uuid = { workspace = true, features = ["v4"] } [dependencies.shuttle-common] workspace = true diff --git a/proto/runtime.proto b/proto/runtime.proto index 8a0ee293d..57a4895df 100644 --- a/proto/runtime.proto +++ b/proto/runtime.proto @@ -39,10 +39,8 @@ message LoadResponse { } message StartRequest { - // Id to associate with the deployment being started - bytes deployment_id = 1; // Address and port to start the service on - string ip = 3; + string ip = 1; } message StartResponse { @@ -81,9 +79,7 @@ enum StopReason { message SubscribeLogsRequest {} message LogItem { - bytes id = 1; google.protobuf.Timestamp timestamp = 2; - LogState state = 3; LogLevel level = 4; optional string file = 5; optional uint32 line = 6; @@ -91,18 +87,6 @@ message LogItem { bytes fields = 8; } -enum LogState { - Queued = 0; - Building = 1; - Built = 2; - Loading = 3; - Running = 4; - Completed = 5; - Stopped = 6; - Crashed = 7; - Unknown = 50; -} - enum LogLevel { Trace = 0; Debug = 1; diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 39275985d..fd2e81caf 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -108,7 +108,6 @@ pub mod runtime { use tokio::process; use tonic::transport::{Channel, Endpoint}; use tracing::info; - use uuid::Uuid; pub enum StorageManagerType { Artifacts(PathBuf), @@ -117,37 +116,6 @@ pub mod runtime { tonic::include_proto!("runtime"); - impl From for LogItem { - fn from(log: shuttle_common::LogItem) -> Self { - Self { - id: log.id.into_bytes().to_vec(), - timestamp: Some(Timestamp::from(SystemTime::from(log.timestamp))), - state: LogState::from(log.state) as i32, - level: LogLevel::from(log.level) as i32, - file: log.file, - line: log.line, - target: log.target, - fields: log.fields, - } - } - } - - impl From for LogState { - fn from(state: shuttle_common::deployment::State) -> Self { - match state { - shuttle_common::deployment::State::Queued => Self::Queued, - shuttle_common::deployment::State::Building => Self::Building, - shuttle_common::deployment::State::Built => Self::Built, - shuttle_common::deployment::State::Loading => Self::Loading, - shuttle_common::deployment::State::Running => Self::Running, - shuttle_common::deployment::State::Completed => Self::Completed, - shuttle_common::deployment::State::Stopped => Self::Stopped, - shuttle_common::deployment::State::Crashed => Self::Crashed, - shuttle_common::deployment::State::Unknown => Self::Unknown, - } - } - } - impl From for LogLevel { fn from(level: shuttle_common::log::Level) -> Self { match level { @@ -165,9 +133,9 @@ pub mod runtime { fn try_from(log: LogItem) -> Result { Ok(Self { - id: Uuid::from_slice(&log.id)?, + id: Default::default(), timestamp: DateTime::from(SystemTime::try_from(log.timestamp.unwrap_or_default())?), - state: LogState::from_i32(log.state).unwrap_or_default().into(), + state: shuttle_common::deployment::State::Running, level: LogLevel::from_i32(log.level).unwrap_or_default().into(), file: log.file, line: log.line, @@ -177,22 +145,6 @@ pub mod runtime { } } - impl From for shuttle_common::deployment::State { - fn from(state: LogState) -> Self { - match state { - LogState::Queued => Self::Queued, - LogState::Building => Self::Building, - LogState::Built => Self::Built, - LogState::Loading => Self::Loading, - LogState::Running => Self::Running, - LogState::Completed => Self::Completed, - LogState::Stopped => Self::Stopped, - LogState::Crashed => Self::Crashed, - LogState::Unknown => Self::Unknown, - } - } - } - impl From for shuttle_common::log::Level { fn from(level: LogLevel) -> Self { match level { @@ -216,9 +168,7 @@ pub mod runtime { let line = if log.line == 0 { None } else { Some(log.line) }; Self { - id: Default::default(), timestamp: Some(Timestamp::from(SystemTime::from(log.timestamp))), - state: LogState::Running as i32, level: LogLevel::from(log.level) as i32, file, line, @@ -240,6 +190,18 @@ pub mod runtime { } } + impl From<&tracing::Level> for LogLevel { + fn from(level: &tracing::Level) -> Self { + match *level { + tracing::Level::TRACE => Self::Trace, + tracing::Level::DEBUG => Self::Debug, + tracing::Level::INFO => Self::Info, + tracing::Level::WARN => Self::Warn, + tracing::Level::ERROR => Self::Error, + } + } + } + pub async fn start( wasm: bool, storage_manager_type: StorageManagerType, diff --git a/provisioner/tests/provisioner.rs b/provisioner/tests/provisioner.rs index 5b2a7a912..3c9d103d1 100644 --- a/provisioner/tests/provisioner.rs +++ b/provisioner/tests/provisioner.rs @@ -168,7 +168,7 @@ async fn shared_mongodb_role_does_not_exist() { .unwrap(); let user = exec_mongosh("db.getUser(\"user-not_exist\")", Some("mongodb-not_exist")); - assert_eq!(user, "null"); + assert_eq!(user, ""); provisioner .request_shared_db("not_exist", shared::Engine::Mongodb(String::new())) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 2035f34a8..632c5ee16 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -19,6 +19,7 @@ anyhow = { workspace = true } async-trait = { workspace = true } chrono = { workspace = true } clap = { workspace = true } +prost-types = { workspace = true } serde_json = { workspace = true } strfmt = "0.2.2" thiserror = { workspace = true } @@ -28,7 +29,6 @@ tonic = { workspace = true } tower = { workspace = true } tracing = { workspace = true, features = ["default"] } tracing-subscriber = { workspace = true, features = ["default", "env-filter", "fmt"] } -uuid = { workspace = true, features = ["v4"] } # TODO: bump these crates to 6.0 when we bump rust to >= 1.66 cap-std = { version = "1.0.2", optional = true } diff --git a/runtime/src/alpha/mod.rs b/runtime/src/alpha/mod.rs index 3ff48029f..a00054092 100644 --- a/runtime/src/alpha/mod.rs +++ b/runtime/src/alpha/mod.rs @@ -15,14 +15,13 @@ use core::future::Future; use shuttle_common::{ claims::{ClaimLayer, InjectPropagationLayer}, storage_manager::{ArtifactsStorageManager, StorageManager, WorkingDirStorageManager}, - LogItem, }; use shuttle_proto::{ provisioner::provisioner_client::ProvisionerClient, runtime::{ self, runtime_server::{Runtime, RuntimeServer}, - LoadRequest, LoadResponse, StartRequest, StartResponse, StopReason, StopRequest, + LoadRequest, LoadResponse, LogItem, StartRequest, StartResponse, StopReason, StopRequest, StopResponse, SubscribeLogsRequest, SubscribeStopRequest, SubscribeStopResponse, }, }; @@ -39,7 +38,6 @@ use tonic::{ }; use tower::ServiceBuilder; use tracing::{error, info, trace}; -use uuid::Uuid; use crate::{provisioner_factory::ProvisionerFactory, Logger}; @@ -186,12 +184,9 @@ where let service_name = ServiceName::from_str(service_name.as_str()) .map_err(|err| Status::from_error(Box::new(err)))?; - let deployment_id = Uuid::new_v4(); - let factory = ProvisionerFactory::new( provisioner_client, service_name, - deployment_id, secrets, self.storage_manager.clone(), self.env, @@ -199,7 +194,7 @@ where trace!("got factory"); let logs_tx = self.logs_tx.clone(); - let logger = Logger::new(logs_tx, deployment_id); + let logger = Logger::new(logs_tx); let loader = self.loader.lock().unwrap().deref_mut().take().unwrap(); @@ -380,7 +375,7 @@ where // Move logger items into stream to be returned tokio::spawn(async move { while let Some(log) = logs_rx.recv().await { - tx.send(Ok(log.into())).await.expect("to send log"); + tx.send(Ok(log)).await.expect("to send log"); } }); diff --git a/runtime/src/logger.rs b/runtime/src/logger.rs index 8a82c2508..b3608e301 100644 --- a/runtime/src/logger.rs +++ b/runtime/src/logger.rs @@ -1,17 +1,20 @@ +use std::time::SystemTime; + use chrono::Utc; -use shuttle_common::{deployment::State, tracing::JsonVisitor, DeploymentId, LogItem}; +use prost_types::Timestamp; +use shuttle_common::tracing::JsonVisitor; +use shuttle_proto::runtime::{LogItem, LogLevel}; use tokio::sync::mpsc::UnboundedSender; use tracing::Subscriber; use tracing_subscriber::Layer; pub struct Logger { - deployment_id: DeploymentId, tx: UnboundedSender, } impl Logger { - pub fn new(tx: UnboundedSender, deployment_id: DeploymentId) -> Self { - Self { tx, deployment_id } + pub fn new(tx: UnboundedSender) -> Self { + Self { tx } } } @@ -33,10 +36,8 @@ where event.record(&mut visitor); LogItem { - id: self.deployment_id, - state: State::Running, - level: metadata.level().into(), - timestamp: datetime, + level: LogLevel::from(metadata.level()) as i32, + timestamp: Some(Timestamp::from(SystemTime::from(datetime))), file: visitor.file.or_else(|| metadata.file().map(str::to_string)), line: visitor.line.or_else(|| metadata.line()), target: visitor @@ -54,7 +55,6 @@ where mod tests { use super::*; - use shuttle_common::log::Level; use tokio::sync::mpsc; use tracing_subscriber::prelude::*; @@ -62,7 +62,7 @@ mod tests { fn logging() { let (s, mut r) = mpsc::unbounded_channel(); - let logger = Logger::new(s, Default::default()); + let logger = Logger::new(s); let _guard = tracing_subscriber::registry().with(logger).set_default(); @@ -73,23 +73,23 @@ mod tests { assert_eq!( r.blocking_recv().map(to_tuple), - Some(("this is".to_string(), Level::Debug)) + Some(("this is".to_string(), LogLevel::Debug as i32)) ); assert_eq!( r.blocking_recv().map(to_tuple), - Some(("hi".to_string(), Level::Info)) + Some(("hi".to_string(), LogLevel::Info as i32)) ); assert_eq!( r.blocking_recv().map(to_tuple), - Some(("from".to_string(), Level::Warn)) + Some(("from".to_string(), LogLevel::Warn as i32)) ); assert_eq!( r.blocking_recv().map(to_tuple), - Some(("logger".to_string(), Level::Error)) + Some(("logger".to_string(), LogLevel::Error as i32)) ); } - fn to_tuple(log: LogItem) -> (String, Level) { + fn to_tuple(log: LogItem) -> (String, i32) { let fields: serde_json::Map = serde_json::from_slice(&log.fields).unwrap(); diff --git a/runtime/src/next/mod.rs b/runtime/src/next/mod.rs index 060c7b4b9..8b4152b51 100644 --- a/runtime/src/next/mod.rs +++ b/runtime/src/next/mod.rs @@ -101,7 +101,7 @@ impl Runtime for AxumWasm { &self, request: tonic::Request, ) -> Result, Status> { - let StartRequest { deployment_id, ip } = request.into_inner(); + let StartRequest { ip } = request.into_inner(); let address = SocketAddr::from_str(&ip) .context("invalid socket address") @@ -121,13 +121,7 @@ impl Runtime for AxumWasm { .context("tried to start a service that was not loaded") .map_err(|err| Status::internal(err.to_string()))?; - tokio::spawn(run_until_stopped( - router, - deployment_id, - address, - logs_tx, - kill_rx, - )); + tokio::spawn(run_until_stopped(router, address, logs_tx, kill_rx)); let message = StartResponse { success: true }; @@ -235,7 +229,6 @@ impl Router { /// Send a HTTP request with body to given endpoint on the axum-wasm router and return the response async fn handle_request( &mut self, - deployment_id: Vec, req: hyper::Request, logs_tx: Sender>, ) -> anyhow::Result> { @@ -274,10 +267,7 @@ impl Router { let mut iter = logs_stream.bytes().filter_map(Result::ok); while let Some(log) = Log::from_bytes(&mut iter) { - let mut log: runtime::LogItem = log.into(); - log.id = deployment_id.clone(); - - logs_tx.blocking_send(Ok(log)).expect("to send log"); + logs_tx.blocking_send(Ok(log.into())).expect("to send log"); } }); @@ -360,33 +350,28 @@ impl Router { /// and a kill receiver for stopping the server. async fn run_until_stopped( router: Router, - deployment_id: Vec, address: SocketAddr, logs_tx: Sender>, kill_rx: tokio::sync::oneshot::Receiver, ) { let make_service = make_service_fn(move |_conn| { - let deployment_id = deployment_id.clone(); let router = router.clone(); let logs_tx = logs_tx.clone(); async move { Ok::<_, Infallible>(service_fn(move |req: Request| { - let deployment_id = deployment_id.clone(); let mut router = router.clone(); let logs_tx = logs_tx.clone(); async move { - Ok::<_, Infallible>( - match router.handle_request(deployment_id, req, logs_tx).await { - Ok(res) => res, - Err(err) => { - error!("error sending request: {}", err); - Response::builder() - .status(hyper::http::StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .expect("building request with empty body should not fail") - } - }, - ) + Ok::<_, Infallible>(match router.handle_request(req, logs_tx).await { + Ok(res) => res, + Err(err) => { + error!("error sending request: {}", err); + Response::builder() + .status(hyper::http::StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .expect("building request with empty body should not fail") + } + }) } })) } @@ -414,7 +399,6 @@ pub mod tests { use super::*; use hyper::{http::HeaderValue, Method, Request, StatusCode, Version}; - use uuid::Uuid; // Compile axum wasm module fn compile_module() { @@ -439,7 +423,6 @@ pub mod tests { .build() .unwrap(); - let id = Uuid::default().as_bytes().to_vec(); let (tx, mut rx) = mpsc::channel(1); tokio::spawn(async move { @@ -458,7 +441,7 @@ pub mod tests { let res = router .clone() - .handle_request(id.clone(), request, tx.clone()) + .handle_request(request, tx.clone()) .await .unwrap(); @@ -485,7 +468,7 @@ pub mod tests { let res = router .clone() - .handle_request(id.clone(), request, tx.clone()) + .handle_request(request, tx.clone()) .await .unwrap(); @@ -512,7 +495,7 @@ pub mod tests { let res = router .clone() - .handle_request(id.clone(), request, tx.clone()) + .handle_request(request, tx.clone()) .await .unwrap(); @@ -527,11 +510,7 @@ pub mod tests { .body("this should be uppercased".into()) .unwrap(); - let res = router - .clone() - .handle_request(id, request, tx) - .await - .unwrap(); + let res = router.clone().handle_request(request, tx).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); assert_eq!( diff --git a/runtime/src/provisioner_factory.rs b/runtime/src/provisioner_factory.rs index 56d91e94c..a2ccba6b3 100644 --- a/runtime/src/provisioner_factory.rs +++ b/runtime/src/provisioner_factory.rs @@ -13,12 +13,10 @@ use shuttle_proto::provisioner::{ use shuttle_service::{Environment, Factory, ServiceName}; use tonic::{transport::Channel, Request}; use tracing::{debug, info, trace}; -use uuid::Uuid; /// A factory (service locator) which goes through the provisioner crate pub struct ProvisionerFactory { service_name: ServiceName, - deployment_id: Uuid, storage_manager: Arc, provisioner_client: ProvisionerClient>>, info: Option, @@ -30,7 +28,6 @@ impl ProvisionerFactory { pub(crate) fn new( provisioner_client: ProvisionerClient>>, service_name: ServiceName, - deployment_id: Uuid, secrets: BTreeMap, storage_manager: Arc, env: Environment, @@ -38,7 +35,6 @@ impl ProvisionerFactory { Self { provisioner_client, service_name, - deployment_id, storage_manager, info: None, secrets, @@ -100,7 +96,7 @@ impl Factory for ProvisionerFactory { fn get_storage_path(&self) -> Result { self.storage_manager - .deployment_storage_path(self.service_name.as_str(), &self.deployment_id) + .service_storage_path(self.service_name.as_str()) .map_err(Into::into) } diff --git a/runtime/tests/integration/loader.rs b/runtime/tests/integration/loader.rs index 3ef17f059..fb62d6878 100644 --- a/runtime/tests/integration/loader.rs +++ b/runtime/tests/integration/loader.rs @@ -1,5 +1,4 @@ use shuttle_proto::runtime::{LoadRequest, StartRequest, StopReason, SubscribeStopRequest}; -use uuid::Uuid; use crate::helpers::{spawn_runtime, TestRuntime}; @@ -30,7 +29,6 @@ async fn bind_panic() { .into_inner(); let start_request = StartRequest { - deployment_id: Uuid::default().as_bytes().to_vec(), ip: runtime_address.to_string(), }; diff --git a/shell.nix b/shell.nix index 2a53950a8..53928fbe1 100644 --- a/shell.nix +++ b/shell.nix @@ -29,6 +29,7 @@ in sqlite fastmod pebble + kondo ]; PROTOC = "${protobuf}/bin/protoc";