Skip to content

Commit cd541a7

Browse files
authored
Working Test Client (#1)
1 parent bb0e8a5 commit cd541a7

16 files changed

Lines changed: 1084 additions & 1 deletion

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target
2+
.git

.github/workflows/docker.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Create and publish SAM/DenIM-on-SAM Test Client
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
7+
env:
8+
REGISTRY: ghcr.io
9+
IMAGE_NAME: ${{ github.repository }}
10+
11+
jobs:
12+
build-and-push-image:
13+
runs-on: ubuntu-latest
14+
15+
permissions:
16+
contents: read
17+
packages: write
18+
attestations: write
19+
id-token: write
20+
21+
steps:
22+
- name: Checkout repository
23+
uses: actions/checkout@v4
24+
25+
- name: Log in to the Container registry
26+
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
27+
with:
28+
registry: ${{ env.REGISTRY }}
29+
username: ${{ github.actor }}
30+
password: ${{ secrets.GHCR_TOKEN }}
31+
32+
- name: Extract metadata (tags, labels) for Docker
33+
id: meta
34+
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
35+
with:
36+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
37+
38+
- name: Build and push Docker image
39+
id: push
40+
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
41+
with:
42+
context: .
43+
push: true
44+
tags: ${{ steps.meta.outputs.tags }}
45+
labels: ${{ steps.meta.outputs.labels }}
46+
47+
- name: Generate artifact attestation
48+
uses: actions/attest-build-provenance@v2
49+
with:
50+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
51+
subject-digest: ${{ steps.push.outputs.digest }}
52+
push-to-registry: true

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,14 @@ target/
1414
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
1515
# and can be added to the global gitignore or merged into this file. For a more nuclear
1616
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
17-
#.idea/
17+
#.idea/
18+
19+
# Added by cargo
20+
21+
/target
22+
23+
*.sql
24+
*.crt
25+
*.key
26+
*.json
27+
Cargo.lock

Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "test-client"
3+
version = "0.1.0"
4+
edition = "2024"
5+
repository = "https://github.com/SAM-Research/test-client"
6+
7+
[dependencies]
8+
denim-sam-client = { git = "https://github.com/SAM-Research/denim-on-sam.git", branch = "main" }
9+
denim-sam-common = { git = "https://github.com/SAM-Research/denim-on-sam.git", branch = "main" }
10+
sam-client = { git = "https://github.com/SAM-Research/sam-instant-messenger.git", branch = "main" }
11+
sam-common = { git = "https://github.com/SAM-Research/sam-instant-messenger.git", branch = "main" }
12+
sam-net = { git = "https://github.com/SAM-Research/sam-instant-messenger.git", branch = "main" }
13+
tokio = { version = "1.40.0", features = ["full"] }
14+
reqwest = { version = "0.12.12", features = ["cookies"] }
15+
serde = { version = "1.0.210" }
16+
serde_with = { version = "3.11.0" }
17+
derive_more = { version = "2.0.1" }
18+
async-trait = "0.1.83"
19+
serde_json = "1.0.139"
20+
bon = "3.3.2"
21+
rustls = "0.23.15"
22+
env_logger = "0.11.6"
23+
rand = "0.8.5"
24+
log = "0.4.25"
25+
clap = "4.5.32"

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,19 @@
11
# Denim SAM Test client
2+
3+
# SAM Dependencies
4+
5+
to update sam dependencies just do:
6+
7+
```sh
8+
cargo update -p sam-server
9+
```
10+
11+
you might need to change `sam-server` to either one of the other sam projects
12+
13+
# Docker
14+
15+
Building the `test-client` docker image:
16+
17+
```sh
18+
docker build -t test-client .
19+
```

config.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"address": "127.0.0.1:4443",
3+
"dispatchAddress": "127.0.0.1:8080",
4+
"certificatePath": "./root.crt",
5+
"channelBufferSize": 10,
6+
"logging": "info"
7+
}

dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM rust:1.86-slim AS builder
2+
ENV SQLX_OFFLINE=true
3+
WORKDIR /test-client
4+
COPY . .
5+
6+
RUN apt update
7+
RUN apt install -y protobuf-compiler pkg-config libssl-dev
8+
9+
RUN cargo build --release
10+
11+
LABEL org.opencontainers.image.source=https://github.com/SAM-Research/test-client
12+
LABEL org.opencontainers.image.description="SAM/DenIM-on-SAM Test Client image"
13+
LABEL org.opencontainers.image.licenses=MIT
14+
15+
16+
FROM debian:bookworm-slim
17+
RUN apt update && apt install -y libssl3 ca-certificates && rm -rf /var/lib/apt/lists/*
18+
COPY --from=builder /test-client/target/release/test-client /test-client
19+
20+
21+
ENTRYPOINT ["/test-client"]

src/config.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Serialize, Deserialize)]
4+
#[serde(rename_all = "camelCase")]
5+
pub struct DenimClientConfig {
6+
pub address: String,
7+
pub dispatch_address: String,
8+
pub certificate_path: Option<String>,
9+
10+
pub channel_buffer_size: Option<usize>,
11+
12+
pub logging: Option<String>,
13+
}

src/data.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use sam_common::AccountId;
2+
use serde::{Deserialize, Serialize};
3+
use std::collections::HashMap;
4+
5+
#[derive(Serialize, Deserialize, Clone, Debug)]
6+
pub struct Friend {
7+
pub username: String,
8+
pub frequency: f64,
9+
pub denim: bool,
10+
}
11+
12+
#[derive(Serialize, Deserialize, Clone, Debug)]
13+
#[serde(rename_all = "camelCase")]
14+
pub struct ClientInfo {
15+
pub client_type: ClientType,
16+
pub username: String,
17+
pub message_size_range: (u32, u32),
18+
pub send_rate: u32,
19+
pub tick_millis: u32,
20+
pub duration_ticks: u32,
21+
pub denim_probability: f32,
22+
pub friends: HashMap<String, Friend>,
23+
}
24+
25+
#[derive(Serialize, Deserialize, Clone, Debug)]
26+
#[serde(rename_all = "lowercase")]
27+
pub enum ClientType {
28+
Denim,
29+
Sam,
30+
#[serde(other)]
31+
Other,
32+
}
33+
34+
#[derive(Serialize, Deserialize, Clone, Debug)]
35+
#[serde(rename_all = "lowercase")]
36+
pub enum MessageType {
37+
Denim,
38+
Regular,
39+
#[serde(other)]
40+
Other,
41+
}
42+
43+
#[derive(Serialize, Deserialize, Clone, bon::Builder, Debug)]
44+
#[serde(rename_all = "camelCase")]
45+
pub struct MessageLog {
46+
#[serde(rename = "type")]
47+
pub r#type: MessageType,
48+
pub from: String,
49+
pub to: String,
50+
pub size: usize,
51+
pub tick: u32,
52+
}
53+
54+
#[derive(Serialize, Deserialize, Clone, bon::Builder, Debug)]
55+
#[serde(rename_all = "camelCase")]
56+
pub struct ClientReport {
57+
pub start_time: u64,
58+
pub messages: Vec<MessageLog>,
59+
}
60+
61+
#[derive(Serialize, Deserialize, Clone, Debug)]
62+
#[serde(rename_all = "camelCase")]
63+
pub struct StartInfo {
64+
pub friends: HashMap<String, AccountId>,
65+
}
66+
67+
#[derive(Serialize, Deserialize, Clone, bon::Builder, Debug)]
68+
#[serde(rename_all = "camelCase")]
69+
pub struct AccountInfo {
70+
pub account_id: AccountId,
71+
}
72+
73+
#[derive(Serialize, Deserialize, Clone, bon::Builder, Debug)]
74+
#[serde(rename_all = "camelCase")]
75+
pub struct HealthCheck {
76+
pub sam: String,
77+
pub denim: Option<String>,
78+
pub database: String,
79+
}
80+
81+
impl HealthCheck {
82+
pub fn is_ok(&self) -> bool {
83+
let status = vec![&self.sam, &self.database];
84+
let is_ok = status.iter().all(|f| *f == "OK");
85+
is_ok && self.denim.as_ref().is_some_and(|x| x == "OK")
86+
}
87+
}
88+
89+
pub struct DispatchData {
90+
pub client: ClientInfo,
91+
pub start: StartInfo,
92+
}
93+
94+
impl DispatchData {
95+
pub fn new(client: ClientInfo, start: StartInfo) -> Self {
96+
Self { client, start }
97+
}
98+
}

src/dispatch.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use crate::data::{AccountInfo, ClientInfo, ClientReport, StartInfo};
2+
use derive_more::{Display, Error, From};
3+
4+
pub struct SamDispatchClient {
5+
url: String,
6+
client: reqwest::Client,
7+
}
8+
9+
#[derive(Debug, Display, Error, From)]
10+
pub enum SamDispatchError {
11+
Json(serde_json::Error),
12+
Reqwest(reqwest::Error),
13+
Unauthorized,
14+
}
15+
16+
impl SamDispatchClient {
17+
pub fn new(url: String) -> Result<Self, SamDispatchError> {
18+
Ok(Self {
19+
url: format!("http://{}", url),
20+
client: reqwest::Client::builder().cookie_store(true).build()?,
21+
})
22+
}
23+
24+
pub async fn health(&self) -> bool {
25+
let res = self.client.get(format!("{}/health", self.url)).send().await;
26+
match res {
27+
Ok(r) => r.status().is_success(),
28+
Err(_) => false,
29+
}
30+
}
31+
32+
pub async fn get_client(&self) -> Result<ClientInfo, SamDispatchError> {
33+
let res = self
34+
.client
35+
.get(format!("{}/client", self.url))
36+
.send()
37+
.await?;
38+
39+
Ok(res.json().await?)
40+
}
41+
42+
pub async fn sync(&self) -> Result<StartInfo, SamDispatchError> {
43+
let res = self.client.get(format!("{}/sync", self.url)).send().await?;
44+
if res.status() == reqwest::StatusCode::UNAUTHORIZED {
45+
return Err(SamDispatchError::Unauthorized);
46+
}
47+
Ok(res.json().await?)
48+
}
49+
50+
pub async fn upload_results(&self, report: ClientReport) -> Result<(), SamDispatchError> {
51+
let json_val = serde_json::to_string(&report)?;
52+
let res = self
53+
.client
54+
.post(format!("{}/upload", self.url))
55+
.body(json_val)
56+
.send()
57+
.await?;
58+
if res.status() == reqwest::StatusCode::UNAUTHORIZED {
59+
return Err(SamDispatchError::Unauthorized);
60+
}
61+
Ok(())
62+
}
63+
64+
pub async fn upload_account_id(&self, account_id: AccountInfo) -> Result<(), SamDispatchError> {
65+
let json_val = serde_json::to_string(&account_id)?;
66+
let res = self
67+
.client
68+
.post(format!("{}/id", self.url))
69+
.body(json_val)
70+
.send()
71+
.await?;
72+
if res.status() == reqwest::StatusCode::UNAUTHORIZED {
73+
return Err(SamDispatchError::Unauthorized);
74+
}
75+
Ok(())
76+
}
77+
}

0 commit comments

Comments
 (0)