Skip to content

Commit 6387a04

Browse files
authored
Merge pull request #193 from ngrok/bmps/allow-implicit-bindings
allow implicit bindings
2 parents 614f1db + 605ec86 commit 6387a04

File tree

6 files changed

+370
-16
lines changed

6 files changed

+370
-16
lines changed

ngrok/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
## Unreleased
2+
3+
### Breaking Changes
4+
- **Binding is now optional**: Tests no longer hardcode `binding("public")`. The ngrok service will use its default binding configuration when not explicitly specified.
5+
- **Binding validation**: The `binding()` method now validates input values and panics on invalid values or multiple calls.
6+
7+
### Added
8+
- Added `Binding` enum with three variants: `Public`, `Internal`, and `Kubernetes`
9+
- Added validation for binding values - only "public", "internal", and "kubernetes" are accepted (case-insensitive)
10+
- Added `binding()` method documentation with examples for both string and typed enum usage
11+
- Added panic behavior when `binding()` is called more than once (only one binding allowed)
12+
13+
### Changed
14+
- `binding()` method now accepts both strings and the `Binding` enum via `Into<String>`
15+
- Removed hardcoded "public" binding from all tests - bindings are now truly optional
16+
117
## 0.15.0
218
- - Removes `hyper-proxy` and `ring` dependencies
319

ngrok/src/config/common.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,69 @@ use crate::{
2323
Tunnel,
2424
};
2525

26+
/// Represents the ingress configuration for an ngrok endpoint.
27+
///
28+
/// Bindings determine where and how your endpoint is exposed.
29+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30+
pub enum Binding {
31+
/// Publicly accessible endpoint (default for most configurations).
32+
Public,
33+
/// Internal-only endpoint, not accessible from the public internet.
34+
Internal,
35+
/// Kubernetes cluster binding for service mesh integration.
36+
Kubernetes,
37+
}
38+
39+
impl Binding {
40+
/// Returns the string representation of this binding.
41+
pub fn as_str(&self) -> &'static str {
42+
match self {
43+
Binding::Public => "public",
44+
Binding::Internal => "internal",
45+
Binding::Kubernetes => "kubernetes",
46+
}
47+
}
48+
49+
/// Validates if a string is a recognized binding value.
50+
pub(crate) fn validate(s: &str) -> Result<(), String> {
51+
match s.to_lowercase().as_str() {
52+
"public" | "internal" | "kubernetes" => Ok(()),
53+
_ => Err(format!(
54+
"Invalid binding value '{}'. Expected 'public', 'internal', or 'kubernetes'",
55+
s
56+
)),
57+
}
58+
}
59+
}
60+
61+
impl From<Binding> for String {
62+
fn from(binding: Binding) -> String {
63+
binding.as_str().to_string()
64+
}
65+
}
66+
67+
impl std::str::FromStr for Binding {
68+
type Err = String;
69+
70+
fn from_str(s: &str) -> Result<Self, Self::Err> {
71+
match s.to_lowercase().as_str() {
72+
"public" => Ok(Binding::Public),
73+
"internal" => Ok(Binding::Internal),
74+
"kubernetes" => Ok(Binding::Kubernetes),
75+
_ => Err(format!(
76+
"Invalid binding value '{}'. Expected 'public', 'internal', or 'kubernetes'",
77+
s
78+
)),
79+
}
80+
}
81+
}
82+
83+
impl std::fmt::Display for Binding {
84+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85+
write!(f, "{}", self.as_str())
86+
}
87+
}
88+
2689
pub(crate) fn default_forwards_to() -> &'static str {
2790
static FORWARDS_TO: OnceCell<String> = OnceCell::new();
2891

ngrok/src/config/http.rs

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::{
2323
config::{
2424
common::{
2525
default_forwards_to,
26+
Binding,
2627
CommonOpts,
2728
TunnelConfig,
2829
},
@@ -261,9 +262,45 @@ impl HttpTunnelBuilder {
261262
self.options.common_opts.metadata = Some(metadata.into());
262263
self
263264
}
264-
/// Sets the ingress configuration for this endpoint
265+
266+
/// Sets the ingress configuration for this endpoint.
267+
///
268+
/// Valid binding values are:
269+
/// - `"public"` - Publicly accessible endpoint
270+
/// - `"internal"` - Internal-only endpoint
271+
/// - `"kubernetes"` - Kubernetes cluster binding
272+
///
273+
/// If not specified, the ngrok service will use its default binding configuration.
274+
///
275+
/// # Panics
276+
///
277+
/// Panics if called more than once or if an invalid binding value is provided.
278+
///
279+
/// # Examples
280+
///
281+
/// ```no_run
282+
/// # use ngrok::Session;
283+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
284+
/// let session = Session::builder().authtoken_from_env().connect().await?;
285+
///
286+
/// // Using string
287+
/// let tunnel = session.http_endpoint().binding("internal").listen().await?;
288+
///
289+
/// // Using typed enum
290+
/// use ngrok::config::Binding;
291+
/// let tunnel = session.http_endpoint().binding(Binding::Public).listen().await?;
292+
/// # Ok(())
293+
/// # }
294+
/// ```
265295
pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
266-
self.options.bindings.push(binding.into());
296+
if !self.options.bindings.is_empty() {
297+
panic!("binding() can only be called once");
298+
}
299+
let binding_str = binding.into();
300+
if let Err(e) = Binding::validate(&binding_str) {
301+
panic!("{}", e);
302+
}
303+
self.options.bindings.push(binding_str);
267304
self
268305
}
269306
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
@@ -482,7 +519,6 @@ impl HttpTunnelBuilder {
482519
mod test {
483520
use super::*;
484521
use crate::config::policies::test::POLICY_JSON;
485-
const BINDING: &str = "public";
486522
const METADATA: &str = "testmeta";
487523
const TEST_FORWARD: &str = "testforward";
488524
const TEST_FORWARD_PROTO: &str = "http2";
@@ -509,7 +545,6 @@ mod test {
509545
.deny_cidr(DENY_CIDR)
510546
.proxy_proto(ProxyProto::V2)
511547
.metadata(METADATA)
512-
.binding(BINDING)
513548
.scheme(Scheme::from_str("hTtPs").unwrap())
514549
.domain(DOMAIN)
515550
.mutual_tlsca(CA_CERT.into())
@@ -554,7 +589,7 @@ mod test {
554589
let extra = tunnel_cfg.extra();
555590
assert_eq!(String::default(), *extra.token);
556591
assert_eq!(METADATA, extra.metadata);
557-
assert_eq!(Vec::from([BINDING]), extra.bindings);
592+
assert_eq!(Vec::<String>::new(), extra.bindings);
558593
assert_eq!(String::default(), extra.ip_policy_ref);
559594

560595
assert_eq!("https", tunnel_cfg.proto());
@@ -623,4 +658,61 @@ mod test {
623658

624659
assert_eq!(HashMap::new(), tunnel_cfg.labels());
625660
}
661+
662+
#[test]
663+
fn test_binding_valid_values() {
664+
let mut builder = HttpTunnelBuilder {
665+
session: None,
666+
options: Default::default(),
667+
};
668+
669+
// Test "public"
670+
builder.binding("public");
671+
assert_eq!(vec!["public"], builder.options.bindings);
672+
673+
// Test "internal"
674+
let mut builder = HttpTunnelBuilder {
675+
session: None,
676+
options: Default::default(),
677+
};
678+
builder.binding("internal");
679+
assert_eq!(vec!["internal"], builder.options.bindings);
680+
681+
// Test "kubernetes"
682+
let mut builder = HttpTunnelBuilder {
683+
session: None,
684+
options: Default::default(),
685+
};
686+
builder.binding("kubernetes");
687+
assert_eq!(vec!["kubernetes"], builder.options.bindings);
688+
689+
// Test with Binding enum
690+
let mut builder = HttpTunnelBuilder {
691+
session: None,
692+
options: Default::default(),
693+
};
694+
builder.binding(Binding::Internal);
695+
assert_eq!(vec!["internal"], builder.options.bindings);
696+
}
697+
698+
#[test]
699+
#[should_panic(expected = "Invalid binding value")]
700+
fn test_binding_invalid_value() {
701+
let mut builder = HttpTunnelBuilder {
702+
session: None,
703+
options: Default::default(),
704+
};
705+
builder.binding("invalid");
706+
}
707+
708+
#[test]
709+
#[should_panic(expected = "binding() can only be called once")]
710+
fn test_binding_called_twice() {
711+
let mut builder = HttpTunnelBuilder {
712+
session: None,
713+
options: Default::default(),
714+
};
715+
builder.binding("public");
716+
builder.binding("internal");
717+
}
626718
}

ngrok/src/config/tcp.rs

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use crate::config::{
1818
use crate::{
1919
config::common::{
2020
default_forwards_to,
21+
Binding,
2122
CommonOpts,
2223
TunnelConfig,
2324
},
@@ -127,9 +128,45 @@ impl TcpTunnelBuilder {
127128
self.options.common_opts.metadata = Some(metadata.into());
128129
self
129130
}
130-
/// Sets the ingress configuration for this endpoint
131+
132+
/// Sets the ingress configuration for this endpoint.
133+
///
134+
/// Valid binding values are:
135+
/// - `"public"` - Publicly accessible endpoint
136+
/// - `"internal"` - Internal-only endpoint
137+
/// - `"kubernetes"` - Kubernetes cluster binding
138+
///
139+
/// If not specified, the ngrok service will use its default binding configuration.
140+
///
141+
/// # Panics
142+
///
143+
/// Panics if called more than once or if an invalid binding value is provided.
144+
///
145+
/// # Examples
146+
///
147+
/// ```no_run
148+
/// # use ngrok::Session;
149+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
150+
/// let session = Session::builder().authtoken_from_env().connect().await?;
151+
///
152+
/// // Using string
153+
/// let tunnel = session.tcp_endpoint().binding("internal").listen().await?;
154+
///
155+
/// // Using typed enum
156+
/// use ngrok::config::Binding;
157+
/// let tunnel = session.tcp_endpoint().binding(Binding::Public).listen().await?;
158+
/// # Ok(())
159+
/// # }
160+
/// ```
131161
pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
132-
self.options.bindings.push(binding.into());
162+
if !self.options.bindings.is_empty() {
163+
panic!("binding() can only be called once");
164+
}
165+
let binding_str = binding.into();
166+
if let Err(e) = Binding::validate(&binding_str) {
167+
panic!("{}", e);
168+
}
169+
self.options.bindings.push(binding_str);
133170
self
134171
}
135172
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
@@ -192,7 +229,6 @@ impl TcpTunnelBuilder {
192229
mod test {
193230
use super::*;
194231
use crate::config::policies::test::POLICY_JSON;
195-
const BINDING: &str = "public";
196232
const METADATA: &str = "testmeta";
197233
const TEST_FORWARD: &str = "testforward";
198234
const REMOTE_ADDR: &str = "4.tcp.ngrok.io:1337";
@@ -212,7 +248,6 @@ mod test {
212248
.deny_cidr(DENY_CIDR)
213249
.proxy_proto(ProxyProto::V2)
214250
.metadata(METADATA)
215-
.binding(BINDING)
216251
.remote_addr(REMOTE_ADDR)
217252
.forwards_to(TEST_FORWARD)
218253
.policy(POLICY_JSON)
@@ -230,7 +265,7 @@ mod test {
230265
let extra = tunnel_cfg.extra();
231266
assert_eq!(String::default(), *extra.token);
232267
assert_eq!(METADATA, extra.metadata);
233-
assert_eq!(Vec::from([BINDING]), extra.bindings);
268+
assert_eq!(Vec::<String>::new(), extra.bindings);
234269
assert_eq!(String::default(), extra.ip_policy_ref);
235270

236271
assert_eq!("tcp", tunnel_cfg.proto());
@@ -248,4 +283,61 @@ mod test {
248283

249284
assert_eq!(HashMap::new(), tunnel_cfg.labels());
250285
}
286+
287+
#[test]
288+
fn test_binding_valid_values() {
289+
let mut builder = TcpTunnelBuilder {
290+
session: None,
291+
options: Default::default(),
292+
};
293+
294+
// Test "public"
295+
builder.binding("public");
296+
assert_eq!(vec!["public"], builder.options.bindings);
297+
298+
// Test "internal"
299+
let mut builder = TcpTunnelBuilder {
300+
session: None,
301+
options: Default::default(),
302+
};
303+
builder.binding("internal");
304+
assert_eq!(vec!["internal"], builder.options.bindings);
305+
306+
// Test "kubernetes"
307+
let mut builder = TcpTunnelBuilder {
308+
session: None,
309+
options: Default::default(),
310+
};
311+
builder.binding("kubernetes");
312+
assert_eq!(vec!["kubernetes"], builder.options.bindings);
313+
314+
// Test with Binding enum
315+
let mut builder = TcpTunnelBuilder {
316+
session: None,
317+
options: Default::default(),
318+
};
319+
builder.binding(Binding::Public);
320+
assert_eq!(vec!["public"], builder.options.bindings);
321+
}
322+
323+
#[test]
324+
#[should_panic(expected = "Invalid binding value")]
325+
fn test_binding_invalid_value() {
326+
let mut builder = TcpTunnelBuilder {
327+
session: None,
328+
options: Default::default(),
329+
};
330+
builder.binding("invalid");
331+
}
332+
333+
#[test]
334+
#[should_panic(expected = "binding() can only be called once")]
335+
fn test_binding_called_twice() {
336+
let mut builder = TcpTunnelBuilder {
337+
session: None,
338+
options: Default::default(),
339+
};
340+
builder.binding("public");
341+
builder.binding("internal");
342+
}
251343
}

0 commit comments

Comments
 (0)