Skip to content

Commit 4aa0663

Browse files
Merge pull request #3357 from didier-wenzek/feat/tedge-http-cli
feat: New tedge http command
2 parents 4f16ca8 + 8049b70 commit 4aa0663

File tree

11 files changed

+825
-3
lines changed

11 files changed

+825
-3
lines changed

crates/core/tedge/src/cli/http/cli.rs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
use crate::cli::http::command::HttpAction;
2+
use crate::cli::http::command::HttpCommand;
3+
use crate::command::BuildCommand;
4+
use crate::command::BuildContext;
5+
use crate::command::Command;
6+
use crate::ConfigError;
7+
use anyhow::anyhow;
8+
use anyhow::Error;
9+
use camino::Utf8PathBuf;
10+
use certificate::CloudRootCerts;
11+
use clap::Args;
12+
use reqwest::blocking;
13+
use reqwest::Identity;
14+
use std::fs::File;
15+
use tedge_config::OptionalConfig;
16+
use tedge_config::ProfileName;
17+
18+
#[derive(clap::Subcommand, Debug)]
19+
pub enum TEdgeHttpCli {
20+
/// POST content to thin-edge local HTTP servers
21+
///
22+
/// Examples:
23+
/// # Create a new Cumulocity Managed Object via the proxy service
24+
/// tedge http post /c8y/inventory/managedObjects '{"name":"test"}' --accept-type application/json
25+
///
26+
/// # Create a new child device
27+
/// tedge http post /tedge/entity-store/v1/entities '{
28+
/// "@topic-id": "device/a//",
29+
/// "@type": "child-device",
30+
/// "@parent": "device/main//"
31+
/// }'
32+
#[clap(verbatim_doc_comment)]
33+
Post {
34+
/// Target URI
35+
uri: String,
36+
37+
/// Content to send
38+
#[command(flatten)]
39+
content: Content,
40+
41+
/// MIME type of the content
42+
#[clap(long)]
43+
#[arg(value_parser = parse_mime_type)]
44+
content_type: Option<String>,
45+
46+
/// MIME type of the expected content
47+
#[clap(long)]
48+
#[arg(value_parser = parse_mime_type)]
49+
accept_type: Option<String>,
50+
51+
/// Optional c8y cloud profile
52+
#[clap(long)]
53+
profile: Option<ProfileName>,
54+
},
55+
56+
/// PUT content to thin-edge local HTTP servers
57+
///
58+
/// Examples:
59+
/// # Upload file to the file transfer service
60+
/// tedge http put /tedge/file-transfer/target.txt --file source.txt
61+
///
62+
/// # Update a Cumulocity Managed Object. Note: Assuming tedge is the owner of the managed object
63+
/// tedge http put /c8y/inventory/managedObjects/2343978440 '{"name":"item A"}' --accept-type application/json
64+
#[clap(verbatim_doc_comment)]
65+
Put {
66+
/// Target URI
67+
uri: String,
68+
69+
/// Content to send
70+
#[command(flatten)]
71+
content: Content,
72+
73+
/// MIME type of the content
74+
#[clap(long)]
75+
#[arg(value_parser = parse_mime_type)]
76+
content_type: Option<String>,
77+
78+
/// MIME type of the expected content
79+
#[clap(long)]
80+
#[arg(value_parser = parse_mime_type)]
81+
accept_type: Option<String>,
82+
83+
/// Optional c8y cloud profile
84+
#[clap(long)]
85+
profile: Option<ProfileName>,
86+
},
87+
88+
/// GET content from thin-edge local HTTP servers
89+
///
90+
/// Examples:
91+
/// # Download file from the file transfer service
92+
/// tedge http get /tedge/file-transfer/target.txt
93+
///
94+
/// # Download file from Cumulocity's binary api
95+
/// tedge http get /c8y/inventory/binaries/104332 > my_file.bin
96+
#[clap(verbatim_doc_comment)]
97+
Get {
98+
/// Source URI
99+
uri: String,
100+
101+
/// MIME type of the expected content
102+
#[clap(long)]
103+
#[arg(value_parser = parse_mime_type)]
104+
accept_type: Option<String>,
105+
106+
/// Optional c8y cloud profile
107+
#[clap(long)]
108+
profile: Option<ProfileName>,
109+
},
110+
111+
/// DELETE resource from thin-edge local HTTP servers
112+
///
113+
/// Examples:
114+
/// # Delete a file from the file transfer service
115+
/// tedge http delete /tedge/file-transfer/target.txt
116+
///
117+
/// # Delete a Cumulocity managed object. Note: Assuming tedge is the owner of the managed object
118+
/// tedge http delete /c8y/inventory/managedObjects/2343978440
119+
#[clap(verbatim_doc_comment)]
120+
Delete {
121+
/// Source URI
122+
uri: String,
123+
124+
/// Optional c8y cloud profile
125+
#[clap(long)]
126+
profile: Option<ProfileName>,
127+
},
128+
}
129+
130+
#[derive(Args, Clone, Debug)]
131+
#[group(required = true, multiple = false)]
132+
pub struct Content {
133+
/// Content to send
134+
#[arg(name = "content")]
135+
arg2: Option<String>,
136+
137+
/// Content to send
138+
#[arg(long)]
139+
data: Option<String>,
140+
141+
/// File which content is sent
142+
#[arg(long)]
143+
file: Option<Utf8PathBuf>,
144+
}
145+
146+
fn parse_mime_type(input: &str) -> Result<String, Error> {
147+
Ok(input.parse::<mime_guess::mime::Mime>()?.to_string())
148+
}
149+
150+
impl TryFrom<Content> for blocking::Body {
151+
type Error = std::io::Error;
152+
153+
fn try_from(content: Content) -> Result<Self, Self::Error> {
154+
let body: blocking::Body = if let Some(data) = content.arg2 {
155+
data.into()
156+
} else if let Some(data) = content.data {
157+
data.into()
158+
} else if let Some(file) = content.file {
159+
File::open(file)?.into()
160+
} else {
161+
"".into()
162+
};
163+
164+
Ok(body)
165+
}
166+
}
167+
168+
impl BuildCommand for TEdgeHttpCli {
169+
fn build_command(self, context: BuildContext) -> Result<Box<dyn Command>, ConfigError> {
170+
let config = context.load_config()?;
171+
let uri = self.uri();
172+
173+
let (protocol, host, port) = if uri.starts_with("/c8y") {
174+
let c8y_config = config.c8y.try_get(self.c8y_profile())?;
175+
let client = &c8y_config.proxy.client;
176+
let protocol = https_if_some(&c8y_config.proxy.cert_path);
177+
(protocol, client.host.clone(), client.port)
178+
} else if uri.starts_with("/tedge") {
179+
let client = &config.http.client;
180+
let protocol = https_if_some(&config.http.cert_path);
181+
(protocol, client.host.clone(), client.port)
182+
} else {
183+
return Err(anyhow!("Not a local HTTP uri: {uri}").into());
184+
};
185+
186+
let url = format!("{protocol}://{host}:{port}{uri}");
187+
let identity = config.http.client.auth.identity()?;
188+
let client = http_client(config.cloud_root_certs(), identity.as_ref())?;
189+
let action = self.into();
190+
191+
Ok(HttpCommand {
192+
client,
193+
url,
194+
action,
195+
}
196+
.into_boxed())
197+
}
198+
}
199+
200+
impl From<TEdgeHttpCli> for HttpAction {
201+
fn from(value: TEdgeHttpCli) -> Self {
202+
match value {
203+
TEdgeHttpCli::Post {
204+
content,
205+
content_type,
206+
accept_type,
207+
..
208+
} => HttpAction::Post {
209+
content,
210+
content_type,
211+
accept_type,
212+
},
213+
TEdgeHttpCli::Put {
214+
content,
215+
content_type,
216+
accept_type,
217+
..
218+
} => HttpAction::Put {
219+
content,
220+
content_type,
221+
accept_type,
222+
},
223+
TEdgeHttpCli::Get { accept_type, .. } => HttpAction::Get { accept_type },
224+
TEdgeHttpCli::Delete { .. } => HttpAction::Delete,
225+
}
226+
}
227+
}
228+
229+
impl TEdgeHttpCli {
230+
fn uri(&self) -> &str {
231+
match self {
232+
TEdgeHttpCli::Post { uri, .. }
233+
| TEdgeHttpCli::Put { uri, .. }
234+
| TEdgeHttpCli::Get { uri, .. }
235+
| TEdgeHttpCli::Delete { uri, .. } => uri.as_ref(),
236+
}
237+
}
238+
239+
fn c8y_profile(&self) -> Option<&ProfileName> {
240+
match self {
241+
TEdgeHttpCli::Post { profile, .. }
242+
| TEdgeHttpCli::Put { profile, .. }
243+
| TEdgeHttpCli::Get { profile, .. }
244+
| TEdgeHttpCli::Delete { profile, .. } => profile.as_ref(),
245+
}
246+
}
247+
}
248+
249+
fn https_if_some<T>(cert_path: &OptionalConfig<T>) -> &'static str {
250+
cert_path.or_none().map_or("http", |_| "https")
251+
}
252+
253+
fn http_client(
254+
root_certs: CloudRootCerts,
255+
identity: Option<&Identity>,
256+
) -> Result<blocking::Client, Error> {
257+
let builder = root_certs.blocking_client_builder();
258+
let builder = if let Some(identity) = identity {
259+
builder.identity(identity.clone())
260+
} else {
261+
builder
262+
};
263+
Ok(builder.build()?)
264+
}
265+
266+
impl Content {
267+
pub fn length(&self) -> Option<usize> {
268+
if let Some(content) = &self.arg2 {
269+
Some(content.len())
270+
} else if let Some(data) = &self.data {
271+
Some(data.len())
272+
} else if let Some(file) = &self.file {
273+
Some(std::fs::metadata(file).ok()?.len().try_into().ok()?)
274+
} else {
275+
None
276+
}
277+
}
278+
279+
pub fn mime_type(&self) -> Option<String> {
280+
let file = self.file.as_ref()?;
281+
Some(
282+
mime_guess::from_path(file)
283+
.first_or_octet_stream()
284+
.to_string(),
285+
)
286+
}
287+
}

0 commit comments

Comments
 (0)