Skip to content

Commit

Permalink
Add support for Cfd Tunnels (#237)
Browse files Browse the repository at this point in the history
Now that Argo Tunnel is a deprecated API, this commit adds support for
Cfd Tunnels

---------

Co-authored-by: GoncaloGarcia <[email protected]>
  • Loading branch information
GoncaloGarcia and GoncaloGarcia authored Jun 6, 2024
1 parent b4fa5ef commit 2b1e12a
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
name: Formatter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Check Formatting
run: cargo fmt --all -- --check
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
rust: [stable]

steps:
- uses: hecrj/setup-rust-action@v1
- uses: hecrj/setup-rust-action@v2
with:
rust-version: ${{ matrix.rust }}
- uses: actions/checkout@master
Expand Down
9 changes: 6 additions & 3 deletions cloudflare-examples/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ struct Section<'a> {
function: SectionFunction<HttpApiClient>,
}

fn print_response<T: ApiResult>(response: ApiResponse<T>) {
fn print_response<T>(response: ApiResponse<T>)
where
T: ApiResult,
{
match response {
Ok(success) => println!("Success: {success:#?}"),
Err(e) => match e {
Expand All @@ -43,9 +46,9 @@ fn print_response<T: ApiResult>(response: ApiResponse<T>) {
}

/// Sometimes you want to pipe results to jq etc
fn print_response_json<T: ApiResult>(response: ApiResponse<T>)
fn print_response_json<T>(response: ApiResponse<T>)
where
T: Serialize,
T: ApiResult + Serialize,
{
match response {
Ok(success) => println!("{}", serde_json::to_string(&success.result).unwrap()),
Expand Down
51 changes: 51 additions & 0 deletions cloudflare/src/endpoints/cfd_tunnel/create_tunnel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::endpoints::cfd_tunnel::{ConfigurationSrc, Tunnel};
use serde::Serialize;
use serde_with::{
base64::{Base64, Standard},
formats::Padded,
serde_as,
};

use crate::framework::endpoint::{EndpointSpec, Method};

/// Create a Cfd Tunnel
/// This creates the Tunnel, which can then be routed and ran. Creating the Tunnel per se is only
/// a metadata operation (i.e. no Tunnel is running at this point).
/// <https://developers.cloudflare.com/api/operations/cloudflare-tunnel-create-a-cloudflare-tunnel>
#[derive(Debug)]
pub struct CreateTunnel<'a> {
pub account_identifier: &'a str,
pub params: Params<'a>,
}

impl<'a> EndpointSpec<Tunnel> for CreateTunnel<'a> {
fn method(&self) -> Method {
Method::POST
}
fn path(&self) -> String {
format!("accounts/{}/cfd_tunnel", self.account_identifier)
}
#[inline]
fn body(&self) -> Option<String> {
let body = serde_json::to_string(&self.params).unwrap();
Some(body)
}
}

/// Params for creating a Named Argo Tunnel
#[serde_as]
#[serde_with::skip_serializing_none]
#[derive(Serialize, Clone, Debug)]
pub struct Params<'a> {
/// The name for the Tunnel to be created. It must be unique within the account.
pub name: &'a str,
/// The byte array (with 32 or more bytes) representing a secret for the tunnel. This is
/// encoded into JSON as a base64 String. This secret is necessary to run the tunnel.
#[serde_as(as = "Base64<Standard, Padded>")]
pub tunnel_secret: &'a Vec<u8>,

pub config_src: &'a ConfigurationSrc,

/// Arbitrary metadata for the tunnel.
pub metadata: Option<serde_json::Value>,
}
103 changes: 103 additions & 0 deletions cloudflare/src/endpoints/cfd_tunnel/data_structures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use chrono::{offset::Utc, DateTime};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use uuid::Uuid;

use crate::framework::response::ApiResult;

/// A Cfd Tunnel
/// This is an Cfd Tunnel that has been created. It can be used for routing and subsequent running.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct Tunnel {
pub id: Uuid,
pub created_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
pub name: String,
pub connections: Vec<ActiveConnection>,
pub metadata: serde_json::Value,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
pub struct TunnelWithConnections {
pub id: Uuid,
pub account_tag: String,
pub created_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
pub name: String,
pub connections: Vec<ActiveConnection>,
pub conns_active_at: Option<DateTime<Utc>>,
pub conns_inactive_at: Option<DateTime<Utc>>,
// tun_type can be inferred from metadata
#[serde(flatten)]
pub metadata: serde_json::Value,
pub status: TunnelStatusType,
// This field is only present for tunnels that make sense to report (e.g: Cfd_Tunnel), which
// are the ones that can be managed via UI or dash in terms of their YAML file.
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_config: Option<bool>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Copy, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum TunnelStatusType {
Inactive, // Tunnel has been created but a connection has yet to be registered
Down, // Tunnel is down and all connections are unregistered
Degraded, // Tunnel health is degraded but still serving connections
Healthy, // Tunnel is healthy
}

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum ConfigurationSrc {
#[serde(rename = "local")]
#[default]
Local,
#[serde(rename = "cloudflare")]
Cloudflare,
}
/// An active connection for a Cfd Tunnel
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Hash)]
pub struct ActiveConnection {
pub colo_name: String,
/// Deprecated, use `id` instead.
pub uuid: Uuid,
pub id: Uuid,
pub is_pending_reconnect: bool,
pub origin_ip: IpAddr,
pub opened_at: DateTime<Utc>,
pub client_id: Uuid,
pub client_version: String,
}

impl ApiResult for Tunnel {}
impl ApiResult for Vec<Tunnel> {}

/// The result of a route request for a Cfd Tunnel
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum RouteResult {
Dns(DnsRouteResult),
Lb(LoadBalancerRouteResult),
}

#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct DnsRouteResult {
pub cname: Change,
pub name: String,
pub dns_tag: String,
}

#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct LoadBalancerRouteResult {
pub load_balancer: Change,
pub pool: Change,
}

#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Change {
Unchanged,
New,
Updated,
}

impl ApiResult for RouteResult {}
36 changes: 36 additions & 0 deletions cloudflare/src/endpoints/cfd_tunnel/delete_tunnel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::framework::endpoint::{serialize_query, EndpointSpec, Method};
use serde::Serialize;

use super::Tunnel;

/// Delete a tunnel
/// <https://developers.cloudflare.com/api/operations/cloudflare-tunnel-delete-a-cloudflare-tunnel>
#[derive(Debug)]
pub struct DeleteTunnel<'a> {
pub account_identifier: &'a str,
pub tunnel_id: &'a str,
pub params: Params,
}

impl<'a> EndpointSpec<Tunnel> for DeleteTunnel<'a> {
fn method(&self) -> Method {
Method::DELETE
}
fn path(&self) -> String {
format!(
"accounts/{}/cfd_tunnel/{}",
self.account_identifier, self.tunnel_id
)
}
#[inline]
fn query(&self) -> Option<String> {
serialize_query(&self.params)
}
}

#[serde_with::skip_serializing_none]
#[derive(Serialize, Clone, Debug, Default)]
pub struct Params {
// should delete tunnel connections if any exists
pub cascade: bool,
}
48 changes: 48 additions & 0 deletions cloudflare/src/endpoints/cfd_tunnel/list_tunnels.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::endpoints::cfd_tunnel::Tunnel;
use chrono::{DateTime, Utc};
use serde::Serialize;

use crate::framework::endpoint::{serialize_query, EndpointSpec, Method};

/// List/search tunnels in an account.
/// <https://developers.cloudflare.com/api/operations/cloudflare-tunnel-list-cloudflare-tunnels>
#[derive(Debug)]
pub struct ListTunnels<'a> {
pub account_identifier: &'a str,
pub params: Params,
}

impl<'a> EndpointSpec<Vec<Tunnel>> for ListTunnels<'a> {
fn method(&self) -> Method {
Method::GET
}
fn path(&self) -> String {
format!("accounts/{}/cfd_tunnel", self.account_identifier)
}
#[inline]
fn query(&self) -> Option<String> {
serialize_query(&self.params)
}
}

/// Params for filtering listed tunnels
#[serde_with::skip_serializing_none]
#[derive(Serialize, Clone, Debug, Default)]
pub struct Params {
pub name: Option<String>,
pub uuid: Option<String>,
pub is_deleted: Option<bool>,
pub existed_at: Option<DateTime<Utc>>,
pub was_active_at: Option<DateTime<Utc>>,
pub include_prefix: Option<String>,
pub was_inactive_at: Option<DateTime<Utc>>,
pub exclude_prefix: Option<String>,
#[serde(flatten)]
pub pagination_params: Option<PaginationParams>,
}

#[derive(Serialize, Clone, Debug)]
pub struct PaginationParams {
pub page: u64,
pub per_page: u64,
}
8 changes: 8 additions & 0 deletions cloudflare/src/endpoints/cfd_tunnel/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub mod create_tunnel;
mod data_structures;
pub mod delete_tunnel;
pub mod list_tunnels;
pub mod route_dns;
pub mod update_tunnel;

pub use data_structures::*;
39 changes: 39 additions & 0 deletions cloudflare/src/endpoints/cfd_tunnel/route_dns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use crate::framework::endpoint::{EndpointSpec, Method};

use super::RouteResult;
use serde::Serialize;
use uuid::Uuid;

/// Route for a Named Argo Tunnel
/// This creates a new route for the identified Tunnel. More than 1 route may co-exist for the same
/// Tunnel.
/// Note that this modifies only metadata on Cloudflare side to route traffic to the Tunnel, but
/// it is still up to the user to run the Tunnel to receive that traffic.
#[derive(Debug)]
pub struct RouteTunnel<'a> {
pub zone_tag: &'a str,
pub tunnel_id: Uuid,
pub params: Params<'a>,
}

impl<'a> EndpointSpec<RouteResult> for RouteTunnel<'a> {
fn method(&self) -> Method {
Method::PUT
}
fn path(&self) -> String {
format!("zones/{}/tunnels/{}/routes", self.zone_tag, self.tunnel_id)
}
#[inline]
fn body(&self) -> Option<String> {
let body = serde_json::to_string(&self.params).unwrap();
Some(body)
}
}

/// Params for routing a Named Argo Tunnel
#[derive(Serialize, Clone, Debug)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Params<'a> {
Dns { user_hostname: &'a str },
Lb { lb_name: &'a str, lb_pool: &'a str },
}
53 changes: 53 additions & 0 deletions cloudflare/src/endpoints/cfd_tunnel/update_tunnel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::endpoints::cfd_tunnel::Tunnel;
use serde::Serialize;
use serde_with::{
base64::{Base64, Standard},
formats::Padded,
serde_as,
};

use crate::framework::endpoint::{EndpointSpec, Method};

/// Create a Cfd Tunnel
/// This creates the Tunnel, which can then be routed and ran. Creating the Tunnel per se is only
/// a metadata operation (i.e. no Tunnel is running at this point).
/// <https://developers.cloudflare.com/api/operations/cloudflare-tunnel-create-a-cloudflare-tunnel>
#[derive(Debug)]
pub struct UpdateTunnel<'a> {
pub account_identifier: &'a str,
pub tunnel_id: &'a str,
pub params: Params<'a>,
}

impl<'a> EndpointSpec<Tunnel> for UpdateTunnel<'a> {
fn method(&self) -> Method {
Method::PATCH
}
fn path(&self) -> String {
format!(
"accounts/{}/cfd_tunnel/{}",
self.account_identifier, self.tunnel_id
)
}
#[inline]
fn body(&self) -> Option<String> {
let body = serde_json::to_string(&self.params).unwrap();
Some(body)
}
}

/// Params for updating a Cfd Tunnel
#[serde_as]
#[serde_with::skip_serializing_none]
#[derive(Serialize, Clone, Debug)]
pub struct Params<'a> {
/// The new name for the Tunnel
pub name: &'a str,
/// The byte array (with 32 or more bytes) representing a secret for the tunnel. This is
/// encoded into JSON as a base64 String. This secret is necessary to run the tunnel.
#[serde_as(as = "Base64<Standard, Padded>")]
pub tunnel_secret: &'a Vec<u8>,

/// Arbitrary metadata for the tunnel.
pub metadata: Option<serde_json::Value>,
}
1 change: 1 addition & 0 deletions cloudflare/src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.
*/
pub mod account;
pub mod argo_tunnel;
pub mod cfd_tunnel;
pub mod dns;
pub mod load_balancing;
pub mod plan;
Expand Down
Loading

0 comments on commit 2b1e12a

Please sign in to comment.