Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ secrecy = "0.10.3"
serial_test = "3.2.0"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.132"
serde_yml = "0.0.12"
serde_with = { version = "3.12.0", features = ["base64"] }
sha2 = "0.10"
sysinfo = "0.38.0"
Expand Down
1 change: 1 addition & 0 deletions agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ rumqttc = { workspace = true }
secrecy = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yml = { workspace = true }
sysinfo = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
Expand Down
135 changes: 129 additions & 6 deletions agent/src/server/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ use std::sync::Arc;
use crate::errors::Error;
use crate::server::{errors::*, state::State};
use crate::services::{
deployment as dpl_svc, device as dvc_svc, git_commit as git_cmt_svc, release as rls_svc,
HttpBackend,
config_instance as ci_svc, deployment as dpl_svc, device as dvc_svc, git_commit as git_cmt_svc,
release as rls_svc, HttpBackend,
};
use crate::version;
use device_api::models as device_server;

// external crates
use axum::{
extract::{Path, State as AxumState},
extract::{Path, Query, State as AxumState},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tracing::error;

Expand Down Expand Up @@ -145,6 +145,129 @@ pub async fn get_git_commit(
.await
}

// ============================= CONFIG INSTANCES ================================= //
#[derive(Deserialize)]
pub struct ExpandQuery {
pub expand: Option<String>,
}

pub async fn get_config_instance(
AxumState(state): AxumState<Arc<State>>,
Path(config_instance_id): Path<String>,
Query(query): Query<ExpandQuery>,
) -> impl IntoResponse {
handle(
async move {
let expand_content = query
.expand
.as_deref()
.map(|v| v == "content")
.unwrap_or(false);
let response = ci_svc::get_response(
&state.storage.cfg_insts.meta,
&state.storage.cfg_insts.content,
config_instance_id,
expand_content,
)
.await?;
Ok::<_, ServerErr>(response)
},
"Error getting config instance",
)
.await
}

pub async fn get_config_instance_content(
AxumState(state): AxumState<Arc<State>>,
Path(config_instance_id): Path<String>,
) -> impl IntoResponse {
let result = ci_svc::get_raw_content(
&state.storage.cfg_insts.meta,
&state.storage.cfg_insts.content,
config_instance_id,
)
.await;

match result {
Ok((ci, content)) => {
let format = ci_svc::infer_format(&ci.filepath);
let content_type = match format {
"yaml" => "application/x-yaml",
_ => "application/json",
};
let filename = ci
.filepath
.rsplit('/')
.next()
.unwrap_or(&ci.filepath)
.to_string();
let disposition = format!("attachment; filename=\"{filename}\"");

(
StatusCode::OK,
[
(axum::http::header::CONTENT_TYPE, content_type.to_string()),
(axum::http::header::CONTENT_DISPOSITION, disposition),
],
content,
)
.into_response()
}
Err(e) => {
let e: ServerErr = e.into();
error!("Error getting config instance content: {e:?}");
let error_response = to_error_response(&e);
(e.http_status(), Json(json!(error_response))).into_response()
}
}
}

#[derive(Deserialize)]
pub struct PrefixQuery {
pub prefix: Option<String>,
}

pub async fn get_config_instance_parameter(
AxumState(state): AxumState<Arc<State>>,
Path((config_instance_id, key)): Path<(String, String)>,
) -> impl IntoResponse {
handle(
async move {
let response = ci_svc::get_parameter(
&state.storage.cfg_insts.meta,
&state.storage.cfg_insts.content,
config_instance_id,
key,
)
.await?;
Ok::<_, ServerErr>(response)
},
"Error getting config instance parameter",
)
.await
}

pub async fn list_config_instance_parameters(
AxumState(state): AxumState<Arc<State>>,
Path(config_instance_id): Path<String>,
Query(query): Query<PrefixQuery>,
) -> impl IntoResponse {
handle(
async move {
let response = ci_svc::list_parameters(
&state.storage.cfg_insts.meta,
&state.storage.cfg_insts.content,
config_instance_id,
query.prefix,
)
.await?;
Ok::<_, ServerErr>(response)
},
"Error listing config instance parameters",
)
.await
}

// ================================ UTILITIES ====================================== //
async fn handle<F, T, E>(service: F, err_msg: &str) -> (StatusCode, Json<Value>)
where
Expand All @@ -157,12 +280,12 @@ where
Err(e) => {
let e: ServerErr = e.into();
error!("{err_msg}: {e:?}");
(e.http_status(), Json(json!(to_error_response(e))))
(e.http_status(), Json(json!(to_error_response(&e))))
}
}
}

fn to_error_response(e: impl Error) -> device_server::ErrorResponse {
fn to_error_response(e: &(impl Error + ?Sized)) -> device_server::ErrorResponse {
let params = e
.params()
.and_then(|v| serde_json::from_value(v).ok())
Expand Down
1 change: 1 addition & 0 deletions agent/src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod errors;
pub mod handlers;
pub mod response;
pub mod responses;
pub mod serve;
pub mod sse;
pub mod state;
Expand Down
75 changes: 75 additions & 0 deletions agent/src/server/responses/config_instance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// internal crates
use crate::models;

// external crates
use serde::{Deserialize, Serialize};
use serde_json::Value;

// ========================== CONFIG INSTANCE ============================== //
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConfigInstanceResponse {
pub object: String,
pub id: String,
pub config_type_name: String,
pub filepath: String,
pub created_at: String,
pub config_schema_id: String,
pub config_type_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ContentField>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContentField {
pub format: String,
pub data: String,
}

impl ConfigInstanceResponse {
pub fn from_model(ci: &models::ConfigInstance, content: Option<ContentField>) -> Self {
Self {
object: "config_instance".to_string(),
id: ci.id.clone(),
config_type_name: ci.config_type_name.clone(),
filepath: ci.filepath.clone(),
created_at: ci.created_at.to_rfc3339(),
config_schema_id: ci.config_schema_id.clone(),
config_type_id: ci.config_type_id.clone(),
content,
}
}
}

// ============================ PARAMETER ================================== //
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ParameterResponse {
pub object: String,
pub key: Vec<String>,
pub value: Value,
}

impl ParameterResponse {
pub fn new(key: Vec<String>, value: Value) -> Self {
Self {
object: "parameter".to_string(),
key,
value,
}
}
}

// ========================= PARAMETER LIST ================================ //
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ParameterListResponse {
pub object: String,
pub data: Vec<ParameterResponse>,
}

impl ParameterListResponse {
pub fn new(data: Vec<ParameterResponse>) -> Self {
Self {
object: "list".to_string(),
data,
}
}
}
1 change: 1 addition & 0 deletions agent/src/server/responses/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod config_instance;
18 changes: 18 additions & 0 deletions agent/src/server/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ pub fn routes(state: Arc<State>) -> Router {
format!("/{api_version}/git_commits/{{git_commit_id}}").as_str(),
get(handlers::get_git_commit),
)
// =========================== CONFIG INSTANCES ============================ //
.route(
format!("/{api_version}/config_instances/{{config_instance_id}}").as_str(),
get(handlers::get_config_instance),
)
.route(
format!("/{api_version}/config_instances/{{config_instance_id}}/content").as_str(),
get(handlers::get_config_instance_content),
)
.route(
format!("/{api_version}/config_instances/{{config_instance_id}}/parameters").as_str(),
get(handlers::list_config_instance_parameters),
)
.route(
format!("/{api_version}/config_instances/{{config_instance_id}}/parameters/{{key}}")
.as_str(),
get(handlers::get_config_instance_parameter),
)
// ============================== EVENTS =================================== //
.route(
format!("/{api_version}/events").as_str(),
Expand Down
1 change: 1 addition & 0 deletions agent/src/services/config_instance/.covgate
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
80.00
89 changes: 89 additions & 0 deletions agent/src/services/config_instance/get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// internal crates
use crate::errors::Trace;
use crate::models;
use crate::server::responses::config_instance::{ConfigInstanceResponse, ContentField};
use crate::services::errors::ServiceErr;
use crate::storage;

// ========================= ERRORS ======================================= //
#[derive(Debug, thiserror::Error)]
#[error("config instance not found: {id}")]
pub struct ConfigInstanceNotFoundErr {
pub id: String,
pub trace: Box<Trace>,
}

impl crate::errors::Error for ConfigInstanceNotFoundErr {
fn code(&self) -> crate::errors::Code {
crate::errors::Code::ResourceNotFound
}
fn http_status(&self) -> crate::errors::HTTPCode {
crate::errors::HTTPCode::NOT_FOUND
}
}

// ========================= SERVICE ====================================== //
pub async fn get(
cfg_insts: &storage::CfgInsts,
id: String,
) -> Result<models::ConfigInstance, ServiceErr> {
let ci = cfg_insts.read_optional(id.clone()).await?;
match ci {
Some(ci) => Ok(ci),
None => Err(ServiceErr::ConfigInstanceNotFoundErr(
ConfigInstanceNotFoundErr {
id,
trace: crate::trace!(),
},
)),
}
}

pub async fn get_response(
cfg_insts: &storage::CfgInsts,
cfg_inst_content: &storage::CfgInstContent,
id: String,
expand_content: bool,
) -> Result<ConfigInstanceResponse, ServiceErr> {
let ci = get(cfg_insts, id.clone()).await?;
let content = if expand_content {
let raw = cfg_inst_content.read_optional(id).await?;
raw.map(|data| ContentField {
format: infer_format(&ci.filepath).to_string(),
data,
})
} else {
None
};
Ok(ConfigInstanceResponse::from_model(&ci, content))
}

pub async fn get_raw_content(
cfg_insts: &storage::CfgInsts,
cfg_inst_content: &storage::CfgInstContent,
id: String,
) -> Result<(models::ConfigInstance, String), ServiceErr> {
let ci = get(cfg_insts, id.clone()).await?;
let content = cfg_inst_content.read_optional(id.clone()).await?;
match content {
Some(data) => Ok((ci, data)),
None => Err(ServiceErr::ConfigInstanceNotFoundErr(
ConfigInstanceNotFoundErr {
id,
trace: crate::trace!(),
},
)),
}
}

pub fn infer_format(filepath: &str) -> &'static str {
if let Some(ext) = filepath.rsplit('.').next() {
match ext {
"yaml" | "yml" => "yaml",
"json" => "json",
_ => "json",
}
} else {
"json"
}
}
5 changes: 5 additions & 0 deletions agent/src/services/config_instance/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod get;
pub mod parameter;

pub use get::*;
pub use parameter::*;
Loading
Loading