Skip to content

Commit ad6b10b

Browse files
committed
refactor: implement builder pattern for OmnectDeviceServiceClient with lifecycle management
This refactoring improves the architecture of OmnectDeviceServiceClient by: - Implementing a type-safe builder pattern as the only way to create the client - Making certificate setup optional and injectable via closure - Making publish endpoint registration optional - Extracting client creation into dedicated build_service_client() function - Bumping version to 1.0.6 Key changes: - OmnectDeviceServiceClient fields are now private, enforcing builder usage - Builder supports optional certificate setup via with_certificate_setup() - Builder supports optional publish endpoint via with_publish_endpoint() - Certificate payload (CreateCertPayload) is now public and reusable - Removed intermediate WorkloadCertPayload struct for simplicity - Shutdown is now a DeviceServiceClient trait method called explicitly from main - Improved Dockerfile to copy entire src directory structure This design provides better encapsulation, clearer lifecycle management, and looser coupling between modules through dependency injection. Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
1 parent 3c7fa22 commit ad6b10b

File tree

11 files changed

+187
-137
lines changed

11 files changed

+187
-137
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ centrifugo
55
*.tar.gz
66
*.env
77
*.http
8-
/static
8+
/static
9+
CLAUDE.md

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
77
name = "omnect-ui"
88
readme = "README.md"
99
repository = "git@github.com:omnect/omnect-ui.git"
10-
version = "1.0.5"
10+
version = "1.0.6"
1111
build = "src/build.rs"
1212

1313
[dependencies]

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ COPY src/build.rs ./omnect-ui/src/build.rs
4242

4343
RUN --mount=type=cache,target=/usr/local/cargo/registry cd omnect-ui && cargo build ${OMNECT_UI_BUILD_ARG} --release --target-dir ./build
4444

45-
COPY src/* ./omnect-ui/src/
45+
COPY src ./omnect-ui/src/
4646
COPY .git ./omnect-ui/.git
4747
RUN --mount=type=cache,target=/usr/local/cargo/registry <<EOF
4848
set -e

src/auth/token.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ impl TokenManager {
4646
self.inner
4747
.key
4848
.authenticate(claims)
49-
.map_err(|e| anyhow::anyhow!("failed to create token: {}", e))
49+
.map_err(|e| anyhow::anyhow!("failed to create token: {e:#}"))
5050
}
5151

5252
/// Verify a token and check if it's valid

src/certificate.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
#![cfg_attr(feature = "mock", allow(dead_code, unused_imports))]
22

3-
use crate::{
4-
common::handle_http_response,
5-
http_client,
6-
omnect_device_service_client::{DeviceServiceClient, OmnectDeviceServiceClient},
7-
};
3+
use crate::{common::handle_http_response, http_client};
84
use anyhow::{Context, Result};
95
use log::info;
106
use serde::{Deserialize, Serialize};
117
use std::{fs::File, io::Write};
128

13-
#[derive(Serialize)]
14-
struct CreateCertPayload {
9+
// Public payload for passing to certificate creation
10+
#[derive(Debug, Serialize)]
11+
pub struct CreateCertPayload {
1512
#[serde(rename = "commonName")]
16-
common_name: String,
13+
pub common_name: String,
1714
}
1815

1916
#[derive(Debug, Deserialize)]
@@ -42,14 +39,13 @@ pub fn key_path() -> String {
4239
}
4340

4441
#[cfg(feature = "mock")]
45-
pub async fn create_module_certificate() -> Result<()> {
42+
pub async fn create_module_certificate(_payload: CreateCertPayload) -> Result<()> {
4643
Ok(())
4744
}
4845

4946
#[cfg(not(feature = "mock"))]
50-
pub async fn create_module_certificate() -> Result<()> {
47+
pub async fn create_module_certificate(payload: CreateCertPayload) -> Result<()> {
5148
info!("create module certificate");
52-
let ods_client = OmnectDeviceServiceClient::new(false).await?;
5349
let id = std::env::var("IOTEDGE_MODULEID")
5450
.context("failed to read IOTEDGE_MODULEID environment variable")?;
5551
let gen_id = std::env::var("IOTEDGE_MODULEGENERATIONID")
@@ -59,17 +55,13 @@ pub async fn create_module_certificate() -> Result<()> {
5955
let workload_uri = std::env::var("IOTEDGE_WORKLOADURI")
6056
.context("failed to read IOTEDGE_WORKLOADURI environment variable")?;
6157

62-
let payload = CreateCertPayload {
63-
common_name: ods_client.ip_address().await?,
64-
};
65-
66-
let path = format!("/modules/{id}/genid/{gen_id}/certificate/server?api-version={api_version}");
58+
let path = format!("modules/{id}/genid/{gen_id}/certificate/server?api-version={api_version}");
6759

6860
// Create a client for the IoT Edge workload socket
6961
let client = http_client::unix_socket_client(&workload_uri)?;
7062

71-
let url = format!("http://localhost{}", path);
72-
info!("POST {url} (IoT Edge workload API)");
63+
let url = format!("http://localhost/{path}");
64+
info!("POST {url} (IoT Edge workload API) with payload: {payload:?}");
7365

7466
let res = client
7567
.post(&url)

src/common.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ pub fn centrifugo_config() -> Arc<CentrifugoConfig> {
3333
.clone()
3434
}
3535

36+
pub fn centrifugo_publish_endpoint() -> crate::omnect_device_service_client::PublishEndpoint {
37+
let cfg = centrifugo_config();
38+
crate::omnect_device_service_client::PublishEndpoint {
39+
url: format!("https://localhost:{}/api/publish", cfg.port),
40+
headers: vec![
41+
crate::omnect_device_service_client::HeaderKeyValue {
42+
name: String::from("Content-Type"),
43+
value: String::from("application/json"),
44+
},
45+
crate::omnect_device_service_client::HeaderKeyValue {
46+
name: String::from("X-API-Key"),
47+
value: cfg.api_key.clone(),
48+
},
49+
],
50+
}
51+
}
52+
3653
macro_rules! config_path {
3754
() => {
3855
std::path::Path::new(
@@ -109,10 +126,7 @@ pub async fn handle_http_response(res: Response, context_msg: &str) -> Result<St
109126

110127
ensure!(
111128
status.is_success(),
112-
"{} failed with status {} and body: {}",
113-
context_msg,
114-
status,
115-
body
129+
"{context_msg} failed with status {status} and body: {body}"
116130
);
117131

118132
Ok(body)

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod api;
22
pub mod auth;
3+
pub mod certificate;
34
pub mod common;
45
pub mod http_client;
56
pub mod keycloak_client;

src/main.rs

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ mod omnect_device_service_client;
1010
use crate::{
1111
api::Api,
1212
auth::TokenManager,
13-
certificate::create_module_certificate,
14-
common::{centrifugo_config, config_path},
13+
certificate::{CreateCertPayload, create_module_certificate},
14+
common::{centrifugo_config, centrifugo_publish_endpoint, config_path},
1515
keycloak_client::KeycloakProvider,
16-
omnect_device_service_client::{DeviceServiceClient, OmnectDeviceServiceClient},
16+
omnect_device_service_client::{
17+
DeviceServiceClient, OmnectDeviceServiceClient, OmnectDeviceServiceClientBuilder,
18+
},
1719
};
1820
use actix_files::Files;
1921
use actix_multipart::form::MultipartFormConfig;
@@ -69,13 +71,30 @@ async fn main() {
6971
env!("GIT_SHORT_REV")
7072
);
7173

72-
create_module_certificate()
73-
.await
74-
.expect("failed to create module certificate");
74+
CryptoProvider::install_default(default_provider()).expect("failed to install crypto provider");
75+
76+
let Ok(true) = fs::exists("/data") else {
77+
panic!("failed to find required data directory: /data is missing");
78+
};
79+
80+
if !fs::exists(config_path!()).is_ok_and(|ok| ok) {
81+
fs::create_dir_all(config_path!()).expect("failed to create config directory");
82+
};
83+
84+
common::create_frontend_config_file().expect("failed to create frontend config file");
7585

7686
let mut sigterm = signal(SignalKind::terminate()).expect("failed to install SIGTERM handler");
7787
let mut centrifugo = run_centrifugo();
78-
let (server_handle, server_task, service_client) = run_server().await;
88+
let service_client = OmnectDeviceServiceClientBuilder::new()
89+
.with_certificate_setup(|payload: CreateCertPayload| async move {
90+
create_module_certificate(payload).await
91+
})
92+
.with_publish_endpoint(centrifugo_publish_endpoint())
93+
.build()
94+
.await
95+
.expect("failed to create device service client");
96+
97+
let (server_handle, server_task) = run_server(service_client.clone()).await;
7998

8099
tokio::select! {
81100
_ = tokio::signal::ctrl_c() => {
@@ -95,7 +114,7 @@ async fn main() {
95114
// Unified cleanup sequence - ensures consistent shutdown regardless of exit reason
96115
info!("shutting down...");
97116

98-
// 1. Shutdown service client first (unregister from omnect-device-service)
117+
// 1. Shutdown service client (unregister from omnect-device-service)
99118
if let Err(e) = service_client.shutdown().await {
100119
error!("failed to shutdown service client: {e:#}");
101120
}
@@ -111,33 +130,7 @@ async fn main() {
111130
info!("centrifugo stopped");
112131
}
113132

114-
async fn run_server() -> (
115-
ServerHandle,
116-
tokio::task::JoinHandle<Result<(), std::io::Error>>,
117-
OmnectDeviceServiceClient,
118-
) {
119-
CryptoProvider::install_default(default_provider()).expect("failed to install crypto provider");
120-
121-
let Ok(true) = fs::exists("/data") else {
122-
panic!("failed to find required data directory: /data is missing");
123-
};
124-
125-
if !fs::exists(config_path!()).is_ok_and(|ok| ok) {
126-
fs::create_dir_all(config_path!()).expect("failed to create config directory");
127-
};
128-
129-
common::create_frontend_config_file().expect("failed to create frontend config file");
130-
131-
type UiApi = Api<OmnectDeviceServiceClient, KeycloakProvider>;
132-
133-
let service_client = OmnectDeviceServiceClient::new(true)
134-
.await
135-
.expect("failed to create client to device service");
136-
137-
let api = UiApi::new(service_client.clone(), Default::default())
138-
.await
139-
.expect("failed to create api");
140-
133+
fn load_tls_config() -> rustls::ServerConfig {
141134
let mut tls_certs = std::io::BufReader::new(
142135
std::fs::File::open(certificate::cert_path()).expect("failed to read certificate file"),
143136
);
@@ -149,8 +142,7 @@ async fn run_server() -> (
149142
.collect::<Result<Vec<_>, _>>()
150143
.expect("failed to parse cert pem");
151144

152-
// set up TLS config options
153-
let tls_config = match rustls_pemfile::read_one(&mut tls_key)
145+
match rustls_pemfile::read_one(&mut tls_key)
154146
.expect("failed to read key pem file")
155147
.expect("failed to parse key pem file: no valid key found")
156148
{
@@ -163,7 +155,22 @@ async fn run_server() -> (
163155
.with_single_cert(tls_certs, rustls::pki_types::PrivateKeyDer::Pkcs8(key))
164156
.expect("failed to create TLS config"),
165157
_ => panic!("failed to parse key pem file: unexpected item type found"),
166-
};
158+
}
159+
}
160+
161+
async fn run_server(
162+
service_client: OmnectDeviceServiceClient,
163+
) -> (
164+
ServerHandle,
165+
tokio::task::JoinHandle<Result<(), std::io::Error>>,
166+
) {
167+
type UiApi = Api<OmnectDeviceServiceClient, KeycloakProvider>;
168+
169+
let api = UiApi::new(service_client.clone(), Default::default())
170+
.await
171+
.expect("failed to create api");
172+
173+
let tls_config = load_tls_config();
167174

168175
let ui_port = std::env::var("UI_PORT")
169176
.expect("failed to read UI_PORT environment variable")
@@ -256,7 +263,7 @@ async fn run_server() -> (
256263
.disable_signals()
257264
.run();
258265

259-
(server.handle(), tokio::spawn(server), service_client)
266+
(server.handle(), tokio::spawn(server))
260267
}
261268

262269
fn run_centrifugo() -> Child {

0 commit comments

Comments
 (0)