Skip to content

Commit 850ece9

Browse files
feat(boil): Add support for port in registry host (#1281)
* feat(boil): Add support for port in registry host * chore: Apply suggestions Co-authored-by: Siegfried Weber <[email protected]> * test(boil): Improve HostPort unit tests --------- Co-authored-by: Siegfried Weber <[email protected]>
1 parent 333e0f5 commit 850ece9

File tree

3 files changed

+139
-14
lines changed

3 files changed

+139
-14
lines changed

rust/boil/src/build/bakefile.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ use semver::Version;
1414
use serde::Serialize;
1515
use snafu::{OptionExt, ResultExt, Snafu};
1616
use time::format_description::well_known::Rfc3339;
17-
use url::Host;
1817

1918
use crate::{
2019
VersionExt,
2120
build::{
22-
cli,
21+
cli::{self, HostPort},
2322
docker::{BuildArgument, BuildArguments, LABEL_BUILD_DATE, ParseBuildArgumentsError},
2423
image::{Image, ImageConfig, ImageConfigError, ImageOptions, VersionOptionsPair},
2524
platform::TargetPlatform,
@@ -311,7 +310,7 @@ impl Bakefile {
311310

312311
// The image registry, eg. `oci.stackable.tech` or `localhost`
313312
let image_registry = if args.use_localhost_registry {
314-
&Host::Domain(String::from("localhost"))
313+
&HostPort::localhost()
315314
} else {
316315
&args.registry
317316
};

rust/boil/src/build/cli.rs

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
use std::{path::PathBuf, str::FromStr};
1+
use std::{
2+
fmt::{Debug, Display},
3+
path::PathBuf,
4+
str::FromStr,
5+
};
26

37
use clap::{Args, ValueHint, value_parser};
48
use semver::Version;
59
use snafu::{ResultExt, Snafu, ensure};
10+
use strum::EnumDiscriminants;
611
use url::Host;
712

813
use crate::build::{
@@ -39,14 +44,14 @@ pub struct BuildArguments {
3944
pub target_platform: TargetPlatform,
4045

4146
/// Image registry used in image manifests, URIs, and tags.
47+
/// The format is host[:port].
4248
#[arg(
4349
short, long,
44-
default_value_t = Self::default_registry(),
45-
value_parser = Host::parse,
50+
default_value_t = HostPort::localhost(),
4651
value_hint = ValueHint::Hostname,
4752
help_heading = "Registry Options"
4853
)]
49-
pub registry: Host,
54+
pub registry: HostPort,
5055

5156
/// The namespace within the given registry.
5257
#[arg(
@@ -125,10 +130,6 @@ impl BuildArguments {
125130
TargetPlatform::Linux(Architecture::Amd64)
126131
}
127132

128-
fn default_registry() -> Host {
129-
Host::Domain(String::from("oci.stackable.tech"))
130-
}
131-
132133
fn default_target_containerfile() -> PathBuf {
133134
PathBuf::from("Dockerfile")
134135
}
@@ -149,3 +150,129 @@ pub fn parse_image_version(input: &str) -> Result<Version, ParseImageVersionErro
149150

150151
Ok(version)
151152
}
153+
154+
#[derive(Debug, PartialEq, Snafu, EnumDiscriminants)]
155+
pub enum ParseHostPortError {
156+
#[snafu(display("unexpected empty input"))]
157+
EmptyInput,
158+
159+
#[snafu(display("invalid format, expected host[:port]"))]
160+
InvalidFormat,
161+
162+
#[snafu(display("failed to parse host"))]
163+
InvalidHost { source: url::ParseError },
164+
165+
#[snafu(display("failed to parse port"))]
166+
InvalidPort { source: std::num::ParseIntError },
167+
}
168+
169+
#[derive(Clone, Debug)]
170+
pub struct HostPort {
171+
pub host: Host,
172+
pub port: Option<u16>,
173+
}
174+
175+
impl Display for HostPort {
176+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177+
match self.port {
178+
Some(port) => write!(f, "{host}:{port}", host = self.host),
179+
None => Display::fmt(&self.host, f),
180+
}
181+
}
182+
}
183+
184+
impl FromStr for HostPort {
185+
type Err = ParseHostPortError;
186+
187+
fn from_str(input: &str) -> Result<Self, Self::Err> {
188+
ensure!(!input.is_empty(), EmptyInputSnafu);
189+
190+
let parts: Vec<_> = input.split(':').collect();
191+
192+
match parts[..] {
193+
[host] => {
194+
let host = Host::parse(host).context(InvalidHostSnafu)?;
195+
Ok(Self { host, port: None })
196+
}
197+
[host, port] => {
198+
let host = Host::parse(host).context(InvalidHostSnafu)?;
199+
let port = u16::from_str(port).context(InvalidPortSnafu)?;
200+
201+
Ok(Self {
202+
host,
203+
port: Some(port),
204+
})
205+
}
206+
_ => InvalidFormatSnafu.fail(),
207+
}
208+
}
209+
}
210+
211+
impl HostPort {
212+
pub fn localhost() -> Self {
213+
HostPort {
214+
host: Host::Domain(String::from("localhost")),
215+
port: None,
216+
}
217+
}
218+
}
219+
220+
#[cfg(test)]
221+
mod tests {
222+
use rstest::rstest;
223+
use strum::IntoDiscriminant;
224+
use url::ParseError;
225+
226+
use super::*;
227+
228+
enum Either<L, R> {
229+
Left(L),
230+
Right(R),
231+
}
232+
233+
impl<L, R> Either<L, R>
234+
where
235+
L: PartialEq,
236+
R: PartialEq,
237+
{
238+
fn is_either(&self, left: &L, right: &R) -> bool {
239+
match self {
240+
Either::Left(l) => l.eq(left),
241+
Either::Right(r) => r.eq(right),
242+
}
243+
}
244+
}
245+
246+
#[rstest]
247+
#[case("registry.example.org:65535")]
248+
#[case("registry.example.org:8080")]
249+
#[case("registry.example.org")]
250+
#[case("example.org:8080")]
251+
#[case("localhost:8080")]
252+
#[case("example.org")]
253+
#[case("localhost")]
254+
fn valid_host_port(#[case] input: &str) {
255+
let host_port = HostPort::from_str(input).expect("must parse");
256+
assert_eq!(host_port.to_string(), input);
257+
}
258+
259+
#[rustfmt::skip]
260+
#[rstest]
261+
// We use the discriminants here, because ParseIntErrors cannot be constructed outside of std.
262+
// As such, it is impossible to fully qualify the error we expect in cases where port parsing
263+
// fails.
264+
#[case("localhost:65536", Either::Right(ParseHostPortErrorDiscriminants::InvalidPort))]
265+
#[case("localhost:", Either::Right(ParseHostPortErrorDiscriminants::InvalidPort))]
266+
// Other errors can be fully qualified.
267+
#[case("with space:", Either::Left(ParseHostPortError::InvalidHost { source: ParseError::IdnaError }))]
268+
#[case("with space", Either::Left(ParseHostPortError::InvalidHost { source: ParseError::IdnaError }))]
269+
#[case(":", Either::Left(ParseHostPortError::InvalidHost { source: ParseError::EmptyHost }))]
270+
#[case("", Either::Left(ParseHostPortError::EmptyInput))]
271+
fn invalid_host_port(
272+
#[case] input: &str,
273+
#[case] expected_error: Either<ParseHostPortError, ParseHostPortErrorDiscriminants>,
274+
) {
275+
let error = HostPort::from_str(input).expect_err("must not parse");
276+
assert!(expected_error.is_either(&error, &error.discriminant()));
277+
}
278+
}

rust/boil/src/utils.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use std::process::Command;
22

33
use semver::Version;
4-
use url::Host;
54

6-
use crate::build::platform::Architecture;
5+
use crate::build::{cli::HostPort, platform::Architecture};
76

87
/// Formats and returns the image repository URI, eg. `oci.stackable.tech/sdp/opa`.
98
pub fn format_image_repository_uri(
10-
image_registry: &Host,
9+
image_registry: &HostPort,
1110
registry_namespace: &str,
1211
image_name: &str,
1312
) -> String {

0 commit comments

Comments
 (0)