Skip to content

Commit bfa182c

Browse files
committed
feat(boil): Add support for floating tags
1 parent eeb27b0 commit bfa182c

10 files changed

Lines changed: 320 additions & 472 deletions

File tree

Cargo.lock

Lines changed: 187 additions & 448 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ reqwest = { version = "0.13.2", features = ["json"] }
2020
rstest = "0.26.1"
2121
secrecy = "0.10.3"
2222
regex = "1.12.3"
23+
semver = "1.0.28"
2324
serde = { version = "1.0.217", features = ["derive"] }
2425
serde_json = "1.0.140"
2526
snafu = "0.9.0"

rust/boil/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ publish = false
99

1010
[dependencies]
1111
cap-std.workspace = true
12-
clap.workspace = true
12+
# We use the unstable-markdown feature to get proper markdown list rendering.
13+
# See tracking issue: https://github.com/clap-rs/clap/issues/5900
14+
clap = { workspace = true, features = ["unstable-markdown"] }
1315
clap_complete.workspace = true
1416
clap_complete_nushell.workspace = true
1517
git2.workspace = true
@@ -19,6 +21,7 @@ oci-spec.workspace = true
1921
reqwest.workspace = true
2022
regex.workspace = true
2123
secrecy.workspace = true
24+
semver.workspace = true
2225
serde.workspace = true
2326
serde_json.workspace = true
2427
snafu.workspace = true

rust/boil/src/cli/build.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ pub struct BuildArguments {
2424
#[arg(help_heading = "Image Options", required = true)]
2525
pub images: Vec<ImageSelector>,
2626

27-
// NOTE (@Techassi): Should this maybe be renamed to vendor_version?
28-
/// The image version being built.
27+
/// The vendor version being built.
2928
#[arg(
3029
short, long,
31-
value_parser = Cli::parse_image_version,
32-
default_value_t = Cli::default_image_version(),
30+
// TODO (@Techassi): Eventually remove these aliases
31+
short_alias = 'i',
32+
alias = "image-version",
33+
value_parser = Cli::parse_vendor_version,
34+
default_value_t = Cli::default_vendor_version(),
3335
help_heading = "Image Options"
3436
)]
35-
pub image_version: String,
37+
pub vendor_version: String,
3638

3739
/// Target platform of the image.
3840
#[arg(
@@ -111,6 +113,16 @@ pub struct BuildArguments {
111113
#[arg(long, help_heading = "Build Options")]
112114
pub strip_architecture: bool,
113115

116+
/// Produces a floating tag (if needed) in addition to the fully-qualified tag.
117+
///
118+
/// Examples:
119+
///
120+
/// - `4.5.6-prefix3.2.1` -> `4.5.6-prefix3.2`
121+
/// - `4.5.6-prefix0.0.0-dev` -> `4.5.6-prefix0.0.0-dev`
122+
/// - `4.5.6-prefix0.0.0-pr123` -> `4.5.6-prefix0.0.0-pr123`
123+
#[arg(long, help_heading = "Build Options")]
124+
pub floating_tag: bool,
125+
114126
/// Loads the image into the local image store.
115127
///
116128
/// DEPRECATED: Use -- --load instead.

rust/boil/src/cli/image.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ pub struct ImageCheckArguments {
4747
/// The image version to check.
4848
#[arg(
4949
short, long,
50-
value_parser = Cli::parse_image_version,
51-
default_value_t = Cli::default_image_version(),
50+
value_parser = Cli::parse_vendor_version,
51+
default_value_t = Cli::default_vendor_version(),
5252
help_heading = "Image Options"
5353
)]
5454
pub image_version: String,
@@ -59,15 +59,17 @@ pub struct ImageSizeArguments {
5959
/// Optionally specify one or more images to check. Checks all images by default.
6060
pub image: Vec<ImageSelector>,
6161

62-
// NOTE (@Techassi): Should this maybe be renamed to vendor_version?
63-
/// The image version to use.
62+
/// The vendor version to use.
6463
#[arg(
6564
short, long,
66-
value_parser = Cli::parse_image_version,
67-
default_value_t = Cli::default_image_version(),
65+
// TODO (@Techassi): Eventually remove these aliases
66+
short_alias = 'i',
67+
alias = "image-version",
68+
value_parser = Cli::parse_vendor_version,
69+
default_value_t = Cli::default_vendor_version(),
6870
help_heading = "Image Options"
6971
)]
70-
pub image_version: String,
72+
pub vendor_version: String,
7173

7274
/// Target platform of the image.
7375
#[arg(

rust/boil/src/cli/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ static VALID_IMAGE_TAG: LazyLock<Regex> =
2020
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_][a-zA-Z0-9_.-]+$").expect("regex is valid"));
2121

2222
#[derive(Debug, Snafu)]
23-
pub enum ParseImageVersionError {
23+
pub enum ParseVendorVersionError {
2424
#[snafu(display("invalid image tag characters for {version:?}"))]
2525
ParseVersion { version: String },
2626
}
@@ -41,7 +41,7 @@ impl Cli {
4141
PathBuf::from("./boil.toml")
4242
}
4343

44-
pub(super) fn default_image_version() -> String {
44+
pub(super) fn default_vendor_version() -> String {
4545
"0.0.0-dev".to_owned()
4646
}
4747

@@ -53,7 +53,7 @@ impl Cli {
5353
}
5454

5555
/// Ensure that the given version will be valid for use in the image tag
56-
pub(super) fn parse_image_version(version: &str) -> Result<String, ParseImageVersionError> {
56+
pub(super) fn parse_vendor_version(version: &str) -> Result<String, ParseVendorVersionError> {
5757
if !VALID_IMAGE_TAG.is_match(version) {
5858
return ParseVersionSnafu { version }.fail();
5959
}

rust/boil/src/cmd/build.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ pub enum Error {
4545
/// This is the `boil build` command handler function.
4646
pub fn run_command(args: Box<BuildArguments>, config: Config) -> Result<(), Error> {
4747
// TODO (@Techassi): Parse Dockerfile instead to build the target graph
48-
// Create bakefile
4948
let bakefile = Bakefile::from_cli_args(&args, config).context(CreateBakefileSnafu)?;
5049
let image_manifest_uris = bakefile.image_manifest_uris();
5150
let count = image_manifest_uris.len();

rust/boil/src/cmd/image.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ pub async fn calculate_size(arguments: ImageSizeArguments, config: Config) -> Re
199199
let image_index_manifest_tag = format_image_index_manifest_tag(
200200
image_version,
201201
&config.metadata.vendor_tag_prefix,
202-
&arguments.image_version,
202+
&arguments.vendor_version,
203203
);
204204

205205
let manifest_tag = format_image_manifest_tag(

rust/boil/src/core/bakefile.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ pub enum Error {
6666
source: std::io::Error,
6767
path: String,
6868
},
69+
70+
#[snafu(display("failed to parse floating vendor version"))]
71+
ParseFloatingVendorVersion {
72+
source: utils::ParseFloatingVendorVersionError,
73+
},
6974
}
7075

7176
#[derive(Debug, Snafu)]
@@ -318,7 +323,7 @@ impl Bakefile {
318323
let target = BakefileTarget::common(
319324
date_time,
320325
revision,
321-
cli_args.image_version.clone(),
326+
cli_args.vendor_version.clone(),
322327
container_build_args,
323328
user_container_build_args,
324329
metadata,
@@ -332,6 +337,10 @@ impl Bakefile {
332337
cli_args: &cli::BuildArguments,
333338
config: Config,
334339
) -> Result<Self, Error> {
340+
let floating_vendor_version =
341+
utils::parse_floating_vendor_version(&cli_args.vendor_version, cli_args.floating_tag)
342+
.context(ParseFloatingVendorVersionSnafu)?;
343+
335344
let mut bakefile_targets = BTreeMap::new();
336345
let mut groups: BTreeMap<String, BakefileGroup> = BTreeMap::new();
337346

@@ -364,7 +373,7 @@ impl Bakefile {
364373
let image_index_manifest_tag = utils::format_image_index_manifest_tag(
365374
&image_version,
366375
&metadata.vendor_tag_prefix,
367-
&cli_args.image_version,
376+
&cli_args.vendor_version,
368377
);
369378

370379
let image_manifest_tag = utils::format_image_manifest_tag(
@@ -400,7 +409,7 @@ impl Bakefile {
400409
));
401410
build_arguments.insert(docker::BuildArgument::new(
402411
"IMAGE_REPOSITORY_URI".to_owned(),
403-
image_repository_uri,
412+
image_repository_uri.clone(),
404413
));
405414
build_arguments.insert(docker::BuildArgument::new(
406415
"IMAGE_INDEX_MANIFEST_TAG".to_owned(),
@@ -415,6 +424,30 @@ impl Bakefile {
415424
image_manifest_uri.clone(),
416425
));
417426

427+
let tags = if let Some(floating_vendor_version) = floating_vendor_version.as_deref()
428+
{
429+
let image_index_manifest_floating_tag = utils::format_image_index_manifest_tag(
430+
&image_version,
431+
&metadata.vendor_tag_prefix,
432+
floating_vendor_version,
433+
);
434+
435+
let image_manifest_floating_tag = utils::format_image_manifest_tag(
436+
&image_index_manifest_floating_tag,
437+
cli_args.target_platform.architecture(),
438+
cli_args.strip_architecture,
439+
);
440+
441+
let floating_image_manifest_uri = utils::format_image_manifest_uri(
442+
&image_repository_uri,
443+
&image_manifest_floating_tag,
444+
);
445+
446+
vec![image_manifest_uri, floating_image_manifest_uri]
447+
} else {
448+
vec![image_manifest_uri]
449+
};
450+
418451
// By using a cap-std Dir, we can ensure that the paths provided must be relative to
419452
// the appropriate image folder and wont escape it by providing absolute or relative
420453
// paths with traversals (..).
@@ -461,11 +494,11 @@ impl Bakefile {
461494
let annotations = BakefileTarget::image_version_annotation(
462495
&image_version,
463496
&metadata.vendor_tag_prefix,
464-
&cli_args.image_version,
497+
&cli_args.vendor_version,
465498
);
466499

467500
let target = BakefileTarget {
468-
tags: vec![image_manifest_uri],
501+
tags,
469502
arguments: build_arguments,
470503
platforms: vec![cli_args.target_platform.clone()],
471504
// NOTE (@Techassi): Should this instead be scoped to the folder of the image we build

rust/boil/src/utils.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,44 @@
1-
use std::process::Command;
1+
use std::{process::Command, str::FromStr};
2+
3+
use snafu::{ResultExt, Snafu};
24

35
use crate::{cli::HostPort, core::platform::Architecture};
46

7+
// FIXME (@Techassi): We should pull this in from a central pice of code, like stackable-shared.
8+
// stackable-shared needs to add a few features to be able to properly select _only_ what is needed
9+
// without pulling in too many unused deps.
10+
pub trait VersionExt {
11+
fn is_floating(&self) -> bool;
12+
13+
fn floating(&self) -> String;
14+
}
15+
16+
impl VersionExt for semver::Version {
17+
fn is_floating(&self) -> bool {
18+
self.major == 0
19+
&& self.minor == 0
20+
&& self.patch == 0
21+
&& (self.pre.starts_with("pr") || self.pre.as_str() == "dev")
22+
}
23+
24+
fn floating(&self) -> String {
25+
if self.is_floating() {
26+
self.to_string()
27+
} else {
28+
format!("{major}.{minor}", major = self.major, minor = self.minor)
29+
}
30+
}
31+
}
32+
33+
#[derive(Debug, Snafu)]
34+
pub enum ParseFloatingVendorVersionError {
35+
#[snafu(display("failed to parse {version:?} as semantic version"))]
36+
ParseSemanticVersion {
37+
source: semver::Error,
38+
version: String,
39+
},
40+
}
41+
542
/// Formats and returns the image repository URI, eg. `oci.stackable.tech/sdp/opa`.
643
pub fn format_image_repository_uri(
744
image_registry: &HostPort,
@@ -49,6 +86,28 @@ pub fn format_registry_token_env_var_name(registry_uri: &str) -> String {
4986
)
5087
}
5188

89+
pub fn parse_floating_vendor_version(
90+
vendor_image_version: &str,
91+
floating_tag: bool,
92+
) -> Result<Option<String>, ParseFloatingVendorVersionError> {
93+
if floating_tag {
94+
let version =
95+
semver::Version::from_str(vendor_image_version).context(ParseSemanticVersionSnafu {
96+
version: vendor_image_version,
97+
})?;
98+
99+
// Return None because the selected version is already considered a floating version as is.
100+
// There is no need to add a second, identical tag to the image.
101+
if version.is_floating() {
102+
return Ok(None);
103+
}
104+
105+
Ok(Some(version.floating()))
106+
} else {
107+
Ok(None)
108+
}
109+
}
110+
52111
pub trait CommandExt {
53112
/// Adds an argument to the command if the `predicate` is `true`.
54113
fn arg_if<S>(&mut self, predicate: bool, arg: S) -> &mut Self

0 commit comments

Comments
 (0)