Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ cargo install cargo-component
./target/release/fabricksd
```

**2. Build and deploy the example service:**
**2. Deploy the example service:**

```bash
# Build the WASM component
cd examples/hello-http
cargo component build --release
cd ../..

# Deploy via CLI
# Option A: Deploy from path (builds automatically if needed)
./target/release/fabricks service run examples/hello-http

# Option B: Build first, then run by tag
./target/release/fabricks build examples/hello-http
./target/release/fabricks images # List stored modules
./target/release/fabricks service run hello-http:0.1.0
```

**3. Test it:**
Expand All @@ -73,8 +73,14 @@ curl http://localhost:8080/
# List services
./target/release/fabricks service ls

# View service details
./target/release/fabricks service inspect <service-id>

# Stop the service
./target/release/fabricks service stop <service-id>

# Remove the service
./target/release/fabricks service rm <service-id>
```

### Create Your Own Service
Expand Down Expand Up @@ -370,8 +376,10 @@ fabricks pull wasm://redis:7.2
- ✅ Service management (create, start, stop, scale, delete)
- ✅ Network management and port binding
- ✅ Fabrickfile and mortar file parsing
- ✅ OCI registry client
- ✅ OCI registry client with local storage
- ✅ Health monitoring infrastructure
- ✅ Module storage integration (`fabricks images`, run by tag)
- ✅ Build-on-run (automatic build when running from path)

**In Progress:**
- 🔄 Auto-scaling
Expand Down
2 changes: 1 addition & 1 deletion fabricks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ hyper.workspace = true
hyper-util.workspace = true
http-body-util.workspace = true
tower.workspace = true
tempfile.workspace = true

[dev-dependencies]
tempfile.workspace = true
33 changes: 26 additions & 7 deletions fabricks/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ pub enum Commands {
///
/// Manage multi-service deployments.
Mortar(MortarArgs),

/// List locally stored modules.
///
/// Shows all modules built or pulled to local storage.
Images(ImagesArgs),
}

/// Arguments for the images command.
#[derive(Parser, Debug)]
pub struct ImagesArgs {
/// Output format.
#[arg(short, long, value_enum, default_value = "text")]
pub format: OutputFormat,
}

/// Arguments for the build command.
Expand Down Expand Up @@ -303,17 +316,23 @@ pub enum ServiceCommands {
format: OutputFormat,
},

/// Deploy and start a service from a Fabrickfile.
/// Deploy and start a service from a Fabrickfile or stored module.
///
/// Creates the service and starts it immediately.
///
/// The reference can be:
/// - A local file path (e.g., `./my-service` or `examples/hello-http`)
/// - A stored module tag (e.g., `hello-http:0.1.0`)
/// - A registry reference (e.g., `ghcr.io/user/module:1.0.0`) - requires `fabricks pull` first
Run {
/// Path to the Fabrickfile or directory containing one.
/// Module reference (path, tag, or registry reference).
///
/// Examples:
/// - ./my-service (local path)
/// - hello-http:0.1.0 (stored module tag)
/// - ghcr.io/user/module:latest (registry reference)
#[arg(default_value = ".")]
path: PathBuf,

/// Path to pre-built WASM module (skips build step).
#[arg(long)]
wasm: Option<PathBuf>,
reference: String,

/// Output format.
#[arg(short, long, value_enum, default_value = "text")]
Expand Down
180 changes: 180 additions & 0 deletions fabricks/src/commands/images.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//! Images command implementation.
//!
//! Lists all locally stored modules in OCI format.

use anyhow::{Context, Result};
use fabricks_oci::LocalStorage;

use crate::cli::{ImagesArgs, OutputFormat};
use crate::output;

/// Information about a stored image.
#[derive(Debug, serde::Serialize)]
struct ImageInfo {
/// Image reference (e.g., "hello-http:0.1.0").
reference: String,
/// Manifest digest.
digest: String,
/// WASM size in bytes.
wasm_size: u64,
/// Module name from config.
name: Option<String>,
/// Module version from config.
version: Option<String>,
}

/// Run the images command.
///
/// # Errors
///
/// Returns an error if storage operations fail.
pub async fn run(args: &ImagesArgs) -> Result<()> {
let storage = get_local_storage()?;
let refs = storage.list_references().await.context("Failed to list references")?;

if refs.is_empty() {
match args.format {
OutputFormat::Text => {
output::writeln("No images found.")?;
output::writeln("")?;
output::writeln("Build an image with: fabricks build <path>")?;
}
OutputFormat::Json => {
output::writeln("[]")?;
}
}
return Ok(());
}

let mut images = Vec::new();

for reference in &refs {
if let Ok(info) = get_image_info(&storage, reference).await {
images.push(info);
}
}

match args.format {
OutputFormat::Text => {
// Print header
output::writeln(&format!(
"{:<30} {:<12} {:<10} {:<64}",
"REFERENCE", "SIZE", "VERSION", "DIGEST"
))?;
output::writeln(&"-".repeat(116))?;

for image in &images {
let size = format_size(image.wasm_size);
let version = image.version.as_deref().unwrap_or("-");
let short_digest = if image.digest.len() > 19 {
&image.digest[..19]
} else {
&image.digest
};

output::writeln(&format!(
"{:<30} {:<12} {:<10} {}...",
image.reference, size, version, short_digest
))?;
}

output::writeln("")?;
output::writeln(&format!("Total: {} image(s)", images.len()))?;
}
OutputFormat::Json => {
let json = serde_json::to_string_pretty(&images)?;
output::writeln(&json)?;
}
}

Ok(())
}

/// Get image info from storage.
async fn get_image_info(storage: &LocalStorage, reference: &str) -> Result<ImageInfo> {
let manifest_digest = storage.get_manifest_digest(reference).await?;
let manifest_bytes = storage.get_blob(&manifest_digest).await?;
let manifest: serde_json::Value = serde_json::from_slice(&manifest_bytes)?;

// Extract WASM size from layers
let wasm_size = manifest["layers"]
.as_array()
.and_then(|layers| layers.first())
.and_then(|layer| layer["size"].as_u64())
.unwrap_or(0);

// Extract name and version from annotations
let annotations = manifest["annotations"].as_object();
let name = annotations
.and_then(|a| a.get("dev.fabricks.name"))
.and_then(|v| v.as_str())
.map(String::from);
let version = annotations
.and_then(|a| a.get("dev.fabricks.module.version"))
.and_then(|v| v.as_str())
.map(String::from);

Ok(ImageInfo {
reference: reference.to_string(),
digest: manifest_digest,
wasm_size,
name,
version,
})
}

/// Get the default local storage location.
fn get_local_storage() -> Result<LocalStorage> {
let home = dirs::home_dir().context("Could not determine home directory")?;
let storage_path = home.join(".fabricks").join("storage");

// Check if storage exists
if !storage_path.exists() {
anyhow::bail!(
"No local storage found at {}\n\
Build an image first with: fabricks build <path>",
storage_path.display()
);
}

LocalStorage::open(storage_path).context("Failed to open local storage")
}

/// Format size in human-readable format.
fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;

if bytes >= GB {
let whole = bytes / GB;
let frac = (bytes % GB) * 10 / GB;
format!("{whole}.{frac} GB")
} else if bytes >= MB {
let whole = bytes / MB;
let frac = (bytes % MB) * 10 / MB;
format!("{whole}.{frac} MB")
} else if bytes >= KB {
let whole = bytes / KB;
let frac = (bytes % KB) * 10 / KB;
format!("{whole}.{frac} KB")
} else {
format!("{bytes} B")
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_format_size() {
assert_eq!(format_size(0), "0 B");
assert_eq!(format_size(500), "500 B");
assert_eq!(format_size(1024), "1.0 KB");
assert_eq!(format_size(1536), "1.5 KB");
assert_eq!(format_size(1048576), "1.0 MB");
assert_eq!(format_size(1572864), "1.5 MB");
assert_eq!(format_size(1073741824), "1.0 GB");
}
}
1 change: 1 addition & 0 deletions fabricks/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

pub mod build;
pub mod daemon;
pub mod images;
pub mod inspect;
pub mod login;
pub mod logout;
Expand Down
Loading
Loading