diff --git a/Cargo.lock b/Cargo.lock index 21057801..29294221 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,6 +631,7 @@ dependencies = [ "rustls 0.21.12", "rustyline", "rustyline-derive", + "semver", "serde", "serde_json", "serde_yaml_ng", @@ -4113,6 +4114,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" diff --git a/Dockerfile.rls b/Dockerfile.rls index b674d873..7dcbf27a 100644 --- a/Dockerfile.rls +++ b/Dockerfile.rls @@ -33,6 +33,7 @@ RUN adduser --uid $USERID --gid $GROUPID --gecos "Brane" --disabled-password bra # Copy over relevant crates & other files RUN mkdir /build && chown -R brane:brane /build COPY --chown=brane:brane . /build +RUN mkdir /build/build && chown brane:brane /build/build # Build optimized binaries USER brane @@ -48,13 +49,13 @@ RUN --mount=type=cache,id=cargoidx,uid=$USERID,target=/usr/local/cargo/registry --package brane-prx \ --package brane-plr \ --package brane-reg \ - && cp ./target/release/brane-api /home/brane/brane-api \ - && cp ./target/release/brane-chk /home/brane/brane-chk \ - && cp ./target/release/brane-drv /home/brane/brane-drv \ - && cp ./target/release/brane-job /home/brane/brane-job \ - && cp ./target/release/brane-prx /home/brane/brane-prx \ - && cp ./target/release/brane-plr /home/brane/brane-plr \ - && cp ./target/release/brane-reg /home/brane/brane-reg + && cp ./target/release/brane-api /build/build/brane-api \ + && cp ./target/release/brane-chk /build/build/brane-chk \ + && cp ./target/release/brane-drv /build/build/brane-drv \ + && cp ./target/release/brane-job /build/build/brane-job \ + && cp ./target/release/brane-prx /build/build/brane-prx \ + && cp ./target/release/brane-plr /build/build/brane-plr \ + && cp ./target/release/brane-reg /build/build/brane-reg # If ever run, run a shell WORKDIR / @@ -148,7 +149,7 @@ ENTRYPOINT [ "/bin/bash" ] FROM brane-base AS brane-prx # Copy `brane-prx` from build stage -COPY --from=build-brane --chown=brane:brane /home/brane//brane-prx /brane-prx +COPY --from=build-brane --chown=brane:brane /build/build/brane-prx /brane-prx # Run the compiled executable as base ENTRYPOINT [ "/brane-prx" ] @@ -169,7 +170,7 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Copy `brane-api` from build stage -COPY --from=build-brane --chown=brane:brane /home/brane/brane-api /brane-api +COPY --from=build-brane --chown=brane:brane /build/build/brane-api /brane-api # Run the compiled executable as base USER brane @@ -184,7 +185,7 @@ ENTRYPOINT [ "/brane-api" ] FROM brane-base AS brane-drv # Copy `brane-drv` from build stage -COPY --from=build-brane --chown=brane:brane /home/brane/brane-drv /brane-drv +COPY --from=build-brane --chown=brane:brane /build/build/brane-drv /brane-drv # Run the compiled executable as base ENTRYPOINT [ "/brane-drv" ] @@ -198,7 +199,7 @@ ENTRYPOINT [ "/brane-drv" ] FROM brane-base AS brane-plr # Copy `brane-plr` from build stage -COPY --from=build-brane --chown=brane:brane /home/brane/brane-plr /brane-plr +COPY --from=build-brane --chown=brane:brane /build/build/brane-plr /brane-plr # Run the compiled executable as base ENTRYPOINT [ "/brane-plr" ] @@ -219,7 +220,7 @@ USER root RUN adduser brane root # Copy `brane-job` from build stage -COPY --from=build-brane --chown=brane:brane /home/brane/brane-job /brane-job +COPY --from=build-brane --chown=brane:brane /build/build/brane-job /brane-job # Run the compiled executable as base # USER brane @@ -234,7 +235,7 @@ ENTRYPOINT [ "/brane-job" ] FROM brane-base AS brane-reg # Copy `brane-reg` from build stage -COPY --from=build-brane --chown=brane:brane /home/brane/brane-reg /brane-reg +COPY --from=build-brane --chown=brane:brane /build/build/brane-reg /brane-reg # Run the compiled executable as base ENTRYPOINT [ "/brane-reg" ] @@ -257,7 +258,7 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Copy `policy-reasoner` from build stage -COPY --from=build-brane --chown=brane:brane /home/brane/brane-chk /brane-chk +COPY --from=build-brane --chown=brane:brane /build/build/brane-chk /brane-chk RUN chmod +x /brane-chk # Copy `eflint-repl` from build stage diff --git a/brane-api/src/errors.rs b/brane-api/src/errors.rs index 28f5cff7..0b6061bd 100644 --- a/brane-api/src/errors.rs +++ b/brane-api/src/errors.rs @@ -20,7 +20,7 @@ use enum_debug::EnumDebug as _; use reqwest::StatusCode; use scylla::transport::errors::NewSessionError; use specifications::address::Address; -use specifications::version::Version; +use specifications::version::AliasedFunctionVersion; /***** ERRORS *****/ @@ -142,16 +142,16 @@ pub enum PackageError { VersionsQueryError { name: String, source: scylla::transport::errors::QueryError }, /// Failed to parse a Version string #[error("Failed to parse '{raw}' as a valid version string")] - VersionParseError { raw: String, source: specifications::version::ParseError }, + VersionParseError { raw: String, source: specifications::version::SemverError }, /// No versions found for the given package #[error("No versions found for package '{name}'")] NoVersionsFound { name: String }, /// Failed to query the database for the file of the given package. #[error("Failed to get path of package '{name}', version {version}")] - PathQueryError { name: String, version: Version, source: scylla::transport::errors::QueryError }, + PathQueryError { name: String, version: AliasedFunctionVersion, source: scylla::transport::errors::QueryError }, /// The given package was unknown. #[error("No package '{name}' exists (or has version {version})")] - UnknownPackage { name: String, version: Version }, + UnknownPackage { name: String, version: AliasedFunctionVersion }, /// Failed to get the metadata of a file. #[error("Failed to get metadata of file '{}'", path.display())] FileMetadataError { path: PathBuf, source: std::io::Error }, diff --git a/brane-api/src/packages.rs b/brane-api/src/packages.rs index e3f1645f..6f4d4cf5 100644 --- a/brane-api/src/packages.rs +++ b/brane-api/src/packages.rs @@ -28,7 +28,7 @@ use rand::distr::Alphanumeric; use scylla::macros::{FromUserType, IntoUserType}; use scylla::{SerializeCql, Session}; use specifications::package::PackageInfo; -use specifications::version::Version; +use specifications::version::AliasedFunctionVersion; // use tar::Archive; use tempfile::TempDir; use tokio::fs as tfs; @@ -249,21 +249,21 @@ pub async fn download(name: String, version: String, context: Context) -> Result // Attempt to resolve the version from the Scylla database in the context debug!("Resolving version '{}'...", version); - let version: Version = if version.to_lowercase() == "latest" { + let version: AliasedFunctionVersion = if version.to_lowercase() == "latest" { let versions = match context.scylla.query("SELECT version FROM brane.packages WHERE name=?", vec![&name]).await { Ok(versions) => versions, Err(source) => { fail!(Error::VersionsQueryError { name, source }); }, }; - let mut latest: Option = None; + let mut latest: Option = None; if let Some(rows) = versions.rows { for row in rows { // Get the string value let version: &str = row.columns[0].as_ref().unwrap().as_text().unwrap(); // Attempt to parse - let version: Version = match Version::from_str(version) { + let version: AliasedFunctionVersion = match AliasedFunctionVersion::from_str(version) { Ok(version) => version, Err(source) => { fail!(Error::VersionParseError { raw: version.into(), source }); @@ -286,7 +286,7 @@ pub async fn download(name: String, version: String, context: Context) -> Result }, } } else { - match Version::from_str(&version) { + match AliasedFunctionVersion::from_str(&version) { Ok(version) => version, Err(source) => { fail!(Error::VersionParseError { raw: version, source }); diff --git a/brane-api/src/schema.rs b/brane-api/src/schema.rs index 6a65d46a..5c7d3b60 100644 --- a/brane-api/src/schema.rs +++ b/brane-api/src/schema.rs @@ -19,7 +19,7 @@ use chrono::{DateTime, TimeZone, Utc}; use juniper::{EmptySubscription, FieldResult, GraphQLObject, RootNode, graphql_object}; use log::{debug, info}; use scylla::IntoTypedRows; -use specifications::version::Version; +use specifications::version::{AliasedFunctionVersion, ConcreteFunctionVersion}; use crate::packages::PackageUdt; use crate::spec::Context; @@ -97,15 +97,15 @@ impl Query { // Now find the target version if relevant if let Some(version) = version { - let target_version: Version = Version::from_str(&version)?; + let target_version = AliasedFunctionVersion::from_str(&version)?; let mut package: Option = None; - let mut version: Option = None; - if target_version.is_latest() { + let mut version: Option = None; + if let AliasedFunctionVersion::Version(semver) = target_version { for p in packages { - // Find the one with the highest version + // Find the first matching one // Parse it as a version - let pversion: Version = match Version::from_str(&p.version) { + let pversion = match ConcreteFunctionVersion::from_str(&p.version) { Ok(version) => version, Err(_) => { continue; @@ -113,17 +113,16 @@ impl Query { }; // Compare - if package.is_none() || &pversion > version.as_ref().unwrap() { + if semver == pversion { package = Some(p); - version = Some(pversion); } } } else { for p in packages { - // Find the first matching one + // Find the one with the highest version // Parse it as a version - let pversion: Version = match Version::from_str(&p.version) { + let pversion = match ConcreteFunctionVersion::from_str(&p.version) { Ok(version) => version, Err(_) => { continue; @@ -131,8 +130,9 @@ impl Query { }; // Compare - if target_version == pversion { + if package.is_none() || &pversion > version.as_ref().unwrap() { package = Some(p); + version = Some(pversion); } } } diff --git a/brane-ast/src/errors.rs b/brane-ast/src/errors.rs index 9a135d3b..ed5b4b60 100644 --- a/brane-ast/src/errors.rs +++ b/brane-ast/src/errors.rs @@ -19,7 +19,7 @@ use std::io::Write; use brane_dsl::ast::Expr; use brane_dsl::{DataType, TextRange}; use console::{Style, style}; -use specifications::version::Version; +use specifications::version::AliasedFunctionVersion; use specifications::wir::builtins::BuiltinClasses; use specifications::wir::merge_strategy::MergeStrategy; @@ -508,10 +508,10 @@ impl SanityError { pub enum ResolveError { /// Failed to parse a package version number. #[error("Failed to parse package version")] - VersionParseError { source: specifications::version::ParseError, range: TextRange }, + VersionParseError { source: specifications::version::SemverError, range: TextRange }, /// The given package/version pair was not found. #[error("Package '{}' does not exist{}", name, if !version.is_latest() { format!(" or has no version '{version}'") } else { String::new() })] - UnknownPackageError { name: String, version: Version, range: TextRange }, + UnknownPackageError { name: String, version: AliasedFunctionVersion, range: TextRange }, /// Failed to declare an imported package function #[error("Could not import function '{name}' from package '{package_name}'")] FunctionImportError { package_name: String, name: String, source: brane_dsl::errors::SymbolTableError, range: TextRange }, diff --git a/brane-ast/src/state.rs b/brane-ast/src/state.rs index ad65fcc0..71953afd 100644 --- a/brane-ast/src/state.rs +++ b/brane-ast/src/state.rs @@ -22,7 +22,7 @@ use brane_dsl::data_type::{ClassSignature, FunctionSignature}; use brane_dsl::symbol_table::{ClassEntry, FunctionEntry, SymbolTable, VarEntry}; use brane_dsl::{DataType, TextRange}; use specifications::package::Capability; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; use specifications::wir::builtins::{BuiltinClasses, BuiltinFunctions}; use specifications::wir::{ClassDef, ComputeTaskDef, Edge, FunctionDef, SymTable, TaskDef, VarDef}; use strum::IntoEnumIterator; @@ -380,7 +380,7 @@ pub struct TaskState { /// The name of the package where this Task is stored. pub package_name: String, /// The version of the package where this Task is stored. - pub package_version: Version, + pub package_version: ConcreteFunctionVersion, /// The range that links this task back to the source text. pub range: TextRange, @@ -395,7 +395,7 @@ impl From<&TaskState> for FunctionEntry { params: vec![], package_name: Some(value.package_name.clone()), - package_version: Some(value.package_version), + package_version: Some(value.package_version.clone()), class_name: None, arg_names: value.arg_names.clone(), @@ -441,7 +441,7 @@ pub struct ClassState { /// If this class is imported from a package, then the package's name is stored here. pub package_name: Option, /// If this class is imported from a package, then the package's version is stored here. - pub package_version: Option, + pub package_version: Option, /// The range that links this class back to the source text. pub range: TextRange, @@ -526,7 +526,7 @@ impl ClassState { symbol_table: c_table, package_name: self.package_name.clone(), - package_version: self.package_version, + package_version: self.package_version.clone(), index: usize::MAX, diff --git a/brane-ast/src/traversals/flatten.rs b/brane-ast/src/traversals/flatten.rs index 433c67d4..a016a3c7 100644 --- a/brane-ast/src/traversals/flatten.rs +++ b/brane-ast/src/traversals/flatten.rs @@ -168,7 +168,7 @@ fn move_task(task: &Rc>, table: &mut TableState) { requirements: entry.requirements.clone().unwrap(), package_name: entry.package_name.clone().unwrap(), - package_version: entry.package_version.unwrap(), + package_version: entry.package_version.clone().unwrap(), range: entry.range.clone(), } @@ -240,7 +240,7 @@ fn move_class(class: &Rc>, table: &mut TableState) -> Result methods, package_name: entry.package_name.clone(), - package_version: entry.package_version, + package_version: entry.package_version.clone(), range: entry.range.clone(), } diff --git a/brane-ast/src/traversals/print/ast.rs b/brane-ast/src/traversals/print/ast.rs index a763e543..b623cfce 100644 --- a/brane-ast/src/traversals/print/ast.rs +++ b/brane-ast/src/traversals/print/ast.rs @@ -95,7 +95,9 @@ fn pass_table(writer: &mut impl Write, table: &SymTable, indent: usize) -> std:: "{}Task {}{}::{}({}){};", indent!(indent), def.package, - if !def.version.is_latest() { format!("<{}>", def.version) } else { String::new() }, + // FIXME: Removed latest alias + // if let AliasedFunctionVersion::Version(semver) = &def.version { format!("<{}>", semver) } else { String::new() }, + format_args!("<{}>", &def.version), &def.function.name, def.function.args.iter().enumerate().map(|(i, a)| format!("{}: {}", def.args_names[i], a)).collect::>().join(", "), if def.function.ret != DataType::Void { format!(" -> {}", def.function.ret) } else { String::new() }, @@ -195,7 +197,9 @@ fn pass_edges( TaskDef::Compute(def) => format!( "{}{}::{}", def.package, - if !def.version.is_latest() { format!("<{}>", def.version) } else { String::new() }, + // FIXME:: Removed latest + // if let AliasedFunctionVersion::Version(semver) = &def.version { format!("<{}>", semver) } else { String::new() }, + format_args!("<{}>", &def.version), def.function.name ), TaskDef::Transfer => "__builtin::transfer".into(), diff --git a/brane-ast/src/traversals/print/ast_unresolved.rs b/brane-ast/src/traversals/print/ast_unresolved.rs index 95e0f852..b2a656b6 100644 --- a/brane-ast/src/traversals/print/ast_unresolved.rs +++ b/brane-ast/src/traversals/print/ast_unresolved.rs @@ -85,7 +85,9 @@ pub fn pass_table(writer: &mut impl Write, table: &TableState, indent: usize) -> "{}Task {}{}::{}({}){};", indent!(indent), t.package_name, - if !t.package_version.is_latest() { format!("<{}>", t.package_version) } else { String::new() }, + // FIXME: Removed latest + // if let AliasedFunctionVersion::Version(semver) = &t.package_version { format!("<{semver}>") } else { String::new() }, + format_args!("<{:?}>", &t.package_version), &t.name, t.signature.args.iter().enumerate().map(|(i, a)| format!("{}: {}", t.arg_names[i], a)).collect::>().join(", "), if t.signature.ret != DataType::Void { format!(" -> {}", t.signature.ret) } else { String::new() }, @@ -105,11 +107,13 @@ pub fn pass_table(writer: &mut impl Write, table: &TableState, indent: usize) -> format!( "{}{}::", package, - if !c.package_version.as_ref().unwrap().is_latest() { - format!("<{}>", c.package_version.as_ref().unwrap()) - } else { - String::new() - } + // FIXME: Unnecessary unwrap (probably) + // if let AliasedFunctionVersion::Version(semver) = &c.package_version.as_ref().unwrap() { + // format!("<{semver}>") + // } else { + // String::new() + // } + format_args!("<{:?}>", &c.package_version), ) } else { String::new() @@ -338,7 +342,9 @@ pub fn pass_edge(writer: &mut impl Write, edge: &Edge, table: &TableState, inden let task: String = format!( "{}{}::{}", task.package_name, - if !task.package_version.is_latest() { format!("<{}>", task.package_version) } else { String::new() }, + // FIXME: Removed latest + // if let AliasedFunctionVersion::Version(semver) = &task.package_version { format!("<{semver}>") } else { String::new() }, + format_args!("<{}>", &task.package_version), task.name ); diff --git a/brane-ast/src/traversals/resolve.rs b/brane-ast/src/traversals/resolve.rs index 11846347..fbb129e0 100644 --- a/brane-ast/src/traversals/resolve.rs +++ b/brane-ast/src/traversals/resolve.rs @@ -25,7 +25,6 @@ use enum_debug::EnumDebug as _; use log::trace; use specifications::data::DataIndex; use specifications::package::{PackageIndex, PackageInfo}; -use specifications::version::Version; use specifications::wir::builtins::{BuiltinClasses, BuiltinFunctions}; use specifications::wir::merge_strategy::MergeStrategy; @@ -159,7 +158,7 @@ fn pass_stmt( Import { name, version, st_funcs, st_classes, attrs: _, range } => { // First: parse the version - let semver: Version = match version.as_version() { + let semver = match version.as_version() { Ok(version) => version, Err(source) => { errors.push(Error::VersionParseError { source, range: version.range().clone() }); @@ -168,7 +167,7 @@ fn pass_stmt( }; // Attempt to resolve this (name, version) pair in the package index. - let info: &PackageInfo = match package_index.get(&name.value, if !semver.is_latest() { Some(&semver) } else { None }) { + let info: &PackageInfo = match package_index.get(&name.value, &semver) { Some(info) => info, None => { errors.push(Error::UnknownPackageError { name: name.value.clone(), version: semver, range: range.clone() }); @@ -190,7 +189,7 @@ fn pass_stmt( name, FunctionSignature::new(arg_types, ret_type), &info.name, - info.version, + info.version.clone(), arg_names, f.requirements.clone().unwrap_or_default(), TextRange::none(), @@ -232,7 +231,7 @@ fn pass_stmt( ClassSignature { name: name.clone() }, c_symbol_table, &info.name, - info.version, + info.version.clone(), TextRange::none(), )) { Ok(entry) => { diff --git a/brane-chk/src/stateresolver.rs b/brane-chk/src/stateresolver.rs index 8776795d..eb694354 100644 --- a/brane-chk/src/stateresolver.rs +++ b/brane-chk/src/stateresolver.rs @@ -25,7 +25,7 @@ use policy_store::spec::databaseconn::{DatabaseConnection as _, DatabaseConnecto use policy_store::spec::metadata::User; use reqwest::StatusCode; use specifications::address::Address; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; use thiserror::Error; use tracing::{debug, instrument, warn}; @@ -76,7 +76,7 @@ pub enum Error { DuplicateInputId { workflow: String, call: String, input: String }, /// Found an illegal version string in a task string. #[error("Illegal version identifier {version:?} in task {task:?} in call {call:?} in workflow {workflow:?}")] - IllegalVersionFormat { workflow: String, call: String, task: String, version: String, source: specifications::version::ParseError }, + IllegalVersionFormat { workflow: String, call: String, task: String, version: String, source: specifications::version::SemverError }, /// Failed to get the package index from the remote registry. #[error("Failed to get package index from the central registry at {addr:?}")] PackageIndex { addr: String, source: brane_tsk::api::Error }, @@ -108,7 +108,7 @@ pub enum Error { UnknownCall { workflow: String, call: String }, /// The function called on a package in a call was unknown to that package. #[error("Unknown function {function:?} in package {package:?} ({version}) in call {call:?} in workflow {workflow:?}")] - UnknownFunction { workflow: String, call: String, package: String, version: Version, function: String }, + UnknownFunction { workflow: String, call: String, package: String, version: ConcreteFunctionVersion, function: String }, /// Some input to a task was unknown to us. #[error("Unknown input {input:?} to call {call:?} in workflow {workflow:?}")] UnknownInput { workflow: String, call: String, input: String }, @@ -123,7 +123,7 @@ pub enum Error { UnknownOwnerUser { workflow: String, call: String, tag: String, user: String }, /// The package extracted from a call was unknown to us. #[error("Unknown package {package:?} ({version}) in call {call:?} in workflow {workflow:?}")] - UnknownPackage { workflow: String, call: String, package: String, version: Version }, + UnknownPackage { workflow: String, call: String, package: String, version: ConcreteFunctionVersion }, /// The planned user of a task was unknown to us. #[error("Unknown planned user {user:?} in call {call:?} in workflow {workflow:?}")] UnknownPlannedUser { workflow: String, call: String, user: String }, diff --git a/brane-cli/Cargo.toml b/brane-cli/Cargo.toml index b559bc9e..a80ed768 100644 --- a/brane-cli/Cargo.toml +++ b/brane-cli/Cargo.toml @@ -29,6 +29,7 @@ path-clean = "1.0.0" prettytable = "0.10.0" rustyline = { version = "15.0.0", default-features = false } rustyline-derive = "0.11.0" +semver = "1.0.0" tar = "0.4.21" tempfile = "3.10.1" tokio-stream = "0.1.16" diff --git a/brane-cli/src/build_ecu.rs b/brane-cli/src/build_ecu.rs index 3e0fb653..caa59f3b 100644 --- a/brane-cli/src/build_ecu.rs +++ b/brane-cli/src/build_ecu.rs @@ -45,11 +45,12 @@ pub async fn handle( let document = ContainerInfo::from_reader(handle).map_err(|source| BuildError::ContainerInfoParseError { file: file.clone(), source })?; // Prepare package directory - let package_dir = ensure_package_dir(&document.name, Some(&document.version), true).map_err(|source| BuildError::PackageDirError { source })?; + let package_dir = + ensure_package_dir(&document.name, Some(&document.version.clone().into()), true).map_err(|source| BuildError::PackageDirError { source })?; // Lock the directory, build, unlock the directory { - let _lock = FileLock::lock(&document.name, document.version, package_dir.join(".lock")) + let _lock = FileLock::lock(&document.name, &document.version, package_dir.join(".lock")) .map_err(|source| BuildError::LockCreateError { name: document.name.clone(), source })?; build(arch, document, context, &package_dir, branelet_path, keep_files, convert_crlf).await?; }; diff --git a/brane-cli/src/cli.rs b/brane-cli/src/cli.rs index cb271b97..812aee78 100644 --- a/brane-cli/src/cli.rs +++ b/brane-cli/src/cli.rs @@ -5,7 +5,7 @@ use brane_tsk::docker::ClientVersion; use brane_tsk::spec::AppId; use clap::Parser; use specifications::arch::Arch; -use specifications::version::Version as SemVersion; +use specifications::version::{AliasedFunctionVersion, ConcreteFunctionVersion}; /***** ARGUMENTS *****/ /// The Brane command-line interface. @@ -416,7 +416,7 @@ pub(crate) enum PackageSubcommand { #[clap(name = "NAME", help = "Name of the package")] name: String, #[clap(name = "VERSION", default_value = "latest", help = "Version of the package")] - version: SemVersion, + version: AliasedFunctionVersion, // Alternative syntax to use. #[clap( @@ -439,7 +439,7 @@ pub(crate) enum PackageSubcommand { #[clap(name = "NAME", help = "Name of the package")] name: String, #[clap(short, long, default_value = "latest", help = "Version of the package")] - version: SemVersion, + version: AliasedFunctionVersion, }, // #[clap(name = "logout", about = "Log out from a registry")] @@ -507,7 +507,7 @@ pub(crate) enum PackageSubcommand { #[clap(name = "NAME", help = "Name of the package")] name: String, #[clap(name = "VERSION", default_value = "latest", help = "Version of the package")] - version: SemVersion, + version: AliasedFunctionVersion, #[clap( short = 'r', long, @@ -557,7 +557,7 @@ pub(crate) enum PackageSubcommand { #[clap(name = "NAME", help = "Name of the package")] name: String, #[clap(name = "VERSION", help = "Version of the package")] - version: SemVersion, + version: ConcreteFunctionVersion, #[clap(short, long, action, help = "Don't ask for confirmation")] force: bool, }, diff --git a/brane-cli/src/errors.rs b/brane-cli/src/errors.rs index 628205c1..bc095d89 100644 --- a/brane-cli/src/errors.rs +++ b/brane-cli/src/errors.rs @@ -21,7 +21,7 @@ use reqwest::StatusCode; use specifications::address::Address; use specifications::container::{ContainerInfoError, Image, LocalContainerInfoError}; use specifications::package::{PackageInfoError, PackageKindError}; -use specifications::version::{ParseError as VersionParseError, Version}; +use specifications::version::{AliasedFunctionVersion, ConcreteFunctionVersion, SemverError as VersionParseError}; /***** GLOBALS *****/ @@ -99,7 +99,7 @@ pub enum CliError { IllegalPackageKind { kind: String, source: PackageKindError }, /// Could not parse a NAME:VERSION pair #[error("Could not parse '{raw}'")] - PackagePairParseError { raw: String, source: specifications::version::ParseError }, + PackagePairParseError { raw: String, source: specifications::version::VersionError }, } /// Collects errors during the build subcommand @@ -635,14 +635,17 @@ pub enum InstanceError { pub enum PackageError { /// Something went wrong while calling utilities #[error(transparent)] - UtilError { source: UtilError }, + UtilError { + #[from] + source: UtilError, + }, /// Something went wrong when fetching an index. #[error("Failed to fetch a local package index")] IndexError { source: brane_tsk::local::Error }, /// Failed to resolve a specific package/version pair #[error("Package '{name}' does not exist or has no version {version}")] - PackageVersionError { name: String, version: Version, source: UtilError }, + PackageVersionError { name: String, version: ConcreteFunctionVersion, source: UtilError }, /// Failed to resolve a specific package #[error("Package '{name}' does not exist")] PackageError { name: String, source: UtilError }, @@ -650,14 +653,14 @@ pub enum PackageError { #[error("Failed to ask for your consent")] ConsentError { source: dialoguer::Error }, /// Failed to remove a package directory - #[error("Failed to remove package '{}' (version {}) at '{}'", name, version, dir.display())] - PackageRemoveError { name: String, version: Version, dir: PathBuf, source: std::io::Error }, + #[error("Failed to remove package '{}' {} at '{}'", name, match version { Some(version) => version.to_string(), None => String::new() }, dir.display())] + PackageRemoveError { name: String, version: Option, dir: PathBuf, source: std::io::Error }, /// Failed to get the versions of a package #[error("Failed to get versions of package '{}' (at '{}')", name, dir.display())] VersionsError { name: String, dir: PathBuf, source: std::io::Error }, /// Failed to parse the version of a package #[error("Could not parse '{raw}' as a version for package '{name}'")] - VersionParseError { name: String, raw: String, source: specifications::version::ParseError }, + VersionParseError { name: String, raw: String, source: specifications::version::SemverError }, /// Failed to load the PackageInfo of the given package #[error("Could not load package info file '{}'", path.display())] PackageInfoError { path: PathBuf, source: specifications::package::PackageInfoError }, @@ -714,7 +717,7 @@ pub enum RegistryError { KindParseError { url: String, raw: String, source: specifications::package::PackageKindError }, /// Could not parse the version as a proper PackageInfo version #[error("Could not parse '{raw}' (received from '{url}') as package version")] - VersionParseError { url: String, raw: String, source: specifications::version::ParseError }, + VersionParseError { url: String, raw: String, source: specifications::version::SemverError }, /// Could not parse the list of requirements of the package. #[error("Could not parse '{raw}' (received from '{url}') as package requirement")] RequirementParseError { url: String, raw: String, source: serde_json::Error }, @@ -742,13 +745,13 @@ pub enum RegistryError { VersionsError { name: String, source: brane_tsk::local::Error }, /// Failed to resolve the directory of a specific package #[error("Could not resolve package directory of package '{name}' (version {version})")] - PackageDirError { name: String, version: Version, source: UtilError }, + PackageDirError { name: String, version: ConcreteFunctionVersion, source: UtilError }, /// Could not create a new temporary file #[error("Could not create a new temporary file")] TempFileError { source: std::io::Error }, /// Could not compress the package file #[error("Could not compress package '{}' (version {}) to '{}'", name, version, path.display())] - CompressionError { name: String, version: Version, path: PathBuf, source: std::io::Error }, + CompressionError { name: String, version: ConcreteFunctionVersion, path: PathBuf, source: std::io::Error }, /// Failed to re-open the compressed package file #[error("Could not re-open compressed package archive '{}'", path.display())] PackageArchiveOpenError { path: PathBuf, source: std::io::Error }, @@ -910,10 +913,10 @@ pub enum TestError { DatasetsDirError { source: UtilError }, /// Failed to get the directory of a package. #[error("Failed to get directory of package '{name}' (version {version})")] - PackageDirError { name: String, version: Version, source: UtilError }, + PackageDirError { name: String, version: AliasedFunctionVersion, source: UtilError }, /// Failed to read the PackageInfo of the given package. #[error("Failed to read package info for package '{name}' (version {version})")] - PackageInfoError { name: String, version: Version, source: specifications::package::PackageInfoError }, + PackageInfoError { name: String, version: AliasedFunctionVersion, source: specifications::package::PackageInfoError }, /// Failed to initialize the offline VM. #[error("Failed to initialize offline VM")] @@ -942,7 +945,7 @@ pub enum VersionError { HostArchError { source: specifications::arch::ArchError }, /// Could not parse a Version number. #[error("Could parse '{raw}' as Version")] - VersionParseError { raw: String, source: specifications::version::ParseError }, + VersionParseError { raw: String, source: specifications::version::SemverError }, /// Could not discover if the instance existed. #[error("Could not check if active instance exists")] @@ -1059,10 +1062,10 @@ pub enum UtilError { PackageDirNotFound { package: String, path: PathBuf }, /// Could not create a new directory for the given version #[error("Could not create directory for package '{}', version: {} (path: '{}')", package, version, path.display())] - VersionDirCreateError { package: String, version: Version, path: PathBuf, source: std::io::Error }, + VersionDirCreateError { package: String, version: ConcreteFunctionVersion, path: PathBuf, source: std::io::Error }, /// The target package/version directory does not exist #[error("Directory for package '{}', version: {} does not exist (path: '{}')", package, version, path.display())] - VersionDirNotFound { package: String, version: Version, path: PathBuf }, + VersionDirNotFound { package: String, version: ConcreteFunctionVersion, path: PathBuf }, /// Could not create the dataset folder for a specific dataset #[error("Could not create Brane dataset directory '{}' for dataset '{}'", path.display(), name)] diff --git a/brane-cli/src/lib.rs b/brane-cli/src/lib.rs index 105e1f44..1b4d0117 100644 --- a/brane-cli/src/lib.rs +++ b/brane-cli/src/lib.rs @@ -33,7 +33,7 @@ pub mod vm; /***** CONSTANTS *****/ /// The minimum Docker version required by the Brane CLI command-line tool -pub const MIN_DOCKER_VERSION: specifications::version::Version = specifications::version::Version::new(19, 0, 0); +pub const MIN_DOCKER_VERSION: semver::Version = semver::Version::new(19, 0, 0); /// The minimum Buildx version required by the Brane CLI command-line tool -pub const MIN_BUILDX_VERSION: specifications::version::Version = specifications::version::Version::new(0, 7, 0); +pub const MIN_BUILDX_VERSION: semver::Version = semver::Version::new(0, 7, 0); diff --git a/brane-cli/src/main.rs b/brane-cli/src/main.rs index 8c39850d..38ce9f44 100644 --- a/brane-cli/src/main.rs +++ b/brane-cli/src/main.rs @@ -36,7 +36,7 @@ use humanlog::{DebugMode, HumanLogger}; use log::{error, info}; use specifications::arch::Arch; use specifications::package::PackageKind; -use specifications::version::Version as SemVersion; +use specifications::version::AliasedFunctionVersion; use tempfile::TempDir; @@ -288,10 +288,11 @@ async fn run(options: Cli) -> Result<(), CliError> { println!("Nothing to do."); return Ok(()); } - let mut parsed: Vec<(String, SemVersion)> = Vec::with_capacity(packages.len()); + let mut parsed: Vec<(String, AliasedFunctionVersion)> = Vec::with_capacity(packages.len()); for package in &packages { parsed.push( - SemVersion::from_package_pair(package) + AliasedFunctionVersion::from_package_pair(package) + .map(|(package, version)| (package, version.unwrap_or(AliasedFunctionVersion::Latest))) .map_err(|source| CliError::PackagePairParseError { raw: package.into(), source })?, ); } @@ -305,10 +306,12 @@ async fn run(options: Cli) -> Result<(), CliError> { println!("Nothing to do."); return Ok(()); } - let mut parsed: Vec<(String, SemVersion)> = Vec::with_capacity(packages.len()); + let mut parsed: Vec<(String, AliasedFunctionVersion)> = Vec::with_capacity(packages.len()); for package in packages { parsed.push( - SemVersion::from_package_pair(&package).map_err(|source| CliError::PackagePairParseError { raw: package, source })?, + AliasedFunctionVersion::from_package_pair(&package) + .map(|(package, version)| (package, version.unwrap_or(AliasedFunctionVersion::Latest))) + .map_err(|source| CliError::PackagePairParseError { raw: package, source })?, ); } @@ -321,10 +324,11 @@ async fn run(options: Cli) -> Result<(), CliError> { println!("Nothing to do."); return Ok(()); } - let mut parsed: Vec<(String, SemVersion)> = Vec::with_capacity(packages.len()); + let mut parsed: Vec<(String, Option)> = Vec::with_capacity(packages.len()); for package in packages { parsed.push( - SemVersion::from_package_pair(&package).map_err(|source| CliError::PackagePairParseError { raw: package, source })?, + AliasedFunctionVersion::from_package_pair(&package) + .map_err(|source| CliError::PackagePairParseError { raw: package, source })?, ); } diff --git a/brane-cli/src/packages.rs b/brane-cli/src/packages.rs index fde9deb5..693972b9 100644 --- a/brane-cli/src/packages.rs +++ b/brane-cli/src/packages.rs @@ -19,13 +19,13 @@ use prettytable::Table; use prettytable::format::FormatBuilder; use specifications::container::Image; use specifications::package::PackageInfo; -use specifications::version::Version; +use specifications::version::{AliasedFunctionVersion, ConcreteFunctionVersion}; use tokio::fs::File as TFile; use tokio_stream::StreamExt; use tokio_util::codec::{BytesCodec, FramedRead}; -use crate::errors::PackageError; -use crate::utils::{ensure_package_dir, ensure_packages_dir}; +use crate::errors::{PackageError, UtilError}; +use crate::utils::{ensure_package_dir, ensure_packages_dir, get_packages_dir}; /***** HELPER FUNCTIONS *****/ @@ -70,7 +70,7 @@ fn insert_package_in_list(infos: &mut Vec, info: PackageInfo) { /// /// # Returns /// Nothing -pub fn inspect(name: String, version: Version, syntax: String) -> Result<()> { +pub fn inspect(name: String, version: AliasedFunctionVersion, syntax: String) -> Result<()> { let package_dir = ensure_package_dir(&name, Some(&version), false)?; let package_file = package_dir.join("package.yml"); @@ -197,7 +197,7 @@ pub fn inspect(name: String, version: Version, syntax: String) -> Result<()> { /// **Arguments** /// * `latest`: If set to true, only shows latest version of each package. /// -/// **Returns** +/// **Returns** /// Nothing other than prints on stdout if successfull, or an ExecutorError otherwise. pub fn list(latest: bool) -> Result<(), PackageError> { // Get the directory with the packages @@ -233,7 +233,7 @@ pub fn list(latest: bool) -> Result<(), PackageError> { } // With the list constructed, add each entry - let now = Utc::now().timestamp(); + let now = Utc::now(); for entry in infos { // Derive the pathname for this package let package_path = packages_dir.join(&entry.name).join(entry.version.to_string()); @@ -246,8 +246,20 @@ pub fn list(latest: bool) -> Result<(), PackageError> { let version = pad_str(&sversion, 10, Alignment::Left, Some("..")); let skind = format!("{}", entry.kind); let kind = pad_str(&skind, 10, Alignment::Left, Some("..")); - let elapsed = Duration::from_secs((now - entry.created.timestamp()) as u64); - let created = format!("{} ago", HumanDuration(elapsed)); + // let elapsed = Duration::from_secs((now - entry.created.timestamp()) as u64); + let created = match (now - entry.created).to_std() { + Ok(elapsed) => format!("{} ago", HumanDuration(elapsed)), + // This function errors when duration is less than zero. + // As standard library implementation is limited to non-negative values. + Err(_) => { + if entry.created.date_naive() == now.date_naive() { + format!("{} (future)", entry.created.time()) + } else { + format!("{} (future)", entry.created.date_naive()) + } + }, + }; + let created = pad_str(&created, 15, Alignment::Left, None); let size = DecimalBytes(dir::get_size(package_path).unwrap()); @@ -271,9 +283,9 @@ pub fn list(latest: bool) -> Result<(), PackageError> { /// * `name`: The name of the package to load. /// * `version`: The Version of the package to load. Might be an unresolved 'latest'. /// -/// **Returns** +/// **Returns** /// Nothing on success, or else an error. -pub async fn load(name: String, version: Version) -> Result<()> { +pub async fn load(name: String, version: AliasedFunctionVersion) -> Result<()> { debug!("Loading package '{}' (version {})", name, &version); let package_dir = ensure_package_dir(&name, Some(&version), false)?; @@ -335,141 +347,162 @@ pub async fn load(name: String, version: Version) -> Result<()> { /// /// # Arguments /// - `force`: Whether or not to force removal (remove the image from the Docker daemon even if there are still containers using it). -/// - `packages`: The list of (name, Version) pairs to remove. +/// - `packages`: The list of (name, Version) pairs to remove. Version can be set to None if all +/// versions need to be removed. /// - `docker_opts`: Configuration for how to connect to the local Docker daemon. /// -/// # Returns +/// # Returns /// Nothing on success, or else an error. -pub async fn remove(force: bool, packages: Vec<(String, Version)>, docker_opts: DockerOptions) -> Result<(), PackageError> { +pub async fn remove(force: bool, packages: Vec<(String, Option)>, docker_opts: DockerOptions) -> Result<(), PackageError> { // Iterate over the packages for (name, version) in packages { // Remove without confirmation if explicity stated package version. - if !version.is_latest() { - // Try to resolve the directory for this pair - let package_dir = ensure_package_dir(&name, Some(&version), false).map_err(|source| PackageError::PackageVersionError { - name: name.clone(), - version, - source, - })?; - - // Ask for permission if needed - if !force { - println!("Are you sure you want to remove package {} version {}?", style(&name).bold().cyan(), style(&version).bold().cyan()); - println!(); - let consent: bool = Confirm::new().interact().map_err(|source| PackageError::ConsentError { source })?; - - if !consent { - return Ok(()); + match version { + Some(version) => { + let packages_dir = get_packages_dir()?; + let package_dir = packages_dir.join(&name); + + let version = match version.clone() { + AliasedFunctionVersion::Latest => brane_tsk::local::get_package_versions(&name, &package_dir) + .map_err(|source| UtilError::VersionsError { source })? + .into_iter() + .max() + .expect("We need at least one version in order to take the latest"), + AliasedFunctionVersion::Version(version) => version.clone(), + }; + + // Try to resolve the directory for this pair + let package_dir = ensure_package_dir(&name, Some(&version.clone().into()), false) + .map_err(|source| PackageError::PackageVersionError { name: name.clone(), version: version.clone(), source })?; + + // Ask for permission if needed + if !force { + println!("Are you sure you want to remove package {} version {}?", style(&name).bold().cyan(), style(&version).bold().cyan()); + println!(); + let consent: bool = Confirm::new().interact().map_err(|source| PackageError::ConsentError { source })?; + + if !consent { + return Ok(()); + } + } + + // If we got permission, get the digest of this version + let package_info_path = package_dir.join("package.yml"); + let package_info = PackageInfo::from_path(package_info_path.clone()) + .map_err(|source| PackageError::PackageInfoError { path: package_info_path.clone(), source })?; + let digest = package_info.digest.ok_or_else(|| PackageError::PackageInfoNoDigest { path: package_info_path.clone() })?; + + // Remove that image from the Docker daemon + let image: Image = Image::new(&package_info.name, Some(package_info.version.to_string()), Some(digest)); + docker::remove_image(&docker_opts, &image) + .await + .map_err(|source| PackageError::DockerRemoveError { image: Box::new(image), source })?; + + // Also remove the package files + fs::remove_dir_all(&package_dir).map_err(|source| PackageError::PackageRemoveError { + name: name.clone(), + version: Some(version.clone().into()), + dir: package_dir, + source, + })?; + + // If there are now no more packages left, remove the package directory itself as well + let package_dir = + ensure_package_dir(&name, None, false).map_err(|source| PackageError::PackageError { name: name.clone(), source })?; + + let versions = fs::read_dir(&package_dir).map_err(|source| PackageError::VersionsError { + name: name.clone(), + dir: package_dir.clone(), + source, + })?; + + if versions.count() == 0 { + // Attempt to remove the main dir + fs::remove_dir_all(&package_dir).map_err(|source| PackageError::PackageRemoveError { + name: name.clone(), + version: Some(version.clone().into()), + dir: package_dir.clone(), + source, + })?; } - } - // If we got permission, get the digest of this version - let package_info_path = package_dir.join("package.yml"); - let package_info = PackageInfo::from_path(package_info_path.clone()) - .map_err(|source| PackageError::PackageInfoError { path: package_info_path.clone(), source })?; - let digest = package_info.digest.ok_or_else(|| PackageError::PackageInfoNoDigest { path: package_info_path.clone() })?; - - // Remove that image from the Docker daemon - let image: Image = Image::new(&package_info.name, Some(format!("{}", package_info.version)), Some(digest)); - docker::remove_image(&docker_opts, &image).await.map_err(|source| PackageError::DockerRemoveError { image: Box::new(image), source })?; - - // Also remove the package files - fs::remove_dir_all(&package_dir).map_err(|source| PackageError::PackageRemoveError { - name: name.clone(), - version, - dir: package_dir, - source, - })?; - - // If there are now no more packages left, remove the package directory itself as well - let package_dir = ensure_package_dir(&name, None, false).map_err(|source| PackageError::PackageError { name: name.clone(), source })?; - match fs::read_dir(&package_dir) { - Ok(versions) => { - if versions.count() == 0 { - // Attempt to remove the main dir - fs::remove_dir_all(&package_dir).map_err(|source| PackageError::PackageRemoveError { + println!("Successfully removed version {} of package {}", style(&version).bold().cyan(), style(&name).bold().cyan()); + return Ok(()); + }, + None => { + // Otherwise, resolve the package directory only + let package_dir = + ensure_package_dir(&name, None, false).map_err(|source| PackageError::PackageError { name: name.clone(), source })?; + + // Look for packages. + let versions = fs::read_dir(&package_dir).map_err(|source| PackageError::VersionsError { + name: name.clone(), + dir: package_dir.clone(), + source, + })?; + + // Parse them all + let result = versions + .map(|version| { + let version = + version.map_err(|source| PackageError::VersionsError { name: name.clone(), dir: package_dir.clone(), source })?; + + // Parse the path as a Version + let version = String::from(version.file_name().to_string_lossy()); + let version = ConcreteFunctionVersion::from_str(&version).map_err(|source| PackageError::VersionParseError { name: name.clone(), - version, - dir: package_dir, + raw: version, source, })?; - } - }, - Err(source) => { - return Err(PackageError::VersionsError { name, dir: package_dir, source }); - }, - }; - // Donelet versions = - println!("Successfully removed version {} of package {}", style(&version).bold().cyan(), style(&name).bold().cyan()); - return Ok(()); - } + Ok(version) + }) + .collect::, PackageError>>()?; - // Otherwise, resolve the package directory only - let package_dir = ensure_package_dir(&name, None, false).map_err(|source| PackageError::PackageError { name: name.clone(), source })?; + // Done + let versions: Vec = result; - // Look for packages. - let versions: Vec = match fs::read_dir(&package_dir) { - Ok(versions) => { - // Parse them all - let mut result = Vec::with_capacity(3); - for version in versions { - // Resolve the entry - let version = version.map_err(|source| PackageError::VersionsError { name: name.clone(), dir: package_dir.clone(), source })?; - - // Parse the path as a Version - let version = String::from(version.file_name().to_string_lossy()); - let version = - Version::from_str(&version).map_err(|source| PackageError::VersionParseError { name: name.clone(), raw: version, source })?; - - // Add it to the list - result.push(version); + + // Ask for permission, if --force is not provided + if !force { + println!("Are you sure you want to remove the following version(s) of package {}?", style(&name).bold().cyan()); + for version in &versions { + println!("- {}", style(&version).bold().cyan()); + } + println!(); + let consent: bool = Confirm::new().interact().map_err(|source| PackageError::ConsentError { source })?; + if !consent { + continue; + } } - // Done - result - }, - Err(source) => { - return Err(PackageError::VersionsError { name, dir: package_dir, source }); - }, - }; + // Check if image is locally loaded in Docker and if so, remove it there first + for version in &versions { + // Get the digest of this version + let package_info_path = package_dir.join(version.to_string()).join("package.yml"); + let package_info = PackageInfo::from_path(package_info_path.clone()) + .map_err(|source| PackageError::PackageInfoError { path: package_info_path.clone(), source })?; + let digest = package_info.digest.ok_or_else(|| PackageError::PackageInfoNoDigest { path: package_info_path.clone() })?; + + // Remove that image from the Docker daemon + let image: Image = Image::new(&package_info.name, Some(package_info.version.to_string()), Some(digest)); + docker::remove_image(&docker_opts, &image) + .await + .map_err(|source| PackageError::DockerRemoveError { image: Box::new(image), source })?; + } - // Ask for permission, if --force is not provided - if !force { - println!("Are you sure you want to remove the following version(s) of package {}?", style(&name).bold().cyan()); - for version in &versions { - println!("- {}", style(&version).bold().cyan()); - } - println!(); - let consent: bool = Confirm::new().interact().map_err(|source| PackageError::ConsentError { source })?; - if !consent { - continue; - } - } + // Remove the package files + fs::remove_dir_all(&package_dir).map_err(|source| PackageError::PackageRemoveError { + name: name.clone(), + version, + dir: package_dir, + source, + })?; - // Check if image is locally loaded in Docker and if so, remove it there first - for version in &versions { - // Get the digest of this version - let package_info_path = package_dir.join(version.to_string()).join("package.yml"); - let package_info = PackageInfo::from_path(package_info_path.clone()) - .map_err(|source| PackageError::PackageInfoError { path: package_info_path.clone(), source })?; - let digest = package_info.digest.ok_or_else(|| PackageError::PackageInfoNoDigest { path: package_info_path.clone() })?; - - // Remove that image from the Docker daemon - let image: Image = Image::new(&package_info.name, Some(format!("{}", package_info.version)), Some(digest)); - docker::remove_image(&docker_opts, &image).await.map_err(|source| PackageError::DockerRemoveError { image: Box::new(image), source })?; + // Done + println!("Successfully removed package {}", style(&name).bold().cyan()); + }, } - - // Remove the package files - fs::remove_dir_all(&package_dir).map_err(|source| PackageError::PackageRemoveError { - name: name.clone(), - version, - dir: package_dir, - source, - })?; - - // Done - println!("Successfully removed package {}", style(&name).bold().cyan()); } // Done! diff --git a/brane-cli/src/registry.rs b/brane-cli/src/registry.rs index 3ca907ca..5f1653c7 100644 --- a/brane-cli/src/registry.rs +++ b/brane-cli/src/registry.rs @@ -17,7 +17,7 @@ use prettytable::Table; use prettytable::format::FormatBuilder; use reqwest::{self, Body, Client}; use specifications::package::{PackageInfo, PackageKind}; -use specifications::version::Version; +use specifications::version::{AliasedFunctionVersion, ConcreteFunctionVersion}; use tokio::fs::File as TokioFile; use tokio_util::codec::{BytesCodec, FramedRead}; use uuid::Uuid; @@ -76,7 +76,7 @@ pub fn get_data_endpoint() -> Result { /// /// # Errors /// This function may error for about a million different reasons, chief of which are the remote not being reachable, the user not being logged-in, not being able to write to the package folder, etc. -pub async fn pull(packages: Vec<(String, Version)>) -> Result<(), RegistryError> { +pub async fn pull(packages: Vec<(String, AliasedFunctionVersion)>) -> Result<(), RegistryError> { // Compile the GraphQL schema #[derive(GraphQLQuery)] #[graphql(schema_path = "src/graphql/api_schema.json", query_path = "src/graphql/get_package.graphql", response_derives = "Debug")] @@ -163,7 +163,7 @@ pub async fn pull(packages: Vec<(String, Version)>) -> Result<(), RegistryError> })?; // Next, the version - let version = Version::from_str(&package.version).map_err(|source| RegistryError::VersionParseError { + let version = ConcreteFunctionVersion::from_str(&package.version).map_err(|source| RegistryError::VersionParseError { url: url.clone(), raw: package.version.clone(), source, @@ -195,7 +195,7 @@ pub async fn pull(packages: Vec<(String, Version)>) -> Result<(), RegistryError> name: package.name.clone(), owners: package.owners.clone(), types, - version, + version: version.clone(), }; // Create the directory @@ -239,9 +239,9 @@ pub async fn pull(packages: Vec<(String, Version)>) -> Result<(), RegistryError> /// **Arguments** /// * `packages`: A list with name/ID / version pairs of the packages to push. /// -/// **Returns** +/// **Returns** /// Nothing on success, or an anyhow error on failure. -pub async fn push(packages: Vec<(String, Version)>) -> Result<(), RegistryError> { +pub async fn push(packages: Vec<(String, AliasedFunctionVersion)>) -> Result<(), RegistryError> { // Try to get the general package directory let packages_dir = ensure_packages_dir(false).map_err(|source| RegistryError::PackagesDirError { source })?; debug!("Using Brane package directory: {}", packages_dir.display()); @@ -252,29 +252,32 @@ pub async fn push(packages: Vec<(String, Version)>) -> Result<(), RegistryError> let package_dir = packages_dir.join(&name); // Resolve the version number - let version = if version.is_latest() { - // Get the list of versions - let mut versions = - get_package_versions(&name, &package_dir).map_err(|source| RegistryError::VersionsError { name: name.clone(), source })?; - - // Sort the versions and return the last one - versions.sort(); - versions[versions.len() - 1] - } else { - // Simply use the version given - version + let version = match version { + AliasedFunctionVersion::Latest => { + // Get the list of versions + let versions = + get_package_versions(&name, &package_dir).map_err(|source| RegistryError::VersionsError { name: name.clone(), source })?; + + if versions.is_empty() { + panic!("At least one version needs to be available for a package in order to take the latest"); + } + + // Sort the versions and return the last one + versions.into_iter().max().unwrap() + }, + AliasedFunctionVersion::Version(version) => { + // Simply use the version given + version + }, }; // Construct the full package directory with version - let package_dir = ensure_package_dir(&name, Some(&version), false).map_err(|source| RegistryError::PackageDirError { + let package_dir = ensure_package_dir(&name, Some(&version.clone().into()), false).map_err(|source| RegistryError::PackageDirError { name: name.clone(), - version, + version: version.clone(), source, })?; - // let temp_file = match tempfile::NamedTempFile::new() { - // Ok(file) => file, - // Err(err) => { return Err(RegistryError::TempFileError{ err }); } - // }; + let temp_path: std::path::PathBuf = std::env::temp_dir().join("temp.tar.gz"); let temp_file: File = File::create(&temp_path).unwrap(); @@ -288,17 +291,22 @@ pub async fn push(packages: Vec<(String, Version)>) -> Result<(), RegistryError> let mut tar = tar::Builder::new(gz); tar.append_path_with_name(package_dir.join("package.yml"), "package.yml").map_err(|source| RegistryError::CompressionError { name: name.clone(), - version, + version: version.clone(), path: temp_path.clone(), source, })?; tar.append_path_with_name(package_dir.join("image.tar"), "image.tar").map_err(|source| RegistryError::CompressionError { name: name.clone(), - version, + version: version.clone(), + path: temp_path.clone(), + source, + })?; + tar.into_inner().map_err(|source| RegistryError::CompressionError { + name: name.clone(), + version: version.clone(), path: temp_path.clone(), source, })?; - tar.into_inner().map_err(|source| RegistryError::CompressionError { name: name.clone(), version, path: temp_path.clone(), source })?; progress.finish(); // Upload file (with progress bar, of course) @@ -387,7 +395,7 @@ pub async fn search(term: Option) -> Result<()> { Ok(()) } -pub async fn unpublish(name: String, version: Version, force: bool) -> Result<()> { +pub async fn unpublish(name: String, version: ConcreteFunctionVersion, force: bool) -> Result<()> { #[derive(GraphQLQuery)] #[graphql(schema_path = "src/graphql/api_schema.json", query_path = "src/graphql/unpublish_package.graphql", response_derives = "Debug")] pub struct UnpublishPackage; @@ -409,9 +417,7 @@ pub async fn unpublish(name: String, version: Version, force: bool) -> Result<() } // Prepare GraphQL query. - if version.is_latest() { - return Err(anyhow!("Cannot unpublish 'latest' package version; choose a version.")); - } + // TODO: Add support for "latest" let variables = unpublish_package::Variables { name, version: version.to_string() }; let graphql_query = UnpublishPackage::build_query(variables); diff --git a/brane-cli/src/spec.rs b/brane-cli/src/spec.rs index 12787936..3a267f1f 100644 --- a/brane-cli/src/spec.rs +++ b/brane-cli/src/spec.rs @@ -23,7 +23,7 @@ use brane_tsk::docker::DockerOptions; use parking_lot::Mutex; use specifications::data::DataIndex; use specifications::package::PackageIndex; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; use crate::errors::HostnameParseError; @@ -119,14 +119,16 @@ impl FromStr for Hostname { /// Parses a version number that scopes a particular operation down. In other words, can be a specific version number or `all`. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct VersionFix(pub Option); +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VersionFix(pub Option); impl Display for VersionFix { #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> FResult { write!(f, "{}", if let Some(version) = self.0 { version.to_string() } else { "all".into() }) } + fn fmt(&self, f: &mut Formatter<'_>) -> FResult { + write!(f, "{}", if let Some(ref version) = self.0 { version.to_string() } else { "all".into() }) + } } impl FromStr for VersionFix { - type Err = specifications::version::ParseError; + type Err = specifications::version::SemverError; fn from_str(s: &str) -> Result { // Parse the auto first @@ -134,7 +136,7 @@ impl FromStr for VersionFix { return Ok(Self(None)); } // Otherwise, delegate to the version parser - Ok(Self(Some(Version::from_str(s)?))) + Ok(Self(Some(ConcreteFunctionVersion::from_str(s)?))) } } diff --git a/brane-cli/src/test.rs b/brane-cli/src/test.rs index e5af7d5b..89f7f479 100644 --- a/brane-cli/src/test.rs +++ b/brane-cli/src/test.rs @@ -22,7 +22,7 @@ use brane_tsk::input::prompt_for_input; use console::style; use specifications::data::DataIndex; use specifications::package::PackageInfo; -use specifications::version::Version; +use specifications::version::AliasedFunctionVersion; use crate::errors::TestError; use crate::repl::Snippet; @@ -93,7 +93,7 @@ fn write_value(value: FullValue) -> String { /// This function errors if any part of that dance failed. pub async fn handle( name: impl Into, - version: Version, + version: AliasedFunctionVersion, show_result: Option, docker_opts: DockerOptions, keep_containers: bool, @@ -101,11 +101,15 @@ pub async fn handle( let name: String = name.into(); // Read the package info of the given package - let package_dir = - ensure_package_dir(&name, Some(&version), false).map_err(|source| TestError::PackageDirError { name: name.clone(), version, source })?; + let package_dir = ensure_package_dir(&name, Some(&version), false).map_err(|source| TestError::PackageDirError { + name: name.clone(), + version: version.clone(), + source, + })?; + let package_info = PackageInfo::from_path(package_dir.join("package.yml")).map_err(|source| TestError::PackageInfoError { name: name.clone(), - version, + version: version.clone(), source, })?; diff --git a/brane-cli/src/upgrade.rs b/brane-cli/src/upgrade.rs index 1afd508d..5d6bef9d 100644 --- a/brane-cli/src/upgrade.rs +++ b/brane-cli/src/upgrade.rs @@ -22,7 +22,7 @@ use std::path::{Path, PathBuf}; use console::style; use log::{debug, info, warn}; use serde::Serialize; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; use crate::old_configs::v1_0_0; use crate::spec::VersionFix; @@ -68,7 +68,7 @@ pub enum Error { FileMetadataRead { path: PathBuf, err: std::io::Error }, /// Failed to convert between the infos - Convert { what: &'static str, version: Version, err: Box }, + Convert { what: &'static str, version: ConcreteFunctionVersion, err: Box }, /// Failed to serialize the new info. Serialize { what: &'static str, err: serde_yaml::Error }, /// Failed to create a new file. @@ -132,7 +132,7 @@ impl error::Error for Error { fn upgrade( what: &'static str, path: impl Into, - versions: Vec<(Version, VersionParser)>, + versions: Vec<(ConcreteFunctionVersion, VersionParser)>, dry_run: bool, overwrite: bool, ) -> Result<(), Error> { @@ -314,16 +314,11 @@ pub fn data(path: impl Into, dry_run: bool, overwrite: bool, version: V info!("Upgrading data.yml files in '{}'...", path.display()); // Construct the list of versions - let mut versions: Vec<(Version, VersionParser)> = vec![( - Version::new(1, 0, 0), + let mut versions: Vec<(ConcreteFunctionVersion, VersionParser)> = vec![( + ConcreteFunctionVersion::new(1, 0, 0), Box::new(|raw: &str| -> Option> { // Attempt to read it with the file - let cfg: v1_0_0::DataInfo = match serde_yaml::from_str(raw) { - Ok(cfg) => cfg, - Err(_) => { - return None; - }, - }; + let cfg: v1_0_0::DataInfo = serde_yaml::from_str(raw).ok()?; // Return a function for converting it to a new-style function Some(Box::new(move |_dir: &Path, _overwrite: bool| -> Result { diff --git a/brane-cli/src/utils.rs b/brane-cli/src/utils.rs index 059624b0..6585951f 100644 --- a/brane-cli/src/utils.rs +++ b/brane-cli/src/utils.rs @@ -17,7 +17,7 @@ use std::io::Read; use std::path::{Path, PathBuf}; use specifications::package::PackageKind; -use specifications::version::Version; +use specifications::version::AliasedFunctionVersion; // use crate::{MIN_DOCKER_VERSION, MIN_BUILDX_VERSION}; use crate::errors::UtilError; @@ -32,14 +32,14 @@ pub enum DependencyError { DockerNotInstalled, /// Docker has a too low version #[error("Docker version is {got}, but Brane requires version {expected} or later")] - DockerMinNotMet { got: Version, expected: Version }, + DockerMinNotMet { got: semver::Version, expected: semver::Version }, /// The Buildkit plugin is not installed for Docker #[error("Local Docker instance does not have the Buildkit plugin installed")] BuildkitNotInstalled, /// The Buildkit plugin has an incorrect version #[error("Buildkit plugin for Docker version is {got}, but Brane requires version {expected} or later")] - BuildKitMinNotMet { got: Version, expected: Version }, + BuildKitMinNotMet { got: semver::Version, expected: semver::Version }, } /***** UTILITIES *****/ @@ -47,7 +47,7 @@ pub enum DependencyError { /// /// Checks the runtime dependencies of brane-cli (Docker + BuildKit) /// -/// **Returns** +/// **Returns** /// Nothing if the dependencies are met, a DependencyError if it wasn't, or a UtilError if we couldn't determine. pub async fn check_dependencies() -> Result, UtilError> { // We checked all the runtime dependencies! (:sweat:) @@ -65,7 +65,7 @@ pub async fn check_dependencies() -> Result, UtilErr /// **Arguments** /// * `dir`: The directory the is the root of a package. /// -/// **Returns** +/// **Returns** /// A PathBuf pointing to what we think is the package file, or else a CliError if we could not determine it or something went wrong. pub fn determine_file(dir: &Path) -> Result { // Open an iterator over the directory's files @@ -97,7 +97,7 @@ pub fn determine_file(dir: &Path) -> Result { /// **Arguments** /// * `path`: Path to file from which we'd like to deduce the kind. /// -/// **Returns** +/// **Returns** /// The PackageKind if we could deduce it, or some sort of CliError if we could not or something went wrong. pub fn determine_kind(path: &Path) -> Result { // See if the filename convention allows us to choose a package kind @@ -142,7 +142,7 @@ pub fn determine_kind(path: &Path) -> Result { /// /// Returns the path of the configuration directory. Is guaranteed to be an absolute path when it returns successfully (but _not_ that it also exists!). /// -/// **Returns** +/// **Returns** /// The path of the Brane configuration directory if successful, or a UtilError otherwise. pub fn get_config_dir() -> Result { // Try to get the user directory @@ -157,7 +157,7 @@ pub fn get_config_dir() -> Result { /// **Arguments** /// * `create`: If true, creates the directory if it does not exist; if false, throws an error. /// -/// **Returns** +/// **Returns** /// The path of the Brane configuration directory if successful, or a UtilError otherwise. pub fn ensure_config_dir(create: bool) -> Result { // Get the brane directory @@ -181,7 +181,7 @@ pub fn ensure_config_dir(create: bool) -> Result { /// /// Returns the location of the history file for Brane. /// -/// **Returns** +/// **Returns** /// The path of the HistoryFile or a UtilError otherwise. pub fn get_history_file() -> Result { // Get the config dir @@ -196,7 +196,7 @@ pub fn get_history_file() -> Result { /// **Arguments** /// * `create`: If true, creates the directory if it does not exist; if false, throws an error. /// -/// **Returns** +/// **Returns** /// The path of the HistoryFile or a UtilError otherwise. pub fn ensure_history_file(create: bool) -> Result { // Get the path to the history file @@ -227,7 +227,7 @@ pub fn ensure_history_file(create: bool) -> Result { /// **Arguments** /// * `create`: If set to true, creates the missing file and directories instead of throwing errors. /// -/// **Returns** +/// **Returns** /// A PathBuf with the absolute path that is guaranteed to exist, or an UtilError otherwise. pub fn get_data_dir() -> Result { // Try to get the user directory @@ -242,7 +242,7 @@ pub fn get_data_dir() -> Result { /// **Arguments** /// * `create`: If true, creates the directory if it does not exist; if false, throws an error. /// -/// **Returns** +/// **Returns** /// A PathBuf with the absolute path that is guaranteed to exist, or an UtilError otherwise. pub fn ensure_data_dir(create: bool) -> Result { // Get the data directory @@ -264,11 +264,11 @@ pub fn ensure_data_dir(create: bool) -> Result { /// **Edited: Changed to return UtilErrors.** /// -/// Returns the general package directory based on the user's home folder. -/// Basically, tries to resolve the folder '~/.local/share/brane/packages`. +/// Returns the general package directory based on the user's home folder. +/// Basically, tries to resolve the folder '~/.local/share/brane/packages`. /// Note that this does not mean that this directory exists. /// -/// **Returns** +/// **Returns** /// A PathBuf with an absolute path to the packages dir, or an UtilError otherwise. pub fn get_packages_dir() -> Result { // Get the data directory @@ -278,13 +278,13 @@ pub fn get_packages_dir() -> Result { Ok(data_dir.join("packages")) } -/// Makes sure that Brane's packages directory exists, and then returns its path. +/// Makes sure that Brane's packages directory exists, and then returns its path. /// Basically, tries to resolve the folder '~/.local/share/brane/packages`. /// /// **Arguments** /// * `create`: If set to true, creates the missing file and directories instead of throwing errors. /// -/// **Returns** +/// **Returns** /// A PathBuf with the absolute path that is guaranteed to exist, or an UtilError otherwise. pub fn ensure_packages_dir(create: bool) -> Result { // Get the packages directory @@ -308,8 +308,8 @@ pub fn ensure_packages_dir(create: bool) -> Result { Ok(packages_dir) } -/// Returns the general data directory based on the user's home folder. -/// Basically, tries to resolve the folder `~/.local/share/brane/data`. +/// Returns the general data directory based on the user's home folder. +/// Basically, tries to resolve the folder `~/.local/share/brane/data`. /// Note that this does not mean that this directory exists. /// /// # Returns @@ -325,7 +325,7 @@ pub fn get_datasets_dir() -> Result { Ok(data_dir.join("data")) } -/// Makes sure that Brane's dataset directory exists, and then returns its path. +/// Makes sure that Brane's dataset directory exists, and then returns its path. /// Basically, tries to resolve the folder `~/.local/share/brane/data`. /// /// # Arguments @@ -358,47 +358,44 @@ pub fn ensure_datasets_dir(create: bool) -> Result { /// **Edited: Now returning UtilErrors.** /// -/// Gets the directory where we likely stored the package. -/// If the given version is omitted, just returns the package directory for this name. -/// If the given version is latest, tries to find the latest version directory to return that; otherwise, errors that there are no versions to choose from. +/// Gets the directory where we likely stored the package. +/// If the given version is omitted, just returns the package directory for this name. +/// If the given version is latest, tries to find the latest version directory to return that; otherwise, errors that there are no versions to choose from. /// Does not guarantee that the directory also exists; check ensure_package_dir() for that. /// /// **Arguments** /// * `name`: The name of the package we want to get the directory from. /// * `version`: The version of the package. Is optional to have a package directory that ignores versions. /// -/// **Returns** +/// **Returns** /// A PathBuf with the directory if successfull, or an UtilError otherwise. -pub fn get_package_dir(name: &str, version: Option<&Version>) -> Result { +pub fn get_package_dir(name: &str, version: Option<&AliasedFunctionVersion>) -> Result { // Try to get the general package directory + the name of the package let packages_dir = get_packages_dir()?; let package_dir = packages_dir.join(name); - // If there is no version, call it quits here - if version.is_none() { - return Ok(package_dir); - } + // If there is no version, return the parent package dir (without version) + let version = match version { + Some(version) => version, + None => return Ok(package_dir), + }; // Otherwise, resolve the version number if its 'latest' - let version = version.unwrap(); - let version = if version.is_latest() { - // Get the list of versions - let mut versions = brane_tsk::local::get_package_versions(name, &package_dir).map_err(|source| UtilError::VersionsError { source })?; - - // Sort the versions and return the last one - versions.sort(); - versions[versions.len() - 1] - } else { - // Simply use the given version - *version + let version = match version { + AliasedFunctionVersion::Latest => brane_tsk::local::get_package_versions(name, &package_dir) + .map_err(|source| UtilError::VersionsError { source })? + .into_iter() + .max() + .expect("We need at least one version in order to take the latest"), + AliasedFunctionVersion::Version(version) => version.clone(), }; // Return the path with the version appended to it Ok(package_dir.join(version.to_string())) } -/// Makes sure that the package directory for the given name/version pair exists, then returns the path to it. -/// If the given version is omitted, just returns the package directory for this name. +/// Makes sure that the package directory for the given name/version pair exists, then returns the path to it. +/// If the given version is omitted, just returns the package directory for this name. /// If the given version is latest, tries to find the latest version directory to return that; otherwise, always errors (regardless of 'create'). /// /// **Arguments** @@ -406,11 +403,27 @@ pub fn get_package_dir(name: &str, version: Option<&Version>) -> Result, create: bool) -> Result { +pub fn ensure_package_dir(name: &str, version: Option<&AliasedFunctionVersion>, create: bool) -> Result { + let packages_dir = get_packages_dir()?; + let package_dir = packages_dir.join(name); + + // Otherwise, resolve the version number if its 'latest' + let version = match version { + Some(AliasedFunctionVersion::Latest) => Some( + brane_tsk::local::get_package_versions(name, &package_dir) + .map_err(|source| UtilError::VersionsError { source })? + .into_iter() + .max() + .expect("We need at least one version in order to take the latest"), + ), + Some(AliasedFunctionVersion::Version(version)) => Some(version.clone()), + None => None, + }; + // Retrieve the path for this version - let package_dir = get_package_dir(name, version)?; + let package_dir = get_package_dir(name, version.clone().map(|v| v.into()).as_ref())?; // Make sure it exists if !package_dir.exists() { @@ -425,12 +438,12 @@ pub fn ensure_package_dir(name: &str, version: Option<&Version>, create: bool) - // Now create the directory fs::create_dir_all(&package_dir).map_err(|source| UtilError::VersionDirCreateError { package: name.to_string(), - version: *version, + version: version.clone(), path: package_dir.clone(), source, })?; } else { - return Err(UtilError::VersionDirNotFound { package: name.to_string(), version: *version, path: package_dir }); + return Err(UtilError::VersionDirNotFound { package: name.to_string(), version: version.clone(), path: package_dir }); } }, @@ -457,7 +470,7 @@ pub fn ensure_package_dir(name: &str, version: Option<&Version>, create: bool) - Ok(package_dir) } -/// Gets the directory where we likely stored a dataset. +/// Gets the directory where we likely stored a dataset. /// Does not guarantee that the directory also exists; check `ensure_dataset_dir()` for that. /// /// # Generic arguments @@ -650,7 +663,7 @@ pub fn get_active_instance_link() -> Result { /// **Arguments** /// * `s`: The string to capitalize. /// -/// **Returns** +/// **Returns** /// A copy of the given string with the first letter in uppercase. pub fn uppercase_first_letter(s: &str) -> String { let mut c = s.chars(); @@ -667,7 +680,7 @@ pub fn uppercase_first_letter(s: &str) -> String { /// **Arguments** /// * `name`: The name to check. /// -/// **Returns** +/// **Returns** /// Nothing if the name is valid, or a UtilError otherwise. pub fn assert_valid_bakery_name(name: &str) -> Result<(), UtilError> { if name.chars().all(|c| c.is_alphanumeric() || c == '_') { Ok(()) } else { Err(UtilError::InvalidBakeryName { name: name.to_string() }) } diff --git a/brane-cli/src/version.rs b/brane-cli/src/version.rs index 9dbcb0d4..ddd16a63 100644 --- a/brane-cli/src/version.rs +++ b/brane-cli/src/version.rs @@ -17,7 +17,7 @@ use std::str::FromStr; use log::debug; use reqwest::{Response, StatusCode}; use specifications::arch::Arch; -use specifications::version::Version; +use specifications::version::BraneVersion; use crate::errors::VersionError; use crate::instance::InstanceInfo; @@ -30,7 +30,7 @@ struct LocalVersion { /// The architecture as reported by `uname -m` arch: Arch, /// The version as reported by the env - version: Version, + version: BraneVersion, } impl LocalVersion { @@ -42,7 +42,7 @@ impl LocalVersion { /// A new LocalVersion instance on success, or else a VersionError. fn new() -> Result { // Parse the env - let version = Version::from_str(env!("CARGO_PKG_VERSION")) + let version = BraneVersion::from_str(env!("CARGO_PKG_VERSION")) .map_err(|source| VersionError::VersionParseError { raw: env!("CARGO_PKG_VERSION").to_string(), source })?; // Done, return the struct @@ -58,7 +58,7 @@ struct RemoteVersion { /// The architecture as reported by the remote _arch: Arch, /// The version as downloaded from the remote - version: Version, + version: BraneVersion, } impl RemoteVersion { @@ -99,7 +99,7 @@ impl RemoteVersion { // Try to parse the version debug!(" > Parsing remote version..."); - let version = Version::from_str(&version_body).map_err(|source| VersionError::VersionParseError { raw: version_body, source })?; + let version = BraneVersion::from_str(&version_body).map_err(|source| VersionError::VersionParseError { raw: version_body, source })?; // Done! debug!("Remote version number: {}", &version); diff --git a/brane-cli/src/vm.rs b/brane-cli/src/vm.rs index ed636084..37290a34 100644 --- a/brane-cli/src/vm.rs +++ b/brane-cli/src/vm.rs @@ -38,6 +38,7 @@ use specifications::data::{AccessKind, DataIndex, DataInfo, DataName, Preprocess use specifications::package::{PackageIndex, PackageInfo}; use specifications::pc::ProgramCounter; use specifications::profiling::ProfileScopeHandle; +use specifications::version::AliasedFunctionVersion; use specifications::wir::Workflow; use specifications::wir::locations::Location; use tokio::fs as tfs; @@ -102,12 +103,11 @@ impl VmPlugin for OfflinePlugin { (state.docker_opts.clone(), state.package_dir.clone(), state.results_dir.clone(), state.pindex.clone(), state.keep_containers) }; - // Next, we resolve the package - let pinfo: &PackageInfo = - match pindex.get(info.package_name, if info.package_version.is_latest() { None } else { Some(info.package_version) }) { - Some(pinfo) => pinfo, - None => return Err(ExecuteError::UnknownPackage { name: info.package_name.into(), version: *info.package_version }), - }; + let aliased_version = AliasedFunctionVersion::Version(info.package_version.clone()); + let pinfo: &PackageInfo = match pindex.get(info.package_name, &aliased_version) { + Some(pinfo) => pinfo, + None => return Err(ExecuteError::UnknownPackage { name: info.package_name.into(), version: aliased_version }), + }; get.stop(); // Resolve the input arguments, generating the folders we have to bind @@ -117,7 +117,7 @@ impl VmPlugin for OfflinePlugin { let params: String = serde_json::to_string(&info.args).map_err(|source| ExecuteError::ArgsEncodeError { source })?; // Create an ExecuteInfo with that - let image: Image = Image::new(info.package_name, Some(info.package_version), Some(pinfo.digest.as_ref().unwrap())); + let image: Image = Image::new(info.package_name, Some(format!("{}", info.package_version)), Some(pinfo.digest.as_ref().unwrap())); let einfo: ExecuteInfo = ExecuteInfo { name: info.name.into(), image: image.clone(), diff --git a/brane-ctl/src/cli.rs b/brane-ctl/src/cli.rs index 12817dee..1e68c49f 100644 --- a/brane-ctl/src/cli.rs +++ b/brane-ctl/src/cli.rs @@ -13,7 +13,7 @@ use jsonwebtoken::jwk::KeyAlgorithm; use specifications::address::{Address, AddressOpt}; use specifications::arch::Arch; use specifications::package::Capability; -use specifications::version::Version; +use specifications::version::BraneVersion; /***** ARGUMENTS *****/ /// The server-side Brane command-line interface. @@ -78,7 +78,7 @@ pub(crate) enum CtlSubcommand { /// The specific Brane version to start. #[clap(short, long, default_value = env!("CARGO_PKG_VERSION"), help = "The Brane version to import.")] - version: Version, + version: BraneVersion, /// Sets the '$IMG_DIR' variable, which can easily switch the location of compiled binaries. #[clap( @@ -526,7 +526,8 @@ pub(crate) enum PackageSubcommand { #[clap( name = "IMAGE", help = "The image to compute the hash of. If it's a path that exists, will attempt to hash that file; otherwise, will hash based on an \ - image in the local node's `packages` directory. You can use `name[:version]` syntax to specify the version." + image in the local node's `packages` directory. You can use `name[:version]` syntax to specify the version. If the version is \ + omitted, the latest version is hashed." )] image: String, }, diff --git a/brane-ctl/src/errors.rs b/brane-ctl/src/errors.rs index bb207edc..4674d0ef 100644 --- a/brane-ctl/src/errors.rs +++ b/brane-ctl/src/errors.rs @@ -23,7 +23,7 @@ use console::style; use enum_debug::EnumDebug as _; use jsonwebtoken::jwk::KeyAlgorithm; use specifications::container::Image; -use specifications::version::Version; +use specifications::version::AliasedFunctionVersion; /***** LIBRARY *****/ @@ -219,7 +219,7 @@ pub enum LifetimeError { DockerComposeNotAFile { path: PathBuf }, /// Relied on a build-in for a Docker Compose version that is not the default one. #[error("No baked-in {kind} Docker Compose for Brane version v{version} exists (give it yourself using '--file')")] - DockerComposeNotBakedIn { kind: NodeKind, version: Version }, + DockerComposeNotBakedIn { kind: NodeKind, version: semver::Version }, /// Failed to open a new Docker Compose file. #[error("Failed to create Docker Compose file '{}'", path.display())] DockerComposeCreateError { path: PathBuf, source: std::io::Error }, @@ -245,7 +245,7 @@ pub enum LifetimeError { #[error("Failed to get digest of image {}", style(path.display()).bold())] ImageDigestError { path: PathBuf, source: brane_tsk::docker::Error }, /// Failed to load/import the given image. - #[error("Failed to load image {} from '{}'", style(image).bold(), style(source).bold())] + #[error("Failed to load image {} from '{}'", style(image_source).bold(), style(source).bold())] ImageLoadError { image: Box, image_source: Box, source: brane_tsk::docker::Error }, /// The user gave us a proxy service definition, but not a proxy file path. @@ -281,6 +281,9 @@ pub enum LifetimeError { /// Failed to generate a new JWT given the given key. #[error("Failed to generate a JWT with the given key {key:?}")] TokenGenerate { key: PathBuf, source: specifications::policy::Error }, + + #[error("Failed to generate a temporary file")] + TempFile { what: String, source: std::io::Error }, } /// Errors that relate to package subcommands. @@ -298,7 +301,7 @@ pub enum PackagesError { FileNotAFile { path: PathBuf }, /// Failed to parse the given `NAME[:VERSION]` pair. #[error("Failed to parse given image name[:version] pair '{raw}'")] - IllegalNameVersionPair { raw: String, source: specifications::version::ParseError }, + IllegalNameVersionPair { raw: String, source: specifications::version::VersionError }, /// Failed to read the given directory #[error("Failed to read {} directory '{}'", what, path.display())] DirReadError { what: &'static str, path: PathBuf, source: std::io::Error }, @@ -307,7 +310,7 @@ pub enum PackagesError { DirEntryReadError { what: &'static str, entry: usize, path: PathBuf, source: std::io::Error }, /// The given `NAME[:VERSION]` pair did not have a candidate. #[error("No image for package '{}', version {} found in '{}'", name, version, path.display())] - UnknownImage { path: PathBuf, name: String, version: Version }, + UnknownImage { path: PathBuf, name: String, version: AliasedFunctionVersion }, /// Failed to hash the found image file. #[error("Failed to hash image")] HashError { source: brane_tsk::docker::Error }, diff --git a/brane-ctl/src/lifetime.rs b/brane-ctl/src/lifetime.rs index 89a7d09e..b0361b2c 100644 --- a/brane-ctl/src/lifetime.rs +++ b/brane-ctl/src/lifetime.rs @@ -31,14 +31,15 @@ use brane_cfg::node::{ ProxyServices, WorkerConfig, WorkerPaths, WorkerServices, }; use brane_cfg::proxy; +use brane_shr::fs::MaybeTempPath; use brane_tsk::docker::{DockerOptions, ImageSource, ensure_image, get_digest}; use console::style; -use log::{debug, info}; +use log::{debug, info, warn}; use rand::Rng; use rand::distr::Alphanumeric; use serde::Serialize; use specifications::container::Image; -use specifications::version::Version; +use specifications::version::BraneVersion; pub use crate::errors::LifetimeError as Error; use crate::spec::{LogsOpts, StartOpts, StartSubcommand}; @@ -202,7 +203,7 @@ fn resolve_exe(exe: impl AsRef) -> Result<(String, Vec), Error> { /// /// # Errors /// This function errors if we failed to verify the given file exists, or failed to unpack the builtin file. -fn resolve_docker_compose_file(file: Option, kind: NodeKind, mut version: Version) -> Result { +fn resolve_docker_compose_file(file: Option, kind: NodeKind) -> Result { // Switch on whether it exists or not match file { Some(file) => { @@ -216,51 +217,47 @@ fn resolve_docker_compose_file(file: Option, kind: NodeKind, mut versio // OK debug!("Using given file '{}'", file.display()); - Ok(file) + Ok(MaybeTempPath::Fixed(file)) }, None => { // It does not; unpack the builtins - // Verify the version matches what we have - if version.is_latest() { - version = Version::from_str(env!("CARGO_PKG_VERSION")).unwrap(); - } - if version != Version::from_str(env!("CARGO_PKG_VERSION")).unwrap() { - return Err(Error::DockerComposeNotBakedIn { kind, version }); - } - // Write the target location if it does not yet exist - let compose_path: PathBuf = PathBuf::from("/tmp").join(format!("docker-compose-{kind}-{version}.yml")); - if !compose_path.exists() { - debug!("Unpacking baked-in {} Docker Compose file to '{}'...", kind, compose_path.display()); - - // Attempt to open the target location - let mut handle: File = - File::create(&compose_path).map_err(|source| Error::DockerComposeCreateError { path: compose_path.clone(), source })?; - - // Write the correct file to it - match kind { - NodeKind::Central => { - write!(handle, "{}", include_str!("../../docker-compose-central.yml")) - .map_err(|source| Error::DockerComposeWriteError { path: compose_path.clone(), source })?; - }, - - NodeKind::Worker => { - write!(handle, "{}", include_str!("../../docker-compose-worker.yml")) - .map_err(|source| Error::DockerComposeWriteError { path: compose_path.clone(), source })?; - }, - - NodeKind::Proxy => { - write!(handle, "{}", include_str!("../../docker-compose-proxy.yml")) - .map_err(|source| Error::DockerComposeWriteError { path: compose_path.clone(), source })?; - }, - } + let compose_path = tempfile::Builder::new() + .prefix(&format!("docker-compose-{kind}")) + .suffix(".yml") + .tempfile() + .map_err(|source| Error::TempFile { what: format!("docker-compose-{kind}.yml"), source })? + .into_temp_path(); + + debug!("Unpacking baked-in {} Docker Compose file to '{}'...", kind, compose_path.display()); + + // Attempt to open the target location + let mut handle: File = + File::create(&compose_path).map_err(|source| Error::DockerComposeCreateError { path: compose_path.to_path_buf(), source })?; + + // Write the correct file to it + match kind { + NodeKind::Central => { + write!(handle, "{}", include_str!("../../docker-compose-central.yml")) + .map_err(|source| Error::DockerComposeWriteError { path: compose_path.to_path_buf(), source })?; + }, + + NodeKind::Worker => { + write!(handle, "{}", include_str!("../../docker-compose-worker.yml")) + .map_err(|source| Error::DockerComposeWriteError { path: compose_path.to_path_buf(), source })?; + }, + + NodeKind::Proxy => { + write!(handle, "{}", include_str!("../../docker-compose-proxy.yml")) + .map_err(|source| Error::DockerComposeWriteError { path: compose_path.to_path_buf(), source })?; + }, } // OK debug!("Using baked-in file '{}'", compose_path.display()); - Ok(compose_path) + Ok(MaybeTempPath::Temp(compose_path)) }, } } @@ -460,7 +457,7 @@ fn generate_override_file(node_config: &NodeConfig, hosts: &HashMap, ImageSource>, version: &Version) -> Result<(), Error> { +async fn load_images(docker: &Docker, images: HashMap, ImageSource>, version: &BraneVersion) -> Result<(), Error> { // Iterate over the images for (name, image_source) in images { let name: &str = name.as_ref(); @@ -474,12 +471,12 @@ async fn load_images(docker: &Docker, images: HashMap, ImageSour let digest: String = get_digest(path).await.map_err(|source| Error::ImageDigestError { path: path.into(), source })?; // Return it - Image::new(name, Some(version), Some(digest)) + Image::new(name, Some(version.to_string().replace("+", "_")), Some(digest)) }, ImageSource::Registry(source) => { println!("Loading image {} from repository {}...", style(name).green().bold(), style(source).bold()); - Image::new(name, Some(version), None::<&str>) + Image::new(name, Some(version.to_string().replace("+", "_")), None::<&str>) }, }; @@ -507,10 +504,11 @@ async fn load_images(docker: &Docker, images: HashMap, ImageSour /// /// # Errors /// This function errors if we fail to canonicalize any of the paths in `node_config`. -fn construct_envs(version: &Version, node_config_path: &Path, node_config: &NodeConfig) -> Result, Error> { +fn construct_envs(version: &BraneVersion, node_config_path: &Path, node_config: &NodeConfig) -> Result, Error> { // Set the global ones first let mut res: HashMap<&str, OsString> = HashMap::from([ ("BRANE_VERSION", OsString::from(version.to_string())), + ("BRANE_IMAGE_VERSION", OsString::from(version.to_string().replace("+", "_"))), ("NODE_CONFIG_PATH", canonicalize(node_config_path)?.as_os_str().into()), ]); @@ -766,9 +764,25 @@ pub async fn start( debug!("Loading node config file '{}'...", node_config_path.display()); let node_config: NodeConfig = NodeConfig::from_path(&node_config_path).map_err(|source| Error::NodeConfigLoadError { source })?; + + if file.is_none() { + let ctl_version = semver::Version::from_str(env!("CARGO_PKG_VERSION")).unwrap(); + let version_req = semver::VersionReq::parse(&format!("^{}", opts.version)).unwrap(); + + // We advise using branectl that is the same major version and at least as old + if !version_req.matches(&ctl_version) { + warn!( + "using an incompatible branectl version for the provided brane version. This can cause issues. branectl version: {}; provided \ + version: {}", + env!("CARGO_PKG_VERSION"), + opts.version + ); + } + } + // Resolve the Docker Compose file debug!("Resolving Docker Compose file..."); - let file: PathBuf = resolve_docker_compose_file(file, node_config.node.kind(), opts.version)?; + let file = resolve_docker_compose_file(file, node_config.node.kind())?; // Match on the command match command { @@ -926,12 +940,13 @@ pub fn stop(compose_verbose: bool, exe: impl AsRef, file: Option, debug!("Loading node config file '{}'...", node_config_path.display()); let node_config: NodeConfig = NodeConfig::from_path(&node_config_path).map_err(|source| Error::NodeConfigLoadError { source })?; + let brane_version = BraneVersion::from_str(env!("CARGO_PKG_VERSION")).unwrap(); // Resolve the Docker Compose file debug!("Resolving Docker Compose file..."); - let file: PathBuf = resolve_docker_compose_file(file, node_config.node.kind(), Version::from_str(env!("CARGO_PKG_VERSION")).unwrap())?; + let file = resolve_docker_compose_file(file, node_config.node.kind())?; // Construct the environment variables - let envs: HashMap<&str, OsString> = construct_envs(&Version::latest(), &node_config_path, &node_config)?; + let envs: HashMap<&str, OsString> = construct_envs(&brane_version, &node_config_path, &node_config)?; // Resolve the filename and deduce the project name let file: PathBuf = resolve_node(file, match node_config.node.kind() { @@ -991,11 +1006,11 @@ pub async fn logs(exe: impl AsRef, file: Option, node_config_path: debug!("Loading node config file '{}'...", node_config_path.display()); let node_config: NodeConfig = NodeConfig::from_path(&node_config_path).map_err(|source| Error::NodeConfigLoadError { source })?; - let version = Version::from_str(env!("CARGO_PKG_VERSION")).unwrap(); + let version = BraneVersion::from_str(env!("CARGO_PKG_VERSION")).expect("Brane versions must always be semver compatible"); // Resolve the Docker Compose file debug!("Resolving Docker Compose file..."); - let file: PathBuf = resolve_docker_compose_file(file, node_config.node.kind(), version)?; + let file = resolve_docker_compose_file(file, node_config.node.kind())?; // Construct the environment variables let envs: HashMap<&str, OsString> = construct_envs(&version, &node_config_path, &node_config)?; diff --git a/brane-ctl/src/packages.rs b/brane-ctl/src/packages.rs index f80f76ec..cad87f75 100644 --- a/brane-ctl/src/packages.rs +++ b/brane-ctl/src/packages.rs @@ -22,7 +22,7 @@ use brane_cfg::info::Info as _; use brane_cfg::node::{NodeConfig, NodeKind, NodeSpecificConfig}; use brane_tsk::docker; use log::{debug, info, warn}; -use specifications::version::Version; +use specifications::version::{AliasedFunctionVersion, ConcreteFunctionVersion}; pub use crate::errors::PackagesError as Error; @@ -60,16 +60,21 @@ pub async fn hash(node_config_path: impl Into, image: impl Into return Err(Error::FileNotAFile { path: image_path }); } } else { - // It needs more work + // TODO: It needs more work // Split the image into a name and possible version number - let (name, version): (String, Version) = - Version::from_package_pair(&image).map_err(|source| Error::IllegalNameVersionPair { raw: image, source })?; + let (name, version) = AliasedFunctionVersion::from_package_pair(&image) + // Default to latest + .map(|(package, version)| (package, version.unwrap_or(AliasedFunctionVersion::Latest))) + .map_err(|source| Error::IllegalNameVersionPair { raw: image, source })?; // Start reading the packages directory let entries: ReadDir = fs::read_dir(&packages_path).map_err(|source| Error::DirReadError { what: "packages", path: packages_path.clone(), source })?; - let mut file: Option<(PathBuf, Version)> = None; + + let mut file: Option<(PathBuf, AliasedFunctionVersion)> = None; + let mut largest_version_opt: Option = None; + for (i, entry) in entries.enumerate() { // Unwrap the entry let entry: DirEntry = @@ -78,25 +83,19 @@ pub async fn hash(node_config_path: impl Into, image: impl Into // Attempt to analyse the filename by parsing it as a (name, version) pair let entry_name: OsString = entry.file_name(); let entry_name: Cow = entry_name.to_string_lossy(); - let dash_pos: usize = match entry_name.find('-') { - Some(pos) => pos, - None => { - warn!("Missing dash ('-') in file '{}' (skipping)", entry.path().display()); - continue; - }, + + let Some((ename, entry_name)) = entry_name.split_once('-') else { + warn!("Missing dash ('-') in file '{}' (skipping)", entry.path().display()); + continue; }; - let dot_pos: usize = match entry_name.rfind('.') { - Some(pos) => pos, - None => { - warn!("Missing extension dot ('.') in file '{}' (skipping)", entry.path().display()); - continue; - }, + + let Some((eversion, _)) = entry_name.rsplit_once('.') else { + warn!("Missing extension dot ('.') in file '{}' (skipping)", entry.path().display()); + continue; }; - let ename: &str = &entry_name[..dash_pos]; - let eversion: &str = &entry_name[dash_pos + 1..dot_pos]; // Attempt to parse the eversion - let eversion: Version = match Version::from_str(eversion) { + let eversion = match AliasedFunctionVersion::from_str(eversion) { Ok(eversion) => eversion, Err(err) => { warn!("File '{}' has illegal version number '{}': {} (skipping)", entry.path().display(), eversion, err); @@ -107,19 +106,25 @@ pub async fn hash(node_config_path: impl Into, image: impl Into // Check if this package checks out if name == ename { // Only write it if the version makes sense - if version.is_latest() { - // Check if it's 'latest' too or the highest - if eversion.is_latest() || file.is_none() || eversion > file.as_ref().unwrap().1 { - let is_latest: bool = eversion.is_latest(); - file = Some((entry.path(), eversion)); - if is_latest { - break; + match (&version, eversion) { + (AliasedFunctionVersion::Version(semver), AliasedFunctionVersion::Version(esemver)) if semver == &esemver => { + file = Some((entry.path(), AliasedFunctionVersion::Version(esemver))); + break; + }, + (AliasedFunctionVersion::Latest, AliasedFunctionVersion::Latest) => { + file = Some((entry.path(), AliasedFunctionVersion::Latest)); + break; + }, + (AliasedFunctionVersion::Latest, AliasedFunctionVersion::Version(esemver)) => { + if let Some(ref largest_version) = largest_version_opt { + if &esemver < largest_version { + continue; + } } - } - } else if version == eversion { - // Always accept it and stop searching - file = Some((entry.path(), eversion)); - break; + largest_version_opt = Some(esemver.clone()); + file = Some((entry.path(), AliasedFunctionVersion::Version(esemver))); + }, + _ => {}, } } } diff --git a/brane-ctl/src/spec.rs b/brane-ctl/src/spec.rs index 311eb869..b7b63e17 100644 --- a/brane-ctl/src/spec.rs +++ b/brane-ctl/src/spec.rs @@ -22,7 +22,7 @@ use brane_tsk::docker::{ClientVersion, ImageSource}; use clap::Subcommand; use enum_debug::EnumDebug; use specifications::address::Address; -use specifications::version::Version; +use specifications::version::{BraneVersion, ConcreteFunctionVersion}; use crate::errors::{InclusiveRangeParseError, PairParseError, PolicyInputLanguageParseError}; @@ -93,14 +93,16 @@ impl FromStr for ResolvableNodeKind { } /// Parses a version number that scopes a particular operation down. In other words, can be a specific version number or `all`. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct VersionFix(pub Option); +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VersionFix(pub Option); impl Display for VersionFix { #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> FResult { write!(f, "{}", if let Some(version) = self.0 { version.to_string() } else { "all".into() }) } + fn fmt(&self, f: &mut Formatter<'_>) -> FResult { + write!(f, "{}", if let Some(ref version) = self.0 { version.to_string() } else { "all".into() }) + } } impl FromStr for VersionFix { - type Err = specifications::version::ParseError; + type Err = specifications::version::SemverError; fn from_str(s: &str) -> Result { // Parse the auto first @@ -108,7 +110,7 @@ impl FromStr for VersionFix { return Ok(Self(None)); } // Otherwise, delegate to the version parser - Ok(Self(Some(Version::from_str(s)?))) + Ok(Self(Some(ConcreteFunctionVersion::from_str(s)?))) } } @@ -278,7 +280,7 @@ pub struct StartOpts { pub compose_verbose: bool, /// The Brane version to start. - pub version: Version, + pub version: BraneVersion, /// The image base directory, which is used to easily switch between using `./target/release` and `./target/debug`. pub image_dir: PathBuf, /// Use local .tars for auxillary images instead of DockerHub ones. diff --git a/brane-ctl/src/upgrade.rs b/brane-ctl/src/upgrade.rs index ca68f77b..aecba69b 100644 --- a/brane-ctl/src/upgrade.rs +++ b/brane-ctl/src/upgrade.rs @@ -27,7 +27,7 @@ use console::style; use log::{debug, info, warn}; use serde::Serialize; use specifications::address::Host; -use specifications::version::Version; +use specifications::version::BraneVersion; use crate::old_configs::v1_0_0; use crate::spec::VersionFix; @@ -73,7 +73,7 @@ pub enum Error { FileMetadataRead { path: PathBuf, err: std::io::Error }, /// Failed to convert between the infos - Convert { what: &'static str, version: Version, err: Box }, + Convert { what: &'static str, version: BraneVersion, err: Box }, /// Failed to serialize the new info. Serialize { what: &'static str, err: serde_yaml::Error }, /// Failed to create a new file. @@ -137,7 +137,7 @@ impl error::Error for Error { fn upgrade( what: &'static str, path: impl Into, - versions: Vec<(Version, VersionParser)>, + versions: Vec<(BraneVersion, VersionParser)>, dry_run: bool, overwrite: bool, ) -> Result<(), Error> { @@ -326,7 +326,7 @@ pub fn node(path: impl Into, dry_run: bool, overwrite: bool, version: V info!("Upgrading node.yml files in '{}'...", path.display()); // Query for missing information first - let hostname: String = if version.0.is_none() || version.0.unwrap() <= Version::new(1, 0, 0) { + let hostname: String = if version.0.is_none() || version.0.as_ref().unwrap() <= &semver::Version::new(1, 0, 0) { match input( "hostname", "Enter the hostname for this node (used to supplement v1.0.0 and older configs)", @@ -345,8 +345,8 @@ pub fn node(path: impl Into, dry_run: bool, overwrite: bool, version: V }; // Construct the list of versions - let mut versions: Vec<(Version, VersionParser)> = vec![( - Version::new(1, 0, 0), + let mut versions: Vec<(BraneVersion, VersionParser)> = vec![( + BraneVersion(semver::Version::new(1, 0, 0)), Box::new(|raw: &str| -> Option> { // Attempt to read it with the file let cfg: v1_0_0::NodeConfig = match serde_yaml::from_str(raw) { @@ -493,7 +493,7 @@ pub fn node(path: impl Into, dry_run: bool, overwrite: bool, version: V )]; // Limit the version to only the given one if applicable if let Some(version) = version.0 { - versions.retain(|(v, _)| v == &version); + versions.retain(|(v, _)| v.0 == version); } // Call the function that does the heavy lifting diff --git a/brane-dsl/src/parser/ast.rs b/brane-dsl/src/parser/ast.rs index fea353a6..db637de7 100644 --- a/brane-dsl/src/parser/ast.rs +++ b/brane-dsl/src/parser/ast.rs @@ -19,7 +19,7 @@ use std::rc::Rc; use std::str::FromStr as _; use enum_debug::EnumDebug; -use specifications::version::{ParseError, Version}; +use specifications::version::{AliasedFunctionVersion, SemverError}; use crate::data_type::DataType; use crate::location::AllowedLocations; @@ -1563,10 +1563,10 @@ impl Literal { /// # Panics /// This function panics if the Literal is not a Semver. #[inline] - pub fn as_version(&self) -> Result { + pub fn as_version(&self) -> Result { use Literal::*; if let Semver { value, .. } = self { - Version::from_str(value) + AliasedFunctionVersion::from_str(value) } else { panic!("Attempted to get Literal of type '{}' as 'Semver'", self.data_type()); } diff --git a/brane-dsl/src/symbol_table.rs b/brane-dsl/src/symbol_table.rs index da9a7d9e..febe58cd 100644 --- a/brane-dsl/src/symbol_table.rs +++ b/brane-dsl/src/symbol_table.rs @@ -20,7 +20,7 @@ use std::mem; use std::rc::Rc; use specifications::package::Capability; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; use crate::data_type::{ClassSignature, DataType, FunctionSignature}; pub use crate::errors::SymbolTableError as Error; @@ -103,7 +103,7 @@ pub struct FunctionEntry { /// If set to non-zero, then this function is imported from a package with the given name. pub package_name: Option, /// If set to non-zero, then this function is imported from a package with the given version. - pub package_version: Option, + pub package_version: Option, /// If set to non-zero, then this function is a method in the class with the given name. pub class_name: Option, @@ -207,7 +207,7 @@ impl FunctionEntry { name: S1, signature: FunctionSignature, package: S2, - package_version: Version, + package_version: ConcreteFunctionVersion, arg_names: Vec, requirements: HashSet, range: TextRange, @@ -277,7 +277,7 @@ pub struct ClassEntry { /// If populated, then this Class was defined in a package with the given name. pub package_name: Option, /// If set to non-zero, then this function is imported from a package with the given version. - pub package_version: Option, + pub package_version: Option, /// The index in the workflow buffer of this class. pub index: usize, @@ -336,7 +336,7 @@ impl ClassEntry { signature: ClassSignature, symbol_table: Rc>, package: S, - package_version: Version, + package_version: ConcreteFunctionVersion, range: TextRange, ) -> Self { Self { signature, symbol_table, package_name: Some(package.into()), package_version: Some(package_version), index: usize::MAX, range } diff --git a/brane-exe/src/errors.rs b/brane-exe/src/errors.rs index a76c31a7..c529aff4 100644 --- a/brane-exe/src/errors.rs +++ b/brane-exe/src/errors.rs @@ -19,7 +19,7 @@ use console::style; use enum_debug::EnumDebug as _; use specifications::data::DataName; use specifications::pc::ProgramCounter; -use specifications::version::Version; +use specifications::version::AliasedFunctionVersion; use specifications::wir::data_type::DataType; use specifications::wir::func_id::FunctionId; use specifications::wir::merge_strategy::MergeStrategy; @@ -265,8 +265,8 @@ pub enum VmError { #[error("Encountered unknown result '{name}'")] UnknownResult { pc: ProgramCounter, name: String }, /// The given package was not known. - #[error("Unknown package with name '{}'{}", name, if !version.is_latest() { format!(" and version {version}") } else { String::new() })] - UnknownPackage { pc: ProgramCounter, name: String, version: Version }, + #[error("Unknown package with name '{}'{}", name, if let AliasedFunctionVersion::Version(semver) = version { format!(" and version {semver}") } else { String::new() })] + UnknownPackage { pc: ProgramCounter, name: String, version: AliasedFunctionVersion }, /// Failed to serialize the given argument list. #[error("Could not serialize task arguments")] ArgumentsSerializeError { pc: ProgramCounter, source: serde_json::Error }, diff --git a/brane-exe/src/spec.rs b/brane-exe/src/spec.rs index ea6e92f4..555a16ba 100644 --- a/brane-exe/src/spec.rs +++ b/brane-exe/src/spec.rs @@ -22,7 +22,7 @@ use specifications::data::{AccessKind, DataName, PreprocessKind}; use specifications::package::Capability; use specifications::pc::ProgramCounter; use specifications::profiling::ProfileScopeHandle; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; use specifications::wir::SymTable; use specifications::wir::locations::Location; @@ -253,7 +253,7 @@ pub struct TaskInfo<'a> { /// The package name of the task to execute. pub package_name: &'a str, /// The package version of the task to execute. - pub package_version: &'a Version, + pub package_version: &'a ConcreteFunctionVersion, /// The requirements that the task has. pub requirements: &'a HashSet, diff --git a/brane-exe/src/thread.rs b/brane-exe/src/thread.rs index 437276df..643a0465 100644 --- a/brane-exe/src/thread.rs +++ b/brane-exe/src/thread.rs @@ -1301,7 +1301,7 @@ impl Thread { name: &function.name, package_name: package, - package_version: version, + package_version: &version.clone(), requirements, args, @@ -1943,7 +1943,7 @@ impl Thread { /// /// # Arguments /// - `prof`: A ProfileScopeHandleOwned that is used to provide more details about the execution times of a workflow execution. Note that this is _not_ user-relevant, only debug/framework-relevant. - /// + /// /// The reason it is owned is due to the boxed return future. It's your responsibility to keep the parent into scope after the future returns; if you don't any collected profile results will likely not be printed. /// /// # Returns @@ -1983,7 +1983,7 @@ impl Thread { /// /// # Arguments /// - `prof`: A ProfileScopeHandleOwned that is used to provide more details about the execution times of a workflow execution. Note that this is _not_ user-relevant, only debug/framework-relevant. - /// + /// /// The reason it is owned is due to the boxed return future. It's your responsibility to keep the parent into scope after the future returns; if you don't any collected profile results will likely not be printed. /// /// # Returns diff --git a/brane-job/src/worker.rs b/brane-job/src/worker.rs index 3158c716..df9ffbc1 100644 --- a/brane-job/src/worker.rs +++ b/brane-job/src/worker.rs @@ -57,7 +57,7 @@ use specifications::package::{Capability, PackageIndex, PackageInfo, PackageKind use specifications::pc::ProgramCounter; use specifications::profiling::{ProfileReport, ProfileScopeHandle}; use specifications::registering::DownloadAssetRequest; -use specifications::version::Version; +use specifications::version::AliasedFunctionVersion; use specifications::wir::func_id::FunctionId; use specifications::wir::locations::Location; use specifications::wir::{ComputeTaskDef, TaskDef, Workflow}; @@ -213,7 +213,7 @@ pub struct TaskInfo { /// The name of the task's parent package. pub package_name: String, /// The version of the task's parent package. - pub package_version: Version, + pub package_version: AliasedFunctionVersion, /// The kind of the task to execute. pub kind: Option, /// The image name of the package where the task is from. Note: won't be populated until later. @@ -250,7 +250,7 @@ impl TaskInfo { name: impl Into, pc: ProgramCounter, package_name: impl Into, - package_version: impl Into, + package_version: impl Into, input: HashMap, result: Option, args: HashMap, @@ -1236,7 +1236,7 @@ async fn execute_task( }; // Get the info - let info: &PackageInfo = match index.get(&tinfo.package_name, Some(&tinfo.package_version)) { + let info: &PackageInfo = match index.get(&tinfo.package_name, &tinfo.package_version) { Some(info) => info, None => { return err!(tx, ExecuteError::UnknownPackage { name: tinfo.package_name.clone(), version: tinfo.package_version }); @@ -1246,7 +1246,7 @@ async fn execute_task( // Deduce the image name from that tinfo.kind = Some(info.kind); - tinfo.image = Some(Image::new(&tinfo.package_name, Some(tinfo.package_version), info.digest.clone())); + tinfo.image = Some(Image::new(&tinfo.package_name, Some(&tinfo.package_version), info.digest.clone())); // Now load the credentials file to get things going let disk = prof.time("File loading"); @@ -1919,7 +1919,7 @@ impl JobService for WorkerServer { call_pc.edge_idx as usize, ), task.package.clone(), - task.version, + task.version.clone(), input, result, args, diff --git a/brane-shr/Cargo.toml b/brane-shr/Cargo.toml index c410930e..5f2eeef8 100644 --- a/brane-shr/Cargo.toml +++ b/brane-shr/Cargo.toml @@ -18,6 +18,7 @@ sha2 = "0.10.6" tokio-stream = "0.1.16" astral-tokio-tar = "0.5.0" url = "2.5.0" +tempfile = "3.10.1" # Workspace dependencies enum-debug.workspace = true diff --git a/brane-shr/src/fs.rs b/brane-shr/src/fs.rs index 3908cbaa..79399808 100644 --- a/brane-shr/src/fs.rs +++ b/brane-shr/src/fs.rs @@ -27,7 +27,7 @@ use indicatif::{ProgressBar, ProgressStyle}; use log::{debug, warn}; use reqwest::{Client, Request, Response, StatusCode, Url}; use sha2::{Digest as _, Sha256}; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; use tokio::fs as tfs; use tokio::io::{self as tio, AsyncWriteExt}; use tokio_stream::StreamExt; @@ -914,9 +914,8 @@ impl FileLock { /// /// # Returns /// A new instance of the FileLock that acts as a guard of the lock. As long as it's in scope, the exclusive lock will be held. - pub fn lock(name: impl AsRef, version: impl AsRef, path: impl Into) -> Result { + pub fn lock(name: impl AsRef, version: &ConcreteFunctionVersion, path: impl Into) -> Result { let name: &str = name.as_ref(); - let version: &Version = version.as_ref(); let path: PathBuf = path.into(); // Attempt to get the file handle @@ -963,6 +962,20 @@ impl Drop for FileLock { } } +/// Small wrapper +pub enum MaybeTempPath { + Fixed(PathBuf), + Temp(tempfile::TempPath), +} + +impl AsRef for MaybeTempPath { + fn as_ref(&self) -> &Path { + match self { + MaybeTempPath::Fixed(path) => path, + MaybeTempPath::Temp(path) => path, + } + } +} /// Changes the permissions of the given file to the given triplet. diff --git a/brane-tsk/src/api.rs b/brane-tsk/src/api.rs index 601a9206..1a5d34db 100644 --- a/brane-tsk/src/api.rs +++ b/brane-tsk/src/api.rs @@ -22,7 +22,7 @@ use reqwest::Client; use specifications::common::{Function, Type}; use specifications::data::{DataIndex, DataInfo}; use specifications::package::{PackageIndex, PackageInfo, PackageKind}; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; use uuid::Uuid; pub use crate::errors::ApiError as Error; @@ -88,7 +88,7 @@ pub async fn get_package_index(endpoint: impl AsRef) -> Result Result (t.0.clone(), ClassDef { name: t.1.name.clone(), package: Some(package.name.clone()), - version: Some(package.version), + version: Some(package.version.clone()), props: t.1.properties.iter().map(|p| VarDef { name: p.name.clone(), data_type: DataType::from(&p.data_type) }).collect(), methods: vec![], @@ -121,7 +121,7 @@ pub fn prompt_for_input(data_index: &DataIndex, package: &PackageInfo) -> Result // We don't care for methods anyway methods: vec![], }) { - return Err(Error::PackageDefinesBuiltin { name: package.name.clone(), version: package.version, builtin: old.name }); + return Err(Error::PackageDefinesBuiltin { name: package.name.clone(), version: package.version.clone(), builtin: old.name }); } } @@ -307,7 +307,7 @@ fn prompt_for_param( let def: &ClassDef = match types.get(&c_name) { Some(def) => def, None => { - return Err(Error::UndefinedClass { name: package.name.clone(), version: package.version, class: c_name }); + return Err(Error::UndefinedClass { name: package.name.clone(), version: package.version.clone(), class: c_name }); }, }; diff --git a/brane-tsk/src/local.rs b/brane-tsk/src/local.rs index c308b370..774bc374 100644 --- a/brane-tsk/src/local.rs +++ b/brane-tsk/src/local.rs @@ -19,7 +19,7 @@ use std::str::FromStr; use serde_json::json; use specifications::data::{DataIndex, DataInfo}; use specifications::package::{PackageIndex, PackageInfo}; -use specifications::version::Version; +use specifications::version::ConcreteFunctionVersion; pub use crate::errors::LocalError as Error; @@ -33,12 +33,12 @@ pub use crate::errors::LocalError as Error; /// /// # Returns /// The list of Versions found in the given package directory, or a PackageError if we couldn't. -pub fn get_package_versions(package_name: &str, package_dir: &Path) -> Result, Error> { +pub fn get_package_versions(package_name: &str, package_dir: &Path) -> Result, Error> { // Get the list of available versions let version_dirs = fs::read_dir(package_dir).map_err(|source| Error::PackageDirReadError { path: package_dir.to_path_buf(), source })?; // Convert the list of strings into a version - let mut versions: Vec = Vec::new(); + let mut versions: Vec = Vec::new(); for dir in version_dirs { let dir_path = match dir { Ok(dir) => dir.path(), @@ -58,7 +58,7 @@ pub fn get_package_versions(package_name: &str, package_dir: &Path) -> Result, - pub version: Version, + pub version: AliasedFunctionVersion, } /* TIM */ diff --git a/specifications/src/container.rs b/specifications/src/container.rs index 480acb79..19fc6322 100644 --- a/specifications/src/container.rs +++ b/specifications/src/container.rs @@ -10,7 +10,7 @@ use serde_with::skip_serializing_none; use crate::common::{CallPattern, Parameter, Type}; use crate::package::{Capability, PackageKind}; -use crate::version::Version; +use crate::version::ConcreteFunctionVersion; /***** CUSTOM TYPES *****/ @@ -433,7 +433,7 @@ pub struct ContainerInfo { /// The name/programming ID of this package. pub name: String, /// The version of this package. - pub version: Version, + pub version: ConcreteFunctionVersion, /// The kind of this package. pub kind: PackageKind, /// The list of owners of this package. diff --git a/specifications/src/package.rs b/specifications/src/package.rs index ba66089a..71e972fe 100644 --- a/specifications/src/package.rs +++ b/specifications/src/package.rs @@ -20,7 +20,6 @@ use std::str::FromStr; use anyhow::Result; use chrono::{DateTime, Utc}; use enum_debug::EnumDebug; -// use semver::Version; use serde::{Deserialize, Serialize}; use serde_json::Value as JValue; use serde_with::skip_serializing_none; @@ -29,7 +28,7 @@ use uuid::Uuid; use crate::common::{Function, Type}; use crate::container::ContainerInfo; -use crate::version::Version; +use crate::version::{AliasedFunctionVersion, ConcreteFunctionVersion}; /***** CUSTOM TYPES *****/ @@ -248,7 +247,7 @@ pub struct PackageInfo { /// The name/programming ID of this package. pub name: String, /// The version of this package. - pub version: Version, + pub version: ConcreteFunctionVersion, /// The kind of this package. pub kind: PackageKind, /// The list of owners of this package. @@ -280,7 +279,7 @@ impl PackageInfo { #[allow(clippy::too_many_arguments)] pub fn new( name: String, - version: Version, + version: ConcreteFunctionVersion, kind: PackageKind, owners: Vec, description: String, @@ -426,22 +425,13 @@ impl From<&ContainerInfo> for PackageInfo { // Put it and other clones in the new instance PackageInfo::new( container.name.clone(), - container.version, + container.version.clone(), container.kind, - match container.owners.as_ref() { - Some(owners) => owners.clone(), - None => Vec::new(), - }, - match container.description.as_ref() { - Some(description) => description.clone(), - None => String::new(), - }, + container.owners.clone().unwrap_or_default(), + container.description.clone().unwrap_or_default(), container.entrypoint.kind == *"service", functions, - match container.types.as_ref() { - Some(types) => types.clone(), - None => Map::new(), - }, + container.types.clone().unwrap_or_default(), ) } } @@ -454,7 +444,7 @@ pub struct PackageIndex { /// The list of packages stored in the index. pub packages: Map, /// Cache of the standard 'latest' packages so we won't have to search every time. - pub latest: Map<(Version, String)>, + pub latest: Map<(ConcreteFunctionVersion, String)>, } impl PackageIndex { @@ -468,19 +458,19 @@ impl PackageIndex { /// * `packages`: The map of packages to base this index on. Each key should be `-` (i.e., every package version is a separate entry). pub fn new(packages: Map) -> Self { // Compute the latest versions for each package - let mut latest: Map<(Version, String)> = Map::with_capacity(packages.len()); + let mut latest: Map<(ConcreteFunctionVersion, String)> = Map::with_capacity(packages.len()); for (key, package) in &packages { // Check if the package name has already been added if !latest.contains_key(&package.name) { - latest.insert(package.name.clone(), (package.version, key.clone())); + latest.insert(package.name.clone(), (package.version.clone(), key.clone())); continue; } // Check if the existing version is later or not - let latest_package: &mut (Version, String) = latest.get_mut(&package.name).unwrap(); + let latest_package: &mut (ConcreteFunctionVersion, String) = latest.get_mut(&package.name).unwrap(); if package.version >= latest_package.0 { // It is; update the version to point to the latest version of this package - latest_package.0 = package.version; + latest_package.0 = package.version.clone(); latest_package.1.clone_from(key); } } @@ -529,18 +519,13 @@ impl PackageIndex { /// The new `PackageIndex` if it all went fine, or a [`PackageIndexError`] if it didn't. pub async fn from_url(url: &str) -> Result { // try to get the file - let json = match reqwest::get(url).await { - Ok(response) => { - if response.status() == reqwest::StatusCode::OK { - // We have the request; now try to get it as json - response.json().await.map_err(|source| PackageIndexError::IllegalJsonFile { url: url.to_string(), source })? - } else { - return Err(PackageIndexError::ResponseNot200 { url: url.to_string(), status: response.status() }); - } - }, - Err(reason) => { - return Err(PackageIndexError::RequestFailed { url: url.to_string(), source: reason }); - }, + let response = reqwest::get(url).await.map_err(|reason| PackageIndexError::RequestFailed { url: url.to_string(), source: reason })?; + + let json = if response.status() == reqwest::StatusCode::OK { + // We have the request; now try to get it as json + response.json().await.map_err(|source| PackageIndexError::IllegalJsonFile { url: url.to_string(), source })? + } else { + return Err(PackageIndexError::ResponseNot200 { url: url.to_string(), status: response.status() }); }; // Done; pass the rest to the from_value() function @@ -594,27 +579,11 @@ impl PackageIndex { /// /// **Returns** /// An (immuteable) reference to the package if it exists, or else None. - pub fn get(&self, name: &str, version: Option<&Version>) -> Option<&PackageInfo> { + pub fn get(&self, name: &str, version: &AliasedFunctionVersion) -> Option<&PackageInfo> { // Resolve the package version let version = match version { - Some(version) => { - if version.is_latest() { - match self.get_latest_version(name) { - Some(version) => version, - None => { - return None; - }, - } - } else { - version - } - }, - None => match self.get_latest_version(name) { - Some(version) => version, - None => { - return None; - }, - }, + AliasedFunctionVersion::Latest => self.get_latest_version(name)?, + AliasedFunctionVersion::Version(version) => version, }; // Try to return the package info matching to this name/version pair @@ -629,5 +598,5 @@ impl PackageIndex { /// **Returns** /// An (immuteable) reference to the version if this package if known, or else None. #[inline] - fn get_latest_version(&self, name: &str) -> Option<&Version> { self.latest.get(name).map(|(version, _)| version) } + fn get_latest_version(&self, name: &str) -> Option<&ConcreteFunctionVersion> { self.latest.get(name).map(|(version, _)| version) } } diff --git a/specifications/src/version.rs b/specifications/src/version.rs index ef29225e..306e8819 100644 --- a/specifications/src/version.rs +++ b/specifications/src/version.rs @@ -14,538 +14,114 @@ // use std::cmp::Ordering; -use std::fmt::{Display, Formatter, Result as FResult}; +use std::fmt::Display; use std::str::FromStr; -use serde::de::{self, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; +pub type ConcreteFunctionVersion = semver::Version; +pub use semver::Error as SemverError; -/***** UNIT TESTS *****/ -#[cfg(test)] -mod tests { - use serde_test::{Token, assert_de_tokens, assert_de_tokens_error, assert_ser_tokens}; - - use super::*; - - - /// A test string that is used for serde, who requires 'static references - const ACCIDENTAL_LATEST_STRING: &str = const_format::formatcp!("{}.{}.{}", u64::MAX, u64::MAX, u64::MAX); - - - - #[test] - fn test_eq() { - // Test if versions equal each other - assert!(Version::new(42, 21, 10) == Version::new(42, 21, 10)); - assert!(Version::new(42, 21, 10) != Version::new(43, 21, 10)); - - // Test the ordering - assert!(Version::new(42, 21, 10) > Version::new(42, 21, 9)); - assert!(Version::new(42, 21, 10) > Version::new(42, 20, 10)); - assert!(Version::new(42, 21, 10) > Version::new(41, 21, 10)); - assert!(Version::new(42, 21, 10) < Version::new(42, 21, 11)); - assert!(Version::new(42, 21, 10) < Version::new(42, 22, 10)); - assert!(Version::new(42, 21, 10) < Version::new(43, 21, 10)); - } - - #[test] - fn test_parse() { - // Test if it can parse string versions - assert_eq!(Version::from_str("42.21.10"), Ok(Version::new(42, 21, 10))); - assert_eq!(Version::from_str("42.21"), Ok(Version::new(42, 21, 0))); - assert_eq!(Version::from_str("42"), Ok(Version::new(42, 0, 0))); - - // Test if it can parse latest - assert_eq!(Version::from_str("latest"), Ok(Version::latest())); - - // Test if it fails properly too - assert_eq!(Version::from_str(&format!("{}.{}.{}", u64::MAX, u64::MAX, u64::MAX)), Err(ParseError::AccidentalLatest)); - assert_eq!(Version::from_str("a"), Err(ParseError::MajorParseError { raw: String::from("a"), source: u64::from_str("a").unwrap_err() })); - assert_eq!(Version::from_str("42.a"), Err(ParseError::MinorParseError { raw: String::from("a"), source: u64::from_str("a").unwrap_err() })); - assert_eq!( - Version::from_str("42.21.a"), - Err(ParseError::PatchParseError { raw: String::from("a"), source: u64::from_str("a").unwrap_err() }) - ); - assert_eq!(Version::from_str("a.b.c"), Err(ParseError::MajorParseError { raw: String::from("a"), source: u64::from_str("a").unwrap_err() })); - assert_eq!(Version::from_str("42.b.c"), Err(ParseError::MinorParseError { raw: String::from("b"), source: u64::from_str("b").unwrap_err() })); - } - - #[test] - fn test_resolve() { - // Create a 'latest' version - let mut latest = Version::latest(); - - // Resolve it with a list - let versions = - vec![Version::new(21, 21, 10), Version::new(42, 20, 10), Version::new(42, 21, 10), Version::new(42, 19, 10), Version::new(0, 0, 0)]; - assert!(latest.resolve_latest(versions.clone()).is_ok()); - assert_eq!(latest, Version::new(42, 21, 10)); - - // Next, check if the errors work - let mut latest = Version::new(42, 21, 10); - assert_eq!(latest.resolve_latest(versions), Err(ResolveError::AlreadyResolved { version: Version::new(42, 21, 10) })); - - let mut latest = Version::latest(); - let versions = vec![Version::new(21, 21, 10), Version::latest(), Version::new(42, 21, 10)]; - assert_eq!(latest.resolve_latest(versions), Err(ResolveError::NotResolved)); - - let mut latest = Version::latest(); - let versions = vec![]; - assert_eq!(latest.resolve_latest(versions), Err(ResolveError::NoVersions)); - } - - - - #[test] - fn test_semver() { - // Make sure the from (consuming) makes sense - let semversion = semver::Version::new(42, 21, 10); - let version = Version::from(semversion.clone()); - assert_eq!(semversion.major, version.major); - assert_eq!(semversion.minor, version.minor); - assert_eq!(semversion.patch, version.patch); - - // Make sure the from (reference) makes sense - let semversion = semver::Version::new(10, 21, 42); - let version = Version::from(&semversion); - assert_eq!(semversion.major, version.major); - assert_eq!(semversion.minor, version.minor); - assert_eq!(semversion.patch, version.patch); - - // Check the eq - assert_eq!(Version::new(42, 21, 10), semver::Version::new(42, 21, 10)); - assert_ne!(Version::latest(), semver::Version::new(u64::MAX, u64::MAX, u64::MAX)); - - // Check the ord - assert!(Version::new(42, 21, 10) > semver::Version::new(42, 21, 9)); - assert!(Version::new(42, 21, 10) > semver::Version::new(42, 20, 10)); - assert!(Version::new(42, 21, 10) > semver::Version::new(41, 21, 10)); - assert!(Version::new(42, 21, 10) < semver::Version::new(42, 21, 11)); - assert!(Version::new(42, 21, 10) < semver::Version::new(42, 22, 10)); - assert!(Version::new(42, 21, 10) < semver::Version::new(43, 21, 10)); - } - - - - #[test] - fn test_serde_serialize() { - // Try to convert some versions to serde tokens - assert_ser_tokens(&Version::new(42, 21, 10), &[Token::Str("42.21.10")]); - assert_ser_tokens(&Version::new(42, 0, 10), &[Token::Str("42.0.10")]); - assert_ser_tokens(&Version::latest(), &[Token::Str("latest")]); - } - - #[test] - fn test_serde_deserialize() { - // Try to convert some versions to serde tokens - assert_de_tokens(&Version::new(42, 21, 10), &[Token::Str("42.21.10")]); - assert_de_tokens(&Version::new(42, 0, 10), &[Token::Str("42.0.10")]); - assert_de_tokens(&Version::latest(), &[Token::Str("latest")]); - - // Check for the same errors as test_parse() - assert_de_tokens_error::(&[Token::Str(ACCIDENTAL_LATEST_STRING)], &format!("{}", ParseError::AccidentalLatest)); - assert_de_tokens_error::( - &[Token::Str("a")], - &format!("{}", ParseError::MajorParseError { raw: String::from("a"), source: u64::from_str("a").unwrap_err() }), - ); - assert_de_tokens_error::( - &[Token::Str("42.a")], - &format!("{}", ParseError::MinorParseError { raw: String::from("a"), source: u64::from_str("a").unwrap_err() }), - ); - assert_de_tokens_error::( - &[Token::Str("42.21.a")], - &format!("{}", ParseError::PatchParseError { raw: String::from("a"), source: u64::from_str("a").unwrap_err() }), - ); - assert_de_tokens_error::( - &[Token::Str("a.b.c")], - &format!("{}", ParseError::MajorParseError { raw: String::from("a"), source: u64::from_str("a").unwrap_err() }), - ); - assert_de_tokens_error::( - &[Token::Str("42.b.c")], - &format!("{}", ParseError::MinorParseError { raw: String::from("b"), source: u64::from_str("b").unwrap_err() }), - ); - } -} - - - - - -/***** ERRORS *****/ -/// Collects errors that relate to the Version. -#[derive(Debug, Eq, PartialEq, thiserror::Error)] -pub enum ResolveError { - /// Could not resolve the version as it's already resolved. - #[error("Cannot resolve already resolved version '{version}'")] - AlreadyResolved { version: Version }, - /// One of the versions we use to resolve this version is not resolved - #[error("Cannot resolve version with unresolved versions")] - NotResolved, - /// Could not resolve this version, as no versions are given - #[error("Cannot resolve version without any versions given")] - NoVersions, -} - -/// Collects errors that relate to the Version. -#[derive(Debug, Eq, PartialEq, thiserror::Error)] -pub enum ParseError { - /// We accidentally created a 'latest' version - #[error("A version with all numbers to {} (64-bit, unsigned integer max) cannot be created; use 'latest' instead", u64::MAX)] - AccidentalLatest, - /// Could not parse the major version number - #[error("Could not parse major version number '{raw}'")] - MajorParseError { raw: String, source: std::num::ParseIntError }, - /// Could not parse the minor version number - #[error("Could not parse minor version number '{raw}'")] - MinorParseError { raw: String, source: std::num::ParseIntError }, - /// Could not parse the patch version number - #[error("Could not parse patch version number '{raw}'")] - PatchParseError { raw: String, source: std::num::ParseIntError }, - - /// Got a NAME:VERSION pair with too many colons - #[error("Given 'NAME[:VERSION]' pair '{raw}' has too many colons (got {got}, expected at most 1)")] - TooManyColons { raw: String, got: usize }, - /// Could not parse the Version in a given NAME:VERSION pair. - #[error("Could not parse version '{raw_version}' in '{raw}'")] - IllegalVersion { raw: String, raw_version: String, source: Box }, -} - -/***** HELPER STRUCTS *****/ -/// Implements a Visitor for the Version. -struct VersionVisitor; - -impl Visitor<'_> for VersionVisitor { - type Value = Version; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FResult { formatter.write_str("a semanting version") } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - // Parse the value with the Version parser - Version::from_str(value).map_err(|err| E::custom(format!("{err}"))) - } -} - - - - - -/***** VERSION *****/ -/// Implements the Version, which is used to keep track of package versions. -#[derive(Clone, Copy, Debug, Eq)] -pub struct Version { - /// The major version number. If all three are set to `u64::MAX`, is interpreted as an unresolved 'latest' version number. - pub major: u64, - /// The minor version number. If all three are set to `u64::MAX`, is interpreted as an unresolved 'latest' version number. - pub minor: u64, - /// The patch version number. If all three are set to `u64::MAX`, is interpreted as an unresolved 'latest' version number. - pub patch: u64, +/// Version information for a Brane DSL function. +/// This version can be specified along semver, or provided as "latest". +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AliasedFunctionVersion { + Latest, + Version(semver::Version), } -impl Version { - /// Constructor for the Version. - /// Note that this function panics if you try to create a 'latest' function this way; use [`Version::latest()`] instead. - /// - /// **Arguments** - /// * `major`: The major version number. - /// * `minor`: The minor version number. - /// * `patch`: The patch version number. - pub const fn new(major: u64, minor: u64, patch: u64) -> Self { - // Create the version - let result = Self { major, minor, patch }; - - // If it's latest, panic; otherwise, return - if result.is_latest() { - panic!( - "A version with all numbers set to 9,223,372,036,854,775,807 (64-bit, unsigned integer max) cannot be created; use 'latest' instead" - ); - } - result - } - - /// Constructor for the Version that sets it to an (unresolved) 'latest' version. - #[inline] - pub const fn latest() -> Self { Self { major: u64::MAX, minor: u64::MAX, patch: u64::MAX } } - - /// Special factory method that creates a package name and a version from a `NAME[:VERSION]` pair. - /// - /// If the `VERSION` is omitted, returns `Version::latest()`. - /// - /// # Arguments - /// - `package`: The package `NAME[:VERSION]` pair to parse. - /// - /// # Errors - /// This function may error if parsing failed, somehow. - pub fn from_package_pair(package: &str) -> Result<(String, Self), ParseError> { - // Get the number of colons in the string - let colons: usize = package.matches(':').count(); - - // Switch on version present or not - if colons == 0 { - // Simply return the name with the latest version - Ok((package.into(), Self::latest())) - } else if colons == 1 { - // Split on the colon - let colon_pos = package.find(':').unwrap(); - let name: &str = &package[..colon_pos]; - let version: &str = &package[colon_pos + 1..]; - - // Attempt to parse the Version - let version: Self = Self::from_str(version).map_err(|source| ParseError::IllegalVersion { - raw: package.into(), - raw_version: version.into(), - source: Box::new(source), - })?; - - // Return them as a pair - Ok((name.to_string(), version)) - } else { - Err(ParseError::TooManyColons { raw: package.into(), got: colons }) +impl Display for AliasedFunctionVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Latest => write!(f, "latest"), + Self::Version(s) => write!(f, "{s}"), } } - - /// Resolves this version in case it's a 'latest' version. - /// - /// **Generic types** - /// * `I`: The type of the iterator passed to this function. - /// - /// **Arguments** - /// * `iter`: An iterator over resolved version numbers. - /// - /// **Returns** - /// Nothing on success (except that this version now is equal to the latest version in the bunch), or a `VersionError` otherwise. - pub fn resolve_latest>(&mut self, iter: I) -> Result<(), ResolveError> { - // Crash if we're already resolved - if !self.is_latest() { - return Err(ResolveError::AlreadyResolved { version: *self }); - } - - // Go through the iterator - let mut last_version: Option = None; - for version in iter { - // If this one isn't resolved, error too - if version.is_latest() { - return Err(ResolveError::NotResolved); - } - - // Then, check if we saw a version before - if let Some(lversion) = &last_version { - // Update if this version is newer - if &version > lversion { - last_version = Some(version); - } - } else { - // Simply set, as this is the first one - last_version = Some(version); - } - } - - // If we found any, set it; otherwise, return failure - if let Some(version) = last_version { - *self = version; - Ok(()) - } else { - Err(ResolveError::NoVersions) - } - } - - /// Returns whether or not this Version represents a 'latest' version. - #[inline] - pub const fn is_latest(&self) -> bool { self.major == u64::MAX && self.minor == u64::MAX && self.patch == u64::MAX } } -impl Default for Version { - /// Default constructor for the Version, which initializes it to 0.0.0. - #[inline] - fn default() -> Self { Self::new(0, 0, 0) } -} +impl FromStr for AliasedFunctionVersion { + type Err = semver::Error; -impl PartialEq for Version { - #[inline] - fn eq(&self, other: &Self) -> bool { self.major == other.major && self.minor == other.minor && self.patch == other.patch } -} - -impl Ord for Version { - fn cmp(&self, other: &Self) -> Ordering { - // Compare the major number - let order = self.major.cmp(&other.major); - if order.is_ne() { - return order; - } - - // Compare the minor number - let order = self.minor.cmp(&other.minor); - if order.is_ne() { - return order; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "latest" => Ok(Self::Latest), + v => Ok(Self::Version(FromStr::from_str(v)?)), } - - // Compare the patch - self.patch.cmp(&other.patch) } } -impl PartialOrd for Version { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } +impl From<&AliasedFunctionVersion> for String { + fn from(value: &AliasedFunctionVersion) -> Self { format!("{value}") } } -impl FromStr for Version { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - // If the (lowercase) string is 'latest', use that - if s.to_lowercase() == "latest" { - return Ok(Self::latest()); - } - - // Otherwise, see if we can split the string into multiple slices - // Compute the possible dot posses first - let dot1 = s.find('.'); - let dot2 = match &dot1 { - Some(pos1) => s[*pos1 + 1..].find('.').map(|pos2| *pos1 + 1 + pos2), - None => None, - }; - - // Use those positions to populate the string parts for each version number - let smajor: &str = match &dot1 { - Some(pos1) => &s[..*pos1], - None => s, - }; - let sminor: &str = match dot1 { - Some(pos1) => match &dot2 { - Some(pos2) => &s[pos1 + 1..*pos2], - None => &s[pos1 + 1..], - }, - None => "", - }; - let spatch: &str = match dot2 { - Some(pos2) => &s[pos2 + 1..], - None => "", - }; - - // If the version starts with a 'v', then skip that one (i.e., that's allowed) - let smajor = if !smajor.is_empty() && smajor.starts_with('v') { &smajor[1..] } else { smajor }; - // Try to parse each part - let major = u64::from_str(smajor).map_err(|source| ParseError::MajorParseError { raw: smajor.to_string(), source })?; - let minor = if !sminor.is_empty() { - u64::from_str(sminor).map_err(|source| ParseError::MinorParseError { raw: sminor.to_string(), source })? - } else { - // Otherwise, use the standard minor value - 0 - }; - let patch = if !spatch.is_empty() { - u64::from_str(spatch).map_err(|source| ParseError::PatchParseError { raw: spatch.to_string(), source })? - } else { - // Otherwise, use the standard patch value - 0 - }; - - // Put them together in a Version - let result = Self { major, minor, patch }; - - // If this version is latest, then error - if result.is_latest() { - return Err(ParseError::AccidentalLatest); +impl PartialEq for AliasedFunctionVersion { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Version(l0), Self::Version(r0)) => l0 == r0, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), } - Ok(result) - } -} - -impl Display for Version { - fn fmt(&self, f: &mut Formatter<'_>) -> FResult { - if self.is_latest() { write!(f, "latest") } else { write!(f, "{}.{}.{}", self.major, self.minor, self.patch) } } } -impl AsRef for Version { - #[inline] - fn as_ref(&self) -> &Self { self } -} -impl From<&Version> for Version { - #[inline] - fn from(value: &Version) -> Self { *value } -} -impl From<&mut Version> for Version { - #[inline] - fn from(value: &mut Version) -> Self { *value } -} - +impl PartialOrd for AliasedFunctionVersion { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + // Latest is just latest + (AliasedFunctionVersion::Latest, AliasedFunctionVersion::Latest) => Some(Ordering::Equal), + // We cannot determine the concrete version of latest + (AliasedFunctionVersion::Latest, AliasedFunctionVersion::Version(_)) => None, + (AliasedFunctionVersion::Version(_), AliasedFunctionVersion::Latest) => None, -impl PartialEq for Version { - #[inline] - fn eq(&self, other: &semver::Version) -> bool { - !self.is_latest() && self.major == other.major && self.minor == other.minor && self.patch == other.patch + // Regular comparison + (AliasedFunctionVersion::Version(v_self), AliasedFunctionVersion::Version(v_other)) => v_self.partial_cmp(v_other), + } } } -impl PartialOrd for Version { - #[inline] - fn partial_cmp(&self, other: &semver::Version) -> Option { - // Do not compare if latest - if self.is_latest() { - return None; - } - // Compare the major number - let order = self.major.cmp(&other.major); - if order.is_ne() { - return Some(order); - } +impl AliasedFunctionVersion { + pub fn from_package_pair(package: &str) -> Result<(String, Option), VersionError> { + let mut parts = package.split(":"); - // Compare the minor number - let order = self.minor.cmp(&other.minor); - if order.is_ne() { - return Some(order); + match (parts.next(), parts.next(), parts.next()) { + (Some(package_name), None, None) => Ok((package_name.to_string(), None)), + (Some(package_name), Some(package_version), None) => { + Ok((package_name.to_string(), Some(AliasedFunctionVersion::from_str(package_version)?))) + }, + (_, _, _) => Err(VersionError::TooManyColons { raw: package.into(), got: parts.count() + 3 }), } - - // Compare the patch - Some(self.patch.cmp(&other.patch)) } -} -impl From for Version { - #[inline] - fn from(version: semver::Version) -> Self { Self { major: version.major, minor: version.minor, patch: version.patch } } + pub fn is_latest(&self) -> bool { matches!(self, Self::Latest) } } -impl From<&semver::Version> for Version { - #[inline] - fn from(version: &semver::Version) -> Self { Self { major: version.major, minor: version.minor, patch: version.patch } } +impl From for AliasedFunctionVersion { + fn from(value: ConcreteFunctionVersion) -> Self { AliasedFunctionVersion::Version(value) } } - - -impl From for String { - #[inline] - fn from(value: Version) -> Self { format!("{value}") } -} - -impl From<&Version> for String { - #[inline] - fn from(value: &Version) -> Self { format!("{value}") } +#[derive(Debug, thiserror::Error)] +pub enum VersionError { + #[error("Something went wrong parsing a version as a semver version")] + SemVer( + #[source] + #[from] + semver::Error, + ), + #[error("Given 'NAME[:VERSION]' pair '{raw}' has too many colons (got {got}, expected at most 1)")] + TooManyColons { raw: String, got: usize }, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)] +pub struct BraneVersion(pub semver::Version); +impl FromStr for BraneVersion { + type Err = semver::Error; -impl Serialize for Version { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } + fn from_str(s: &str) -> Result { Ok(BraneVersion(semver::Version::from_str(s)?)) } } -impl<'de> Deserialize<'de> for Version { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(VersionVisitor) - } +impl Display for BraneVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } diff --git a/specifications/src/wir/mod.rs b/specifications/src/wir/mod.rs index ac6d3476..649b7584 100644 --- a/specifications/src/wir/mod.rs +++ b/specifications/src/wir/mod.rs @@ -45,7 +45,7 @@ use serde_json_any_key::any_key_map; use crate::data::{AvailabilityKind, DataName}; use crate::package::Capability; -use crate::version::Version; +use crate::version::ConcreteFunctionVersion; /***** CONSTANTS *****/ @@ -466,7 +466,7 @@ pub struct ComputeTaskDef { pub package: String, /// The version of the package that this task belongs to. #[serde(rename = "v")] - pub version: Version, + pub version: ConcreteFunctionVersion, /// The definition of the function that this package implements. #[serde(rename = "d")] @@ -492,7 +492,7 @@ pub struct ClassDef { pub package: Option, /// The version of the package that this class belongs to. #[serde(rename = "v")] - pub version: Option, + pub version: Option, /// The properties in this class. #[serde(rename = "p")]