Skip to content

Commit 2f3ac7d

Browse files
committed
feat(network-group): introduce Network Group API
1 parent 12d2ef1 commit 2f3ac7d

File tree

8 files changed

+724
-1
lines changed

8 files changed

+724
-1
lines changed

src/v4/mod.rs

100644100755
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
mod error;
66
pub use error::HttpError;
77

8-
pub type ErrorResponse = oauth10a::rest::ErrorResponse<HttpError>;
8+
pub type ErrorResponse = oauth10a::rest::ErrorResponse<Box<HttpError>>;
99

1010
pub mod addon_provider;
1111
pub mod functions;
12+
#[cfg(feature = "network-group")]
13+
pub mod network_group;
1214
pub mod products;

src/v4/network_group/delete.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use oauth10a::rest::RestClient;
2+
3+
use crate::{Client, EndpointError, RestError, v4::ErrorResponse};
4+
5+
use super::{OwnerId, network_group_id::NetworkGroupId, peer::PeerId};
6+
7+
#[derive(Debug, thiserror::Error)]
8+
pub enum Error {
9+
#[error(transparent)]
10+
Endpoint(#[from] EndpointError),
11+
#[error("failed to delete for owner '{0}', network group '{1}', peer id '{2}', {0}")]
12+
Delete(String, NetworkGroupId, PeerId, RestError),
13+
#[error(transparent)]
14+
Status(#[from] ErrorResponse),
15+
}
16+
17+
pub async fn delete(
18+
client: &Client,
19+
owner_id: &OwnerId,
20+
ng_id: &NetworkGroupId,
21+
peer_id: PeerId,
22+
) -> Result<(), Error> {
23+
let endpoint = client.endpoint(format_args!(
24+
"/v4/networkgroups/organisations/{owner_id}/networkgroups/{ng_id}/external-peers/{peer_id}"
25+
))?;
26+
27+
debug!(
28+
%endpoint,
29+
owner = %owner_id,
30+
network_group = %ng_id,
31+
peer = %peer_id,
32+
"execute a request to delete peer from Network Group"
33+
);
34+
35+
Ok(client
36+
.delete(endpoint)
37+
.await
38+
.map_err(|e| Error::Delete(owner_id.to_owned(), *ng_id, peer_id, e))??)
39+
}

src/v4/network_group/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//! # Network Group module
2+
//!
3+
//! This module provide structures and helpers to interact with Clever Cloud's
4+
//! Network Group API.
5+
6+
pub type MemberId = String;
7+
8+
pub type OwnerId = String;
9+
10+
pub mod delete;
11+
pub mod network_group_id;
12+
pub mod peer;
13+
pub mod wannabe_external_peer;
14+
pub mod wireguard;
15+
pub mod wireguard_configuration;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use core::{fmt, str};
2+
use std::path::PathBuf;
3+
4+
use serde::{Deserialize, Serialize};
5+
use uuid::Uuid;
6+
7+
// NETWORK GROUP ID PARSE ERROR ////////////////////////////////////////////////
8+
9+
#[derive(Debug, thiserror::Error)]
10+
enum NetworkGroupIdParseErrorKind {
11+
#[error("expected `ng_` prefix")]
12+
MissingPrefix,
13+
#[error("invalid UUID, {0}")]
14+
Uuid(uuid::Error),
15+
}
16+
17+
#[derive(Debug, thiserror::Error)]
18+
#[error("invalid network group identifier: {kind}")]
19+
pub struct NetworkGroupIdParseError {
20+
kind: NetworkGroupIdParseErrorKind,
21+
}
22+
23+
// NETWORK GROUP ID ////////////////////////////////////////////////////////////
24+
25+
/// A network group identifier.
26+
///
27+
/// It is represented as an hyphenate lowercased UUID prefixed with `ng_`.
28+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29+
pub struct NetworkGroupId(Uuid);
30+
31+
impl NetworkGroupId {
32+
/// Returns the name of the file in which the peer identifier is stored.
33+
pub fn ng_info_file_name(&self) -> PathBuf {
34+
PathBuf::from(self.to_string()).with_extension("id")
35+
}
36+
37+
/// Returns the associated Wireguard interface name.
38+
pub fn wg_interface_name(&self) -> String {
39+
let time_low = &self.0.to_string()[..8];
40+
format!("wgcc{time_low}")
41+
}
42+
}
43+
44+
impl str::FromStr for NetworkGroupId {
45+
type Err = NetworkGroupIdParseError;
46+
47+
fn from_str(s: &str) -> Result<Self, Self::Err> {
48+
match s.strip_prefix("ng_") {
49+
Some(s) => match s.parse::<uuid::fmt::Hyphenated>() {
50+
Ok(hyphenated) => Ok(Self(hyphenated.into_uuid())),
51+
Err(e) => Err(NetworkGroupIdParseError {
52+
kind: NetworkGroupIdParseErrorKind::Uuid(e),
53+
}),
54+
},
55+
None => Err(NetworkGroupIdParseError {
56+
kind: NetworkGroupIdParseErrorKind::MissingPrefix,
57+
}),
58+
}
59+
}
60+
}
61+
62+
impl fmt::Display for NetworkGroupId {
63+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64+
write!(f, "ng_{}", self.0.as_hyphenated())
65+
}
66+
}
67+
68+
impl<'de> Deserialize<'de> for NetworkGroupId {
69+
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
70+
String::deserialize(deserializer)?
71+
.parse()
72+
.map_err(serde::de::Error::custom)
73+
}
74+
}
75+
76+
impl Serialize for NetworkGroupId {
77+
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
78+
self.to_string().serialize(serializer)
79+
}
80+
}

src/v4/network_group/peer.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use core::{fmt, str};
2+
3+
use serde::{Deserialize, Serialize};
4+
use uuid::Uuid;
5+
6+
// PEER KIND ///////////////////////////////////////////////////////////////////
7+
8+
#[derive(
9+
Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize,
10+
)]
11+
pub enum PeerKind {
12+
#[default]
13+
#[serde(rename = "CLEVER")]
14+
Clever,
15+
#[serde(rename = "EXTERNAL")]
16+
External,
17+
}
18+
19+
impl PeerKind {
20+
/// Returns `true` if the peer kind is [`External`](PeerKind::External).
21+
pub const fn is_external(&self) -> bool {
22+
matches!(self, Self::External)
23+
}
24+
25+
const fn prefix(self) -> Option<&'static str> {
26+
match self {
27+
Self::Clever => None,
28+
Self::External => Some("external"),
29+
}
30+
}
31+
32+
const fn from_prefix(prefix: &str) -> Option<Self> {
33+
match prefix.as_bytes() {
34+
b"external" => Some(Self::External),
35+
_ => None,
36+
}
37+
}
38+
}
39+
40+
// PEER ID PARSE ERROR /////////////////////////////////////////////////////////
41+
42+
#[derive(Debug, thiserror::Error)]
43+
enum PeerIdParseErrorKind {
44+
#[error("unknown peer kind, {0}")]
45+
PeerKind(String),
46+
#[error("invalid UUID, {0}")]
47+
Uuid(uuid::Error),
48+
}
49+
50+
#[derive(Debug, thiserror::Error)]
51+
#[error("invalid peer identifier: {kind}")]
52+
pub struct PeerIdParseError {
53+
kind: PeerIdParseErrorKind,
54+
}
55+
56+
// PEER ID /////////////////////////////////////////////////////////////////////
57+
58+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
59+
pub struct PeerId {
60+
peer_kind: PeerKind,
61+
uuid: Uuid,
62+
}
63+
64+
impl PeerId {
65+
pub const fn peer_kind(&self) -> PeerKind {
66+
self.peer_kind
67+
}
68+
69+
pub const fn uuid(&self) -> &Uuid {
70+
&self.uuid
71+
}
72+
73+
pub const fn is_external(&self) -> bool {
74+
self.peer_kind.is_external()
75+
}
76+
}
77+
78+
impl fmt::Display for PeerId {
79+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80+
if let Some(prefix) = self.peer_kind.prefix() {
81+
write!(f, "{prefix}_")?;
82+
}
83+
write!(f, "{}", self.uuid.as_hyphenated())
84+
}
85+
}
86+
87+
impl str::FromStr for PeerId {
88+
type Err = PeerIdParseError;
89+
90+
fn from_str(s: &str) -> Result<Self, Self::Err> {
91+
let (s, peer_kind) = match s.split_once('_') {
92+
None => (s, PeerKind::default()),
93+
Some((prefix, s)) => match PeerKind::from_prefix(prefix) {
94+
Some(peer_kind) => (s, peer_kind),
95+
None => {
96+
return Err(PeerIdParseError {
97+
kind: PeerIdParseErrorKind::PeerKind(prefix.to_owned()),
98+
});
99+
}
100+
},
101+
};
102+
match s.parse::<uuid::fmt::Hyphenated>() {
103+
Ok(hyphenated) => Ok(Self {
104+
uuid: hyphenated.into_uuid(),
105+
peer_kind,
106+
}),
107+
Err(e) => Err(PeerIdParseError {
108+
kind: PeerIdParseErrorKind::Uuid(e),
109+
}),
110+
}
111+
}
112+
}
113+
114+
impl<'de> Deserialize<'de> for PeerId {
115+
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116+
String::deserialize(deserializer)?
117+
.parse()
118+
.map_err(serde::de::Error::custom)
119+
}
120+
}
121+
122+
impl Serialize for PeerId {
123+
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
124+
self.to_string().serialize(serializer)
125+
}
126+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use core::net::IpAddr;
2+
3+
use oauth10a::rest::RestClient;
4+
use serde::{Deserialize, Serialize};
5+
6+
use crate::{Client, EndpointError, RestError, v4::ErrorResponse};
7+
8+
use super::{
9+
MemberId, OwnerId, network_group_id::NetworkGroupId, peer::PeerId,
10+
wireguard::WireGuardPublicKey,
11+
};
12+
13+
// PEER ROLE ///////////////////////////////////////////////////////////////////
14+
15+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16+
pub enum PeerRole {
17+
#[serde(rename = "CLIENT")]
18+
Client,
19+
#[serde(rename = "SERVER")]
20+
Server,
21+
}
22+
23+
// PEER CREATED ////////////////////////////////////////////////////////////////
24+
25+
/// Response from [`WannabePeer`] and [`WannabeExternalPeer`] requests.
26+
#[derive(Debug, Deserialize)]
27+
pub struct PeerCreated {
28+
#[serde(rename = "peerId")]
29+
pub peer_id: PeerId,
30+
}
31+
32+
// ERROR ///////////////////////////////////////////////////////////////////////
33+
34+
#[derive(Debug, thiserror::Error)]
35+
pub enum Error {
36+
#[error(transparent)]
37+
Endpoint(#[from] EndpointError),
38+
#[error("failed to post Wannabe External Peer for owner '{0}', network group '{1}', {0}")]
39+
Post(String, NetworkGroupId, RestError),
40+
#[error(transparent)]
41+
Status(#[from] ErrorResponse),
42+
}
43+
44+
// WANNABE EXTERNAL PEER ///////////////////////////////////////////////////////
45+
46+
/// Request to join a Network Group as an external peer.
47+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48+
pub struct WannabeExternalPeer {
49+
/// Label of a Network Group peer.
50+
#[serde(rename = "label")]
51+
pub label: String,
52+
/// The public IP v4 or v6 on which the peer is listening,
53+
/// when `peer_role` is [`PeerRole::Server`].
54+
#[serde(rename = "ip", skip_serializing_if = "Option::is_none")]
55+
pub ip: Option<IpAddr>,
56+
/// The public TCP or UDP port number on which the peer is listening,
57+
/// when `peer_role` is [`PeerRole::Server`].
58+
#[serde(rename = "port", skip_serializing_if = "Option::is_none")]
59+
pub port: Option<u16>,
60+
/// The role of this peer in the Network Group.
61+
#[serde(rename = "peerRole")]
62+
pub peer_role: PeerRole,
63+
/// Base64-encoded WireGuard public key of the peer.
64+
#[serde(rename = "publicKey")]
65+
pub public_key: WireGuardPublicKey,
66+
/// Host name of the peer.
67+
#[serde(rename = "hostname", skip_serializing_if = "Option::is_none")]
68+
pub hostname: Option<String>,
69+
/// Event that created the peer within the Network Group.
70+
#[serde(rename = "parentEvent", skip_serializing_if = "Option::is_none")]
71+
pub parent_event: Option<String>,
72+
/// Unique ID of a Network Group member.
73+
#[serde(rename = "parentMember")]
74+
pub parent_member: MemberId,
75+
}
76+
77+
impl WannabeExternalPeer {
78+
pub async fn post(
79+
&self,
80+
client: &Client,
81+
owner_id: &OwnerId,
82+
ng_id: &NetworkGroupId,
83+
) -> Result<PeerCreated, Error> {
84+
let endpoint = client.endpoint(format_args!(
85+
"/v4/networkgroups/organisations/{owner_id}/networkgroups/{ng_id}/external-peers"
86+
))?;
87+
88+
debug!(
89+
%endpoint,
90+
owner = %owner_id,
91+
network_group = %ng_id,
92+
"execute a request to join Network Group"
93+
);
94+
95+
Ok(client
96+
.post(endpoint, self)
97+
.await
98+
.map_err(|e| Error::Post(owner_id.to_owned(), *ng_id, e))??)
99+
}
100+
}

0 commit comments

Comments
 (0)