feat: add grpc server
This commit is contained in:
parent
e10a40dc6b
commit
b194af0453
866
Cargo.lock
generated
866
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,12 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
norun = { path = "./crates/norun" }
|
||||||
|
norun-grpc-interface = { path = "./crates/norun-grpc-interface" }
|
||||||
|
|
||||||
|
bytes = "1.10.1"
|
||||||
|
prost = "0.13.5"
|
||||||
|
prost-types = "0.13.5"
|
||||||
|
tonic = "0.12.1"
|
||||||
|
10
buf.gen.yaml
Normal file
10
buf.gen.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
version: v2
|
||||||
|
managed:
|
||||||
|
enabled: true
|
||||||
|
plugins:
|
||||||
|
- remote: buf.build/community/neoeinstein-prost:v0.4.0
|
||||||
|
out: ./crates/norun-grpc-interface/src/grpc/
|
||||||
|
- remote: buf.build/community/neoeinstein-tonic:v0.4.0
|
||||||
|
out: ./crates/norun-grpc-interface/src/grpc/
|
||||||
|
inputs:
|
||||||
|
- directory: ./interface/proto
|
4
buf.yaml
Normal file
4
buf.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
version: v2
|
||||||
|
modules:
|
||||||
|
- path: interface/proto
|
||||||
|
name: buf.build/noschemaplz/norun
|
11
crates/norun-grpc-interface/Cargo.toml
Normal file
11
crates/norun-grpc-interface/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "norun-grpc-interface"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bytes = { workspace = true }
|
||||||
|
prost = { workspace = true }
|
||||||
|
prost-types = { workspace = true }
|
||||||
|
tokio-util = "0.7.15"
|
||||||
|
tonic = { workspace = true }
|
26
crates/norun-grpc-interface/src/grpc/norun.v1.rs
Normal file
26
crates/norun-grpc-interface/src/grpc/norun.v1.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// @generated
|
||||||
|
// This file is @generated by prost-build.
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct PublishRequest {
|
||||||
|
#[prost(message, optional, tag="1")]
|
||||||
|
pub project: ::core::option::Option<Project>,
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||||
|
pub struct PublishResponse {
|
||||||
|
}
|
||||||
|
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct Project {
|
||||||
|
#[prost(string, tag="1")]
|
||||||
|
pub name: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag="2")]
|
||||||
|
pub image: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag="3")]
|
||||||
|
pub version: ::prost::alloc::string::String,
|
||||||
|
#[prost(uint32, optional, tag="4")]
|
||||||
|
pub port: ::core::option::Option<u32>,
|
||||||
|
}
|
||||||
|
include!("norun.v1.tonic.rs");
|
||||||
|
// @@protoc_insertion_point(module)
|
291
crates/norun-grpc-interface/src/grpc/norun.v1.tonic.rs
Normal file
291
crates/norun-grpc-interface/src/grpc/norun.v1.tonic.rs
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
// @generated
|
||||||
|
/// Generated client implementations.
|
||||||
|
pub mod registry_service_client {
|
||||||
|
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
|
||||||
|
use tonic::codegen::*;
|
||||||
|
use tonic::codegen::http::Uri;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RegistryServiceClient<T> {
|
||||||
|
inner: tonic::client::Grpc<T>,
|
||||||
|
}
|
||||||
|
impl RegistryServiceClient<tonic::transport::Channel> {
|
||||||
|
/// Attempt to create a new client by connecting to a given endpoint.
|
||||||
|
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||||
|
where
|
||||||
|
D: TryInto<tonic::transport::Endpoint>,
|
||||||
|
D::Error: Into<StdError>,
|
||||||
|
{
|
||||||
|
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||||
|
Ok(Self::new(conn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> RegistryServiceClient<T>
|
||||||
|
where
|
||||||
|
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||||
|
T::Error: Into<StdError>,
|
||||||
|
T::ResponseBody: Body<Data = Bytes> + Send + 'static,
|
||||||
|
<T::ResponseBody as Body>::Error: Into<StdError> + Send,
|
||||||
|
{
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
let inner = tonic::client::Grpc::new(inner);
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||||
|
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
pub fn with_interceptor<F>(
|
||||||
|
inner: T,
|
||||||
|
interceptor: F,
|
||||||
|
) -> RegistryServiceClient<InterceptedService<T, F>>
|
||||||
|
where
|
||||||
|
F: tonic::service::Interceptor,
|
||||||
|
T::ResponseBody: Default,
|
||||||
|
T: tonic::codegen::Service<
|
||||||
|
http::Request<tonic::body::BoxBody>,
|
||||||
|
Response = http::Response<
|
||||||
|
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
<T as tonic::codegen::Service<
|
||||||
|
http::Request<tonic::body::BoxBody>,
|
||||||
|
>>::Error: Into<StdError> + Send + Sync,
|
||||||
|
{
|
||||||
|
RegistryServiceClient::new(InterceptedService::new(inner, interceptor))
|
||||||
|
}
|
||||||
|
/// Compress requests with the given encoding.
|
||||||
|
///
|
||||||
|
/// This requires the server to support it otherwise it might respond with an
|
||||||
|
/// error.
|
||||||
|
#[must_use]
|
||||||
|
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.inner = self.inner.send_compressed(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Enable decompressing responses.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.inner = self.inner.accept_compressed(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of a decoded message.
|
||||||
|
///
|
||||||
|
/// Default: `4MB`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.inner = self.inner.max_decoding_message_size(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of an encoded message.
|
||||||
|
///
|
||||||
|
/// Default: `usize::MAX`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.inner = self.inner.max_encoding_message_size(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub async fn publish(
|
||||||
|
&mut self,
|
||||||
|
request: impl tonic::IntoRequest<super::PublishRequest>,
|
||||||
|
) -> std::result::Result<
|
||||||
|
tonic::Response<super::PublishResponse>,
|
||||||
|
tonic::Status,
|
||||||
|
> {
|
||||||
|
self.inner
|
||||||
|
.ready()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tonic::Status::new(
|
||||||
|
tonic::Code::Unknown,
|
||||||
|
format!("Service was not ready: {}", e.into()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
|
"/norun.v1.RegistryService/Publish",
|
||||||
|
);
|
||||||
|
let mut req = request.into_request();
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(GrpcMethod::new("norun.v1.RegistryService", "Publish"));
|
||||||
|
self.inner.unary(req, path, codec).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Generated server implementations.
|
||||||
|
pub mod registry_service_server {
|
||||||
|
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
|
||||||
|
use tonic::codegen::*;
|
||||||
|
/// Generated trait containing gRPC methods that should be implemented for use with RegistryServiceServer.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RegistryService: Send + Sync + 'static {
|
||||||
|
async fn publish(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<super::PublishRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<super::PublishResponse>, tonic::Status>;
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RegistryServiceServer<T: RegistryService> {
|
||||||
|
inner: _Inner<T>,
|
||||||
|
accept_compression_encodings: EnabledCompressionEncodings,
|
||||||
|
send_compression_encodings: EnabledCompressionEncodings,
|
||||||
|
max_decoding_message_size: Option<usize>,
|
||||||
|
max_encoding_message_size: Option<usize>,
|
||||||
|
}
|
||||||
|
struct _Inner<T>(Arc<T>);
|
||||||
|
impl<T: RegistryService> RegistryServiceServer<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self::from_arc(Arc::new(inner))
|
||||||
|
}
|
||||||
|
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||||
|
let inner = _Inner(inner);
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
accept_compression_encodings: Default::default(),
|
||||||
|
send_compression_encodings: Default::default(),
|
||||||
|
max_decoding_message_size: None,
|
||||||
|
max_encoding_message_size: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_interceptor<F>(
|
||||||
|
inner: T,
|
||||||
|
interceptor: F,
|
||||||
|
) -> InterceptedService<Self, F>
|
||||||
|
where
|
||||||
|
F: tonic::service::Interceptor,
|
||||||
|
{
|
||||||
|
InterceptedService::new(Self::new(inner), interceptor)
|
||||||
|
}
|
||||||
|
/// Enable decompressing requests with the given encoding.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.accept_compression_encodings.enable(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Compress responses with the given encoding, if the client supports it.
|
||||||
|
#[must_use]
|
||||||
|
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.send_compression_encodings.enable(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of a decoded message.
|
||||||
|
///
|
||||||
|
/// Default: `4MB`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.max_decoding_message_size = Some(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of an encoded message.
|
||||||
|
///
|
||||||
|
/// Default: `usize::MAX`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.max_encoding_message_size = Some(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T, B> tonic::codegen::Service<http::Request<B>> for RegistryServiceServer<T>
|
||||||
|
where
|
||||||
|
T: RegistryService,
|
||||||
|
B: Body + Send + 'static,
|
||||||
|
B::Error: Into<StdError> + Send + 'static,
|
||||||
|
{
|
||||||
|
type Response = http::Response<tonic::body::BoxBody>;
|
||||||
|
type Error = std::convert::Infallible;
|
||||||
|
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||||
|
fn poll_ready(
|
||||||
|
&mut self,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
match req.uri().path() {
|
||||||
|
"/norun.v1.RegistryService/Publish" => {
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
struct PublishSvc<T: RegistryService>(pub Arc<T>);
|
||||||
|
impl<
|
||||||
|
T: RegistryService,
|
||||||
|
> tonic::server::UnaryService<super::PublishRequest>
|
||||||
|
for PublishSvc<T> {
|
||||||
|
type Response = super::PublishResponse;
|
||||||
|
type Future = BoxFuture<
|
||||||
|
tonic::Response<Self::Response>,
|
||||||
|
tonic::Status,
|
||||||
|
>;
|
||||||
|
fn call(
|
||||||
|
&mut self,
|
||||||
|
request: tonic::Request<super::PublishRequest>,
|
||||||
|
) -> Self::Future {
|
||||||
|
let inner = Arc::clone(&self.0);
|
||||||
|
let fut = async move {
|
||||||
|
<T as RegistryService>::publish(&inner, request).await
|
||||||
|
};
|
||||||
|
Box::pin(fut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let accept_compression_encodings = self.accept_compression_encodings;
|
||||||
|
let send_compression_encodings = self.send_compression_encodings;
|
||||||
|
let max_decoding_message_size = self.max_decoding_message_size;
|
||||||
|
let max_encoding_message_size = self.max_encoding_message_size;
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
let fut = async move {
|
||||||
|
let inner = inner.0;
|
||||||
|
let method = PublishSvc(inner);
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let mut grpc = tonic::server::Grpc::new(codec)
|
||||||
|
.apply_compression_config(
|
||||||
|
accept_compression_encodings,
|
||||||
|
send_compression_encodings,
|
||||||
|
)
|
||||||
|
.apply_max_message_size_config(
|
||||||
|
max_decoding_message_size,
|
||||||
|
max_encoding_message_size,
|
||||||
|
);
|
||||||
|
let res = grpc.unary(method, req).await;
|
||||||
|
Ok(res)
|
||||||
|
};
|
||||||
|
Box::pin(fut)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Box::pin(async move {
|
||||||
|
Ok(
|
||||||
|
http::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.header("grpc-status", "12")
|
||||||
|
.header("content-type", "application/grpc")
|
||||||
|
.body(empty_body())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: RegistryService> Clone for RegistryServiceServer<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
accept_compression_encodings: self.accept_compression_encodings,
|
||||||
|
send_compression_encodings: self.send_compression_encodings,
|
||||||
|
max_decoding_message_size: self.max_decoding_message_size,
|
||||||
|
max_encoding_message_size: self.max_encoding_message_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: RegistryService> Clone for _Inner<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(Arc::clone(&self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: std::fmt::Debug> std::fmt::Debug for _Inner<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{:?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: RegistryService> tonic::server::NamedService for RegistryServiceServer<T> {
|
||||||
|
const NAME: &'static str = "norun.v1.RegistryService";
|
||||||
|
}
|
||||||
|
}
|
5
crates/norun-grpc-interface/src/lib.rs
Normal file
5
crates/norun-grpc-interface/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod grpc {
|
||||||
|
include!("./grpc/norun.v1.rs");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use grpc::*;
|
@ -4,8 +4,23 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
norun-grpc-interface.workspace = true
|
||||||
|
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
clap = { version = "4.5.40", features = ["derive", "env"] }
|
clap = { version = "4.5.40", features = ["derive", "env"] }
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
tokio = { version = "1.46.1", features = ["full"] }
|
tokio = { version = "1.46.1", features = ["full"] }
|
||||||
|
toml = "0.8.23"
|
||||||
tracing = { version = "0.1.41", features = ["log"] }
|
tracing = { version = "0.1.41", features = ["log"] }
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
|
|
||||||
|
bytes = { workspace = true }
|
||||||
|
prost = { workspace = true }
|
||||||
|
prost-types = { workspace = true }
|
||||||
|
tonic = { workspace = true }
|
||||||
|
tokio-util = "0.7.15"
|
||||||
|
async-trait = "0.1.88"
|
||||||
|
notmad = "0.7.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1.4.1"
|
||||||
|
@ -1,29 +1,40 @@
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
use crate::cli::publish::PublishCommand;
|
use crate::{
|
||||||
|
cli::{publish::PublishCommand, serve::ServeCommand},
|
||||||
|
state::ClientState,
|
||||||
|
};
|
||||||
|
|
||||||
mod publish;
|
mod publish;
|
||||||
|
mod serve;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about)]
|
#[command(author, version, about)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
subcommands: CliSubcommands,
|
subcommands: CliSubcommands,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long = "server-url",
|
||||||
|
env = "NORUN_SERVER_URL",
|
||||||
|
default_value = "http://localhost:4242"
|
||||||
|
)]
|
||||||
|
server_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum CliSubcommands {
|
enum CliSubcommands {
|
||||||
Publish(PublishCommand),
|
Publish(PublishCommand),
|
||||||
|
Serve(ServeCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute() -> anyhow::Result<()> {
|
pub async fn execute() -> anyhow::Result<()> {
|
||||||
let cmd = Cli::parse();
|
let cmd = Cli::parse();
|
||||||
|
|
||||||
let state = State::new();
|
let state = ClientState::new(&cmd.server_url);
|
||||||
|
|
||||||
match cmd.subcommands {
|
match cmd.subcommands {
|
||||||
CliSubcommands::Publish(cmd) => cmd.execute(&state),
|
CliSubcommands::Publish(cmd) => cmd.execute(&state).await,
|
||||||
|
CliSubcommands::Serve(cmd) => cmd.execute().await,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,26 @@
|
|||||||
#[derive(clap::Parser)]
|
use std::path::PathBuf;
|
||||||
pub struct PublishCommand {}
|
|
||||||
|
use crate::{grpc_client::GrpcClientState, models::ProjectTag, project_file, state::ClientState};
|
||||||
|
|
||||||
|
#[derive(clap::Parser, Debug)]
|
||||||
|
pub struct PublishCommand {
|
||||||
|
#[arg(value_parser = clap::value_parser!(ProjectTag))]
|
||||||
|
project_tag: ProjectTag,
|
||||||
|
|
||||||
|
#[arg(long = "project-path", default_value = ".")]
|
||||||
|
project_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
impl PublishCommand {
|
impl PublishCommand {
|
||||||
pub async fn execute(&self, state: &State) -> anyhow::Result<()> {
|
#[tracing::instrument(skip(state), level = "trace")]
|
||||||
|
pub async fn execute(&self, state: &ClientState) -> anyhow::Result<()> {
|
||||||
|
tracing::debug!("running norun publish");
|
||||||
|
|
||||||
|
let project_file = project_file::parse_file(&self.project_path).await?;
|
||||||
|
|
||||||
|
// FIXME: 2. Load resources
|
||||||
|
state.grpc_client().publish(&project_file).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
crates/norun/src/cli/serve.rs
Normal file
31
crates/norun/src/cli/serve.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use notmad::Mad;
|
||||||
|
|
||||||
|
use crate::{grpc_server::GrpcServer, state::ServerState};
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
pub struct ServeCommand {
|
||||||
|
#[arg(
|
||||||
|
long = "grpc-host",
|
||||||
|
env = "NORUN_GRPC_HOST",
|
||||||
|
default_value = "127.0.0.1:4242"
|
||||||
|
)]
|
||||||
|
grpc_host: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServeCommand {
|
||||||
|
pub async fn execute(&self) -> anyhow::Result<()> {
|
||||||
|
let state = ServerState {};
|
||||||
|
|
||||||
|
Mad::builder()
|
||||||
|
.add(GrpcServer {
|
||||||
|
host: self.grpc_host,
|
||||||
|
state: state.clone(),
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
72
crates/norun/src/grpc_client.rs
Normal file
72
crates/norun/src/grpc_client.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
use norun_grpc_interface::{PublishRequest, registry_service_client::RegistryServiceClient};
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
use crate::{project_file::ProjectFile, state::ClientState};
|
||||||
|
|
||||||
|
pub struct GrpcClient {
|
||||||
|
url: String,
|
||||||
|
registry_client: OnceCell<RegistryServiceClient<Channel>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrpcClient {
|
||||||
|
#[tracing::instrument(skip(self), level = "trace")]
|
||||||
|
pub async fn publish(&self, project_file: &ProjectFile) -> anyhow::Result<()> {
|
||||||
|
tracing::trace!("calling publish via. grpc on registry");
|
||||||
|
|
||||||
|
let mut registry_client = self.get_registry_client().await?;
|
||||||
|
|
||||||
|
let res = registry_client
|
||||||
|
.publish(PublishRequest {
|
||||||
|
project: Some(project_file.into()),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let _res = res.into_inner();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_registry_client(&self) -> anyhow::Result<RegistryServiceClient<Channel>> {
|
||||||
|
let client = self
|
||||||
|
.registry_client
|
||||||
|
.get_or_try_init(move || async move {
|
||||||
|
let channel = Channel::from_shared(self.url.clone())?.connect().await?;
|
||||||
|
let client = RegistryServiceClient::new(channel);
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(client)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(client.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ProjectFile> for norun_grpc_interface::Project {
|
||||||
|
fn from(value: &ProjectFile) -> Self {
|
||||||
|
value.clone().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProjectFile> for norun_grpc_interface::Project {
|
||||||
|
fn from(value: ProjectFile) -> Self {
|
||||||
|
Self {
|
||||||
|
name: value.project.name,
|
||||||
|
image: value.container.image,
|
||||||
|
version: value.container.version,
|
||||||
|
port: value.expose.and_then(|e| e.port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait GrpcClientState {
|
||||||
|
fn grpc_client(&self) -> GrpcClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrpcClientState for ClientState {
|
||||||
|
fn grpc_client(&self) -> GrpcClient {
|
||||||
|
GrpcClient {
|
||||||
|
url: self.norun_server_url.clone(),
|
||||||
|
registry_client: OnceCell::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
crates/norun/src/grpc_server.rs
Normal file
47
crates/norun/src/grpc_server.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use norun_grpc_interface::registry_service_server::RegistryServiceServer;
|
||||||
|
use notmad::MadError;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use crate::{grpc_server::registry::GrpcRegistryService, state::ServerState};
|
||||||
|
|
||||||
|
mod registry;
|
||||||
|
|
||||||
|
pub struct GrpcServer {
|
||||||
|
pub host: SocketAddr,
|
||||||
|
pub state: ServerState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrpcServer {
|
||||||
|
pub async fn serve(&self, cancellation_token: CancellationToken) -> anyhow::Result<()> {
|
||||||
|
tracing::info!("serving grpc on {}", self.host);
|
||||||
|
|
||||||
|
tonic::transport::Server::builder()
|
||||||
|
.add_service(RegistryServiceServer::new(GrpcRegistryService {
|
||||||
|
state: self.state.clone(),
|
||||||
|
}))
|
||||||
|
.serve_with_shutdown(
|
||||||
|
self.host,
|
||||||
|
async move { cancellation_token.cancelled().await },
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl notmad::Component for GrpcServer {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some("norun/gprc-server".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||||
|
self.serve(cancellation_token)
|
||||||
|
.await
|
||||||
|
.map_err(MadError::Inner)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
33
crates/norun/src/grpc_server/registry.rs
Normal file
33
crates/norun/src/grpc_server/registry.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use norun_grpc_interface::{registry_service_server::RegistryService, *};
|
||||||
|
|
||||||
|
use crate::{server::services::registry::RegistryServiceState, state::ServerState};
|
||||||
|
|
||||||
|
pub struct GrpcRegistryService {
|
||||||
|
pub state: ServerState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl RegistryService for GrpcRegistryService {
|
||||||
|
#[tracing::instrument(skip(self), level = "debug")]
|
||||||
|
async fn publish(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<PublishRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<PublishResponse>, tonic::Status> {
|
||||||
|
tracing::debug!("publish called");
|
||||||
|
|
||||||
|
let req = request.into_inner();
|
||||||
|
|
||||||
|
let project = req
|
||||||
|
.project
|
||||||
|
.ok_or(tonic::Status::invalid_argument("a project is required"))?;
|
||||||
|
|
||||||
|
self.state
|
||||||
|
.registry_service()
|
||||||
|
.store(&project)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| tracing::warn!("failed to handle storage of registry item: {}", e))
|
||||||
|
.map_err(|e| tonic::Status::internal(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(tonic::Response::new(PublishResponse {}))
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,14 @@
|
|||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
mod models;
|
||||||
|
mod project_file;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
mod grpc_client;
|
||||||
|
mod grpc_server;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
2
crates/norun/src/models.rs
Normal file
2
crates/norun/src/models.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod project_tag;
|
||||||
|
pub use project_tag::*;
|
110
crates/norun/src/models/project_tag.rs
Normal file
110
crates/norun/src/models/project_tag.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
// ProjectTag can be a destination@name:version
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct ProjectTag {
|
||||||
|
pub destination: String,
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ProjectTag {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (destination, rest) = s.split_once('@').ok_or(anyhow::anyhow!("failed to find an '@' in your project tag, make sure it looks like 'destination@name:version'"))?;
|
||||||
|
if destination.is_empty() {
|
||||||
|
anyhow::bail!("destination is required like so 'destination@name:version'");
|
||||||
|
}
|
||||||
|
|
||||||
|
let (name, version) = rest.split_once(':').ok_or(anyhow::anyhow!("failed to find an ':' in your project tag, make sure it looks like 'destination@name:version'"))?;
|
||||||
|
if name.is_empty() {
|
||||||
|
anyhow::bail!("name is required like so 'destination@name:version'");
|
||||||
|
}
|
||||||
|
if version.is_empty() {
|
||||||
|
anyhow::bail!("version is required like so 'destination@name:version'");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
destination: destination.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
version: version.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::models::ProjectTag;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn happy_path() -> anyhow::Result<()> {
|
||||||
|
let raw = "destination@name:version";
|
||||||
|
let expected = ProjectTag {
|
||||||
|
destination: "destination".to_string(),
|
||||||
|
name: "name".to_string(),
|
||||||
|
version: "version".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let actual = ProjectTag::from_str(raw)?;
|
||||||
|
|
||||||
|
pretty_assertions::assert_eq!(expected, actual);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_destination_fails() -> anyhow::Result<()> {
|
||||||
|
let raw = "@name:version";
|
||||||
|
|
||||||
|
ProjectTag::from_str(raw).expect_err("test expected err");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_destination_tag_fails() -> anyhow::Result<()> {
|
||||||
|
let raw = "name:version";
|
||||||
|
|
||||||
|
ProjectTag::from_str(raw).expect_err("test expected err");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_name_fails() -> anyhow::Result<()> {
|
||||||
|
let raw = "destination@:version";
|
||||||
|
|
||||||
|
ProjectTag::from_str(raw).expect_err("test expected err");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_name_tag_fails() -> anyhow::Result<()> {
|
||||||
|
let raw = "destination@:version";
|
||||||
|
|
||||||
|
ProjectTag::from_str(raw).expect_err("test expected err");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn missing_version_fails() -> anyhow::Result<()> {
|
||||||
|
let raw = "destination@name:";
|
||||||
|
|
||||||
|
ProjectTag::from_str(raw).expect_err("test expected err");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_version_tag_fails() -> anyhow::Result<()> {
|
||||||
|
let raw = "destination@name:";
|
||||||
|
|
||||||
|
ProjectTag::from_str(raw).expect_err("test expected err");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
88
crates/norun/src/project_file.rs
Normal file
88
crates/norun/src/project_file.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
const NORUN_PROJECT_FILE_NAME: &str = "norun.toml";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
|
pub struct ProjectFile {
|
||||||
|
pub project: ProjectDecl,
|
||||||
|
pub container: ContainerDecl,
|
||||||
|
pub expose: Option<ExposeDecl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
|
pub struct ProjectDecl {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
|
pub struct ContainerDecl {
|
||||||
|
pub image: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
|
pub struct ExposeDecl {
|
||||||
|
pub port: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(content: &str) -> anyhow::Result<ProjectFile> {
|
||||||
|
let project_file: ProjectFile = toml::from_str(content)?;
|
||||||
|
|
||||||
|
Ok(project_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn parse_file(path: &Path) -> anyhow::Result<ProjectFile> {
|
||||||
|
let path = if let Some(file_name) = path.file_name()
|
||||||
|
&& file_name == NORUN_PROJECT_FILE_NAME
|
||||||
|
{
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
&path.join(NORUN_PROJECT_FILE_NAME)
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = tokio::fs::read_to_string(path)
|
||||||
|
.await
|
||||||
|
.context("failed to read project file in the given directory")?;
|
||||||
|
|
||||||
|
parse(&file)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::project_file::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_parse_file_happy_path() -> anyhow::Result<()> {
|
||||||
|
let raw = r#"
|
||||||
|
[project]
|
||||||
|
name = "hello-world"
|
||||||
|
|
||||||
|
[container]
|
||||||
|
image = "kasperhermansen/hello-world"
|
||||||
|
version = "latest" # default
|
||||||
|
|
||||||
|
[expose]
|
||||||
|
port = 8080
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let expected = ProjectFile {
|
||||||
|
project: ProjectDecl {
|
||||||
|
name: "hello-world".into(),
|
||||||
|
},
|
||||||
|
container: ContainerDecl {
|
||||||
|
image: "kasperhermansen/hello-world".into(),
|
||||||
|
version: "latest".into(),
|
||||||
|
},
|
||||||
|
expose: Some(ExposeDecl { port: Some(8080) }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let actual = parse(raw)?;
|
||||||
|
|
||||||
|
pretty_assertions::assert_eq!(expected, actual);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
1
crates/norun/src/server.rs
Normal file
1
crates/norun/src/server.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod services;
|
1
crates/norun/src/server/services.rs
Normal file
1
crates/norun/src/server/services.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod registry;
|
42
crates/norun/src/server/services/registry.rs
Normal file
42
crates/norun/src/server/services/registry.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
sync::{Arc, LazyLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use norun_grpc_interface::Project;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::state::ServerState;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RegistryService {
|
||||||
|
store: Arc<Mutex<Vec<Project>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegistryService {
|
||||||
|
pub async fn store(&self, project: &Project) -> anyhow::Result<()> {
|
||||||
|
tracing::debug!("storing project");
|
||||||
|
|
||||||
|
let mut store = self.store.lock().await;
|
||||||
|
|
||||||
|
store.push(project.clone());
|
||||||
|
|
||||||
|
tracing::debug!("currently has {} in storage", store.len());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RegistryServiceState {
|
||||||
|
fn registry_service(&self) -> RegistryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegistryServiceState for ServerState {
|
||||||
|
fn registry_service(&self) -> RegistryService {
|
||||||
|
static SERVICE: LazyLock<RegistryService> = LazyLock::new(|| RegistryService {
|
||||||
|
store: Arc::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
SERVICE.clone()
|
||||||
|
}
|
||||||
|
}
|
21
crates/norun/src/state.rs
Normal file
21
crates/norun/src/state.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ClientState {
|
||||||
|
pub norun_server_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientState {
|
||||||
|
pub fn new(norun_server_url: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
norun_server_url: norun_server_url.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ServerState {}
|
||||||
|
|
||||||
|
impl ServerState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
9
examples/hello-world/norun.toml
Normal file
9
examples/hello-world/norun.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "hello-world"
|
||||||
|
|
||||||
|
[container]
|
||||||
|
image = "kasperhermansen/hello-world"
|
||||||
|
version = "latest"
|
||||||
|
|
||||||
|
[expose]
|
||||||
|
port = 8080
|
21
interface/proto/norun/v1/registry.proto
Normal file
21
interface/proto/norun/v1/registry.proto
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package norun.v1;
|
||||||
|
|
||||||
|
service RegistryService {
|
||||||
|
rpc Publish(PublishRequest) returns (PublishResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message PublishRequest {
|
||||||
|
Project project = 1;
|
||||||
|
}
|
||||||
|
message PublishResponse {}
|
||||||
|
|
||||||
|
message Project {
|
||||||
|
string name = 1;
|
||||||
|
|
||||||
|
string image = 2;
|
||||||
|
string version = 3;
|
||||||
|
|
||||||
|
optional uint32 port = 4;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user