Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use crate::api::Discovery;
|
||||
|
||||
use super::{config::AgentConfig, discovery_client::DiscoveryClient, grpc_client::GrpcClient};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AgentState(Arc<State>);
|
||||
|
||||
@@ -23,10 +27,24 @@ impl Deref for AgentState {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {}
|
||||
pub struct State {
|
||||
pub grpc: GrpcClient,
|
||||
pub config: AgentConfig,
|
||||
pub discovery: Discovery,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
Ok(Self {})
|
||||
let config = AgentConfig::new().await?;
|
||||
let discovery = DiscoveryClient::new(&config.discovery);
|
||||
let discovery = discovery.discover().await?;
|
||||
|
||||
let grpc = GrpcClient::new(&discovery.process_host);
|
||||
|
||||
Ok(Self {
|
||||
grpc,
|
||||
config,
|
||||
discovery,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
80
crates/churn/src/agent/config.rs
Normal file
80
crates/churn/src/agent/config.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AgentConfig {
|
||||
pub agent_id: String,
|
||||
pub discovery: String,
|
||||
}
|
||||
|
||||
impl AgentConfig {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
let config = ConfigFile::load().await?;
|
||||
|
||||
Ok(Self {
|
||||
agent_id: config.agent_id,
|
||||
discovery: config.discovery,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ConfigFile {
|
||||
agent_id: String,
|
||||
discovery: String,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
pub async fn load() -> anyhow::Result<Self> {
|
||||
let directory = dirs::data_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get data dir"))?
|
||||
.join("io.kjuulh.churn-agent")
|
||||
.join("churn-agent.toml");
|
||||
|
||||
if !directory.exists() {
|
||||
anyhow::bail!(
|
||||
"No churn agent file was setup, run `churn agent setup` to setup the defaults"
|
||||
)
|
||||
}
|
||||
|
||||
let contents = tokio::fs::read_to_string(&directory).await?;
|
||||
|
||||
toml::from_str(&contents).context("failed to parse the contents of the churn agent config")
|
||||
}
|
||||
|
||||
pub async fn write_default(discovery: impl Into<String>, force: bool) -> anyhow::Result<Self> {
|
||||
let s = Self {
|
||||
agent_id: Uuid::new_v4().to_string(),
|
||||
discovery: discovery.into(),
|
||||
};
|
||||
|
||||
let directory = dirs::data_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get data dir"))?
|
||||
.join("io.kjuulh.churn-agent")
|
||||
.join("churn-agent.toml");
|
||||
|
||||
if let Some(parent) = directory.parent() {
|
||||
tokio::fs::create_dir_all(&parent).await?;
|
||||
}
|
||||
|
||||
if !force && directory.exists() {
|
||||
anyhow::bail!("config file already exists, consider moving it to a backup before trying again: {}", directory.display());
|
||||
}
|
||||
|
||||
let contents = toml::to_string_pretty(&s)
|
||||
.context("failed to convert default implementation to string")?;
|
||||
|
||||
tokio::fs::write(directory, contents.as_bytes())
|
||||
.await
|
||||
.context("failed to write to agent file")?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_config(discovery: impl Into<String>, force: bool) -> anyhow::Result<()> {
|
||||
ConfigFile::write_default(discovery, force).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
21
crates/churn/src/agent/discovery_client.rs
Normal file
21
crates/churn/src/agent/discovery_client.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use crate::api::Discovery;
|
||||
|
||||
pub struct DiscoveryClient {
|
||||
host: String,
|
||||
}
|
||||
|
||||
impl DiscoveryClient {
|
||||
pub fn new(discovery_host: impl Into<String>) -> Self {
|
||||
Self {
|
||||
host: discovery_host.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn discover(&self) -> anyhow::Result<Discovery> {
|
||||
tracing::info!(
|
||||
"getting details from discovery endpoint: {}/discovery",
|
||||
self.host.trim_end_matches('/')
|
||||
);
|
||||
crate::api::Discovery::get_from_host(&self.host).await
|
||||
}
|
||||
}
|
53
crates/churn/src/agent/event_handler.rs
Normal file
53
crates/churn/src/agent/event_handler.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use notmad::{Component, MadError};
|
||||
|
||||
use super::{agent_state::AgentState, config::AgentConfig, grpc_client::GrpcClient};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EventHandler {
|
||||
config: AgentConfig,
|
||||
grpc: GrpcClient,
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
pub fn new(state: impl Into<AgentState>) -> Self {
|
||||
let state: AgentState = state.into();
|
||||
|
||||
Self {
|
||||
config: state.config.clone(),
|
||||
grpc: state.grpc.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Component for EventHandler {
|
||||
fn name(&self) -> Option<String> {
|
||||
Some("event_handler".into())
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
cancellation_token: tokio_util::sync::CancellationToken,
|
||||
) -> Result<(), notmad::MadError> {
|
||||
tokio::select! {
|
||||
_ = cancellation_token.cancelled() => {},
|
||||
res = self.grpc.listen_events("agents", None::<String>, self.clone()) => {
|
||||
res.map_err(MadError::Inner)?;
|
||||
},
|
||||
res = self.grpc.listen_events("agents", Some(&self.config.agent_id), self.clone()) => {
|
||||
res.map_err(MadError::Inner)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::grpc_client::ListenEventsExecutor for EventHandler {
|
||||
async fn execute(&self, event: crate::grpc::ListenEventsResponse) -> anyhow::Result<()> {
|
||||
tracing::info!(value = event.value, "received event");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
100
crates/churn/src/agent/grpc_client.rs
Normal file
100
crates/churn/src/agent/grpc_client.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use tonic::transport::{Channel, ClientTlsConfig};
|
||||
|
||||
use crate::grpc::{churn_client::ChurnClient, *};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GrpcClient {
|
||||
host: String,
|
||||
}
|
||||
|
||||
impl GrpcClient {
|
||||
pub fn new(host: impl Into<String>) -> Self {
|
||||
Self { host: host.into() }
|
||||
}
|
||||
|
||||
pub async fn get_key(
|
||||
&self,
|
||||
namespace: &str,
|
||||
id: Option<impl Into<String>>,
|
||||
key: &str,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
let mut client = self.client().await?;
|
||||
|
||||
let resp = client
|
||||
.get_key(GetKeyRequest {
|
||||
key: key.into(),
|
||||
namespace: namespace.into(),
|
||||
id: id.map(|i| i.into()),
|
||||
})
|
||||
.await?;
|
||||
let resp = resp.into_inner();
|
||||
|
||||
Ok(resp.value)
|
||||
}
|
||||
|
||||
pub async fn set_key(
|
||||
&self,
|
||||
namespace: &str,
|
||||
id: Option<impl Into<String>>,
|
||||
key: &str,
|
||||
value: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut client = self.client().await?;
|
||||
|
||||
client
|
||||
.set_key(SetKeyRequest {
|
||||
key: key.into(),
|
||||
value: value.into(),
|
||||
namespace: namespace.into(),
|
||||
id: id.map(|i| i.into()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn listen_events(
|
||||
&self,
|
||||
namespace: &str,
|
||||
id: Option<impl Into<String>>,
|
||||
exec: impl ListenEventsExecutor,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut client = self.client().await?;
|
||||
|
||||
let resp = client
|
||||
.listen_events(ListenEventsRequest {
|
||||
namespace: namespace.into(),
|
||||
id: id.map(|i| i.into()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut inner = resp.into_inner();
|
||||
while let Ok(Some(message)) = inner.message().await {
|
||||
exec.execute(message).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn client(&self) -> anyhow::Result<ChurnClient<tonic::transport::Channel>> {
|
||||
let channel = if self.host.starts_with("https") {
|
||||
Channel::from_shared(self.host.to_owned())?
|
||||
.tls_config(ClientTlsConfig::new().with_native_roots())?
|
||||
.connect()
|
||||
.await?
|
||||
} else {
|
||||
Channel::from_shared(self.host.to_owned())?
|
||||
.connect()
|
||||
.await?
|
||||
};
|
||||
|
||||
let client = ChurnClient::new(channel);
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ListenEventsExecutor {
|
||||
async fn execute(&self, event: ListenEventsResponse) -> anyhow::Result<()>;
|
||||
}
|
@@ -4,21 +4,24 @@ use super::agent_state::AgentState;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AgentRefresh {
|
||||
_state: AgentState,
|
||||
host: String,
|
||||
process_host: String,
|
||||
}
|
||||
|
||||
impl AgentRefresh {
|
||||
pub fn new(state: impl Into<AgentState>, host: impl Into<String>) -> Self {
|
||||
pub fn new(state: impl Into<AgentState>) -> Self {
|
||||
let state: AgentState = state.into();
|
||||
Self {
|
||||
_state: state.into(),
|
||||
host: host.into(),
|
||||
process_host: state.discovery.process_host.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl notmad::Component for AgentRefresh {
|
||||
fn name(&self) -> Option<String> {
|
||||
Some("agent_refresh".into())
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
cancellation_token: tokio_util::sync::CancellationToken,
|
||||
@@ -39,7 +42,7 @@ impl notmad::Component for AgentRefresh {
|
||||
#[async_trait::async_trait]
|
||||
impl nodrift::Drifter for AgentRefresh {
|
||||
async fn execute(&self, _token: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
|
||||
tracing::info!(host = self.host, "refreshing agent");
|
||||
tracing::info!(process_host = self.process_host, "refreshing agent");
|
||||
|
||||
// Get plan
|
||||
let plan = Plan::new();
|
||||
|
Reference in New Issue
Block a user