Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
dbb0cdd
feat: add sites, deployments, env vars and domains schema
CodeMaster4711 Nov 1, 2026
f87a463
feat: add site-builder crate with framework and mode detection
CodeMaster4711 Nov 1, 2026
713f490
feat: add site artifact storage and gateway binding
CodeMaster4711 Nov 1, 2026
1c44d5c
feat: add site-builder service for builds, artifacts and static edge
CodeMaster4711 Nov 1, 2026
86da50c
feat: add site build image with git clone, static packaging and kanik…
CodeMaster4711 Nov 1, 2026
a2a2074
feat: route static site domains through the ingress gateway
CodeMaster4711 Nov 1, 2026
682ccee
feat: add site management API with vault tokens, webhooks and deploym…
CodeMaster4711 Nov 1, 2026
e99a313
chore: run the site-builder service in the dev stack
CodeMaster4711 Nov 1, 2026
59ccbf5
feat: list web apps in resource groups with deployment status
CodeMaster4711 Nov 1, 2026
29d8a41
feat: add web app creation flow with repository inspection
CodeMaster4711 Nov 1, 2026
e48df90
feat: add web app detail page with deployments, build log, settings a…
CodeMaster4711 Nov 1, 2026
f4e0fbe
fix: route only docker hub images through the registry mirror
CodeMaster4711 Nov 1, 2026
c19893a
fix: pass registry transport settings to site build VMs
CodeMaster4711 Nov 1, 2026
8cd0eb0
fix: derive site image references on the control plane
CodeMaster4711 Nov 1, 2026
55a8d8f
fix: build the site builder image for the target architecture
CodeMaster4711 Nov 1, 2026
2bee2cd
ci: build and publish the site-builder binary and builder image
CodeMaster4711 Nov 1, 2026
797013f
feat: distribute the site secret to joining masters
CodeMaster4711 Nov 1, 2026
55aa22f
feat: store the site registry signing key
CodeMaster4711 Nov 1, 2026
4d61e28
feat: issue scoped registry tokens from the site builder
CodeMaster4711 Nov 1, 2026
46fbecb
feat: authenticate agent pulls of site images
CodeMaster4711 Nov 1, 2026
7d2c81d
feat: store site images under a reserved registry host
CodeMaster4711 Nov 1, 2026
4ad9123
feat: reject user workloads that reference site images
CodeMaster4711 Nov 1, 2026
7b98942
feat: prune old site images and artifacts
CodeMaster4711 Nov 1, 2026
879fbf1
feat: clean up registry images when a web app is deleted
CodeMaster4711 Nov 1, 2026
b123741
feat: show image state on web app deployments
CodeMaster4711 Nov 1, 2026
5b6f7b5
fix: publish local dns records for static site domains
CodeMaster4711 Nov 1, 2026
a46b8e3
fix: bound the edge archive cache by size
CodeMaster4711 Nov 1, 2026
9c9965c
feat: add hourly site usage table
CodeMaster4711 Nov 1, 2026
d34bad5
feat: count static site requests and bandwidth at the edge
CodeMaster4711 Nov 1, 2026
0918367
feat: show web app usage on the overview
CodeMaster4711 Nov 1, 2026
e94a9b1
feat: add container registry and access token tables
CodeMaster4711 Nov 1, 2026
74a1266
feat: serve user registry namespaces with scoped access tokens
CodeMaster4711 Nov 1, 2026
c52a47d
feat: manage container registries, access tokens and images from the …
CodeMaster4711 Nov 1, 2026
e4745cd
feat: keep workloads inside their own group's registry namespace
CodeMaster4711 Nov 1, 2026
df2342e
feat: add container registry page with images and access tokens
CodeMaster4711 Nov 1, 2026
0cab8f1
feat: list container registries as a resource in resource groups
CodeMaster4711 Nov 1, 2026
311a9d0
chore: run site hosting in the lima dev nodes and slim the builder image
CodeMaster4711 Nov 1, 2026
e0aacf1
fix: cast the build log limit so postgres accepts the append
CodeMaster4711 Nov 1, 2026
b7c63f0
fix: send each build log chunk once and keep git metadata out of site…
CodeMaster4711 Nov 1, 2026
e8b3be0
fix: build container images with kaniko in a chroot using the deploym…
CodeMaster4711 Nov 1, 2026
e75e755
fix: give kaniko a static mount table instead of bind mounts inside t…
CodeMaster4711 Nov 1, 2026
ebac4c6
fix: release the workload reference when a replaced runtime is retired
CodeMaster4711 Nov 1, 2026
9a373ed
fix: keep serving the previous runtime until its replacement is running
CodeMaster4711 Nov 1, 2026
71a8add
style: apply cargo fmt and clippy fixes
github-actions[bot] Nov 1, 2026
cdb0506
feat: add framework picker, package manager and auto detection to the…
CodeMaster4711 Nov 3, 2026
007f734
feat: show a live preview of production on the web app overview
CodeMaster4711 Nov 3, 2026
6630ce5
feat: report rebuild progress with a percent bar in the lima setup
CodeMaster4711 Nov 3, 2026
55754b3
fix: raise lima vm memory to avoid out of memory rebuilds
CodeMaster4711 Nov 3, 2026
3fb06ca
refactor: move the site builder image to its own repository
CodeMaster4711 Nov 3, 2026
68d21ca
feat: serve static sites from an internal resource group domain
CodeMaster4711 Nov 4, 2026
07c4160
feat: publish static sites in the resource group dns and respawn it a…
CodeMaster4711 Nov 4, 2026
5833e9f
feat: add build log tab, gateway icon and static site routes to the app
CodeMaster4711 Nov 4, 2026
0fa50c9
style: apply cargo fmt and clippy fixes
github-actions[bot] Nov 4, 2026
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
13 changes: 12 additions & 1 deletion .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ jobs:
- failover-controller
- sdn-controller
- object-storage
- site-builder
- csfx-migrate
arch:
- amd64
Expand Down Expand Up @@ -220,7 +221,7 @@ jobs:
run: |
VERSION="${{ needs.version.outputs.version }}"
CP_BINS=""
for svc in api-gateway registry scheduler volume-manager failover-controller sdn-controller object-storage csfx-migrate; do
for svc in api-gateway registry scheduler volume-manager failover-controller sdn-controller object-storage site-builder csfx-migrate; do
for arch in amd64 arm64; do
CP_BINS="${CP_BINS} csfx-cp-${svc}-${arch} csfx-cp-${svc}-${arch}.sha256"
done
Expand Down Expand Up @@ -422,6 +423,16 @@ jobs:
sha256 = "$(get_cp_sha256 csfx-cp-object-storage-arm64)";
};
};
site-builder = {
amd64 = {
url = "${RELEASE_BASE}/csfx-cp-site-builder-amd64";
sha256 = "$(get_cp_sha256 csfx-cp-site-builder-amd64)";
};
arm64 = {
url = "${RELEASE_BASE}/csfx-cp-site-builder-arm64";
sha256 = "$(get_cp_sha256 csfx-cp-site-builder-arm64)";
};
};
csfx-updater = {
amd64 = {
url = "${RELEASE_BASE}/csfx-updater-amd64";
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ jobs:
- failover-controller
- sdn-controller
- object-storage
- site-builder
- csfx-migrate
arch:
- amd64
Expand Down Expand Up @@ -217,7 +218,7 @@ jobs:
TAG="v${VERSION}"

CP_BINS=""
for svc in api-gateway registry scheduler volume-manager failover-controller sdn-controller object-storage csfx-migrate; do
for svc in api-gateway registry scheduler volume-manager failover-controller sdn-controller object-storage site-builder csfx-migrate; do
for arch in amd64 arm64; do
CP_BINS="${CP_BINS} csfx-cp-${svc}-${arch} csfx-cp-${svc}-${arch}.sha256"
done
Expand Down Expand Up @@ -416,6 +417,16 @@ jobs:
sha256 = "$(get_cp_sha256 csfx-cp-object-storage-arm64)";
};
};
site-builder = {
amd64 = {
url = "${RELEASE_BASE}/csfx-cp-site-builder-amd64";
sha256 = "$(get_cp_sha256 csfx-cp-site-builder-amd64)";
};
arm64 = {
url = "${RELEASE_BASE}/csfx-cp-site-builder-arm64";
sha256 = "$(get_cp_sha256 csfx-cp-site-builder-arm64)";
};
};
csfx-updater = {
amd64 = {
url = "${RELEASE_BASE}/csfx-updater-amd64";
Expand Down
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"control-plane/volume-manager",
"control-plane/object-storage",
"control-plane/registry",
"control-plane/site-builder",
"control-plane/shared/entity",
"control-plane/shared/migration",
"control-plane/shared/shared",
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ RUN apt-get update && apt-get install -y \
libssl-dev \
libpq-dev \
curl \
git \
protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*

Expand All @@ -28,6 +29,7 @@ COPY control-plane/sdn-controller/Cargo.toml ./control-plane/sdn-controller/
COPY control-plane/volume-manager/Cargo.toml ./control-plane/volume-manager/
COPY control-plane/object-storage/Cargo.toml ./control-plane/object-storage/
COPY control-plane/registry/Cargo.toml ./control-plane/registry/
COPY control-plane/site-builder/Cargo.toml ./control-plane/site-builder/
COPY control-plane/shared/entity/Cargo.toml ./control-plane/shared/entity/
COPY control-plane/shared/migration/Cargo.toml ./control-plane/shared/migration/
COPY control-plane/shared/shared/Cargo.toml ./control-plane/shared/shared/
Expand All @@ -42,6 +44,7 @@ RUN mkdir -p agent/src \
control-plane/volume-manager/src \
control-plane/object-storage/src \
control-plane/registry/src \
control-plane/site-builder/src \
control-plane/shared/entity/src \
control-plane/shared/migration/src \
control-plane/shared/shared/src \
Expand All @@ -55,6 +58,8 @@ RUN mkdir -p agent/src \
&& echo "fn main() {}" > control-plane/volume-manager/src/main.rs \
&& echo "fn main() {}" > control-plane/object-storage/src/main.rs \
&& echo "fn main() {}" > control-plane/registry/src/main.rs \
&& echo "fn main() {}" > control-plane/site-builder/src/main.rs \
&& echo "pub fn lib() {}" > control-plane/site-builder/src/lib.rs \
&& echo "fn main() {}" > control-plane/csfx-migrate/src/main.rs \
&& echo "fn main() {}" > control-plane/csfx-updater/src/main.rs \
&& echo "pub fn lib() {}" > control-plane/shared/entity/src/lib.rs \
Expand Down
40 changes: 40 additions & 0 deletions agent/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ pub struct ResourceGroupMember {
pub guest_ip: String,
}

#[derive(Debug, Deserialize, Clone)]
pub struct SiteService {
pub service_name: String,
pub address: String,
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct VpnPeer {
pub client_public_key: String,
Expand Down Expand Up @@ -168,6 +174,8 @@ pub struct MasterBundle {
pub openbao_seal_key: String,
pub openbao_role_id: String,
pub openbao_secret_id: String,
#[serde(default)]
pub site_secret: Option<String>,
pub etcd_name: String,
pub etcd_initial_cluster: String,
#[serde(default)]
Expand Down Expand Up @@ -486,6 +494,38 @@ impl ApiClient {
.context("Failed to parse resource group members response")
}

pub async fn fetch_resource_group_site_services(
&self,
api_key: &str,
resource_group_id: &str,
) -> Result<Vec<SiteService>> {
let url = format!(
"{}/api/resource-groups/{}/site-services",
self.gateway_url, resource_group_id
);

let resp = self
.client
.get(&url)
.header("X-API-Key", api_key)
.send()
.await
.context("Failed to fetch resource group site services")?;

if !resp.status().is_success() {
let status = resp.status();
anyhow::bail!(
"Failed to fetch resource group site services status={} {}",
status,
resp.text().await.unwrap_or_default()
);
}

resp.json::<Vec<SiteService>>()
.await
.context("Failed to parse resource group site services response")
}

pub async fn fetch_resource_group_peers(
&self,
api_key: &str,
Expand Down
1 change: 1 addition & 0 deletions agent/src/firecracker/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod api;
pub mod rootfs;
pub mod runtime;
pub mod site_registry;
121 changes: 94 additions & 27 deletions agent/src/firecracker/rootfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use std::time::{Duration, Instant};
use tokio::process::Command;
use tracing::{debug, info};

use super::site_registry::{SiteRegistry, SITE_IMAGE_HOST};

const DOCKER_HUB_REGISTRY: &str = "docker.io";
const CONTAINER_BUNDLE_DIR: &str = "csfx-bundle";
const CONTAINER_ROOTFS_DIR: &str = "rootfs";

Expand Down Expand Up @@ -59,26 +62,61 @@ fn store_digest(reference: &str, digest: &str) {
.insert(reference.to_string(), (digest.to_string(), Instant::now()));
}

pub struct RootfsBuilder {
client: Client,
registry_mirror: Option<String>,
fn resolve_reference(
image: &str,
mirror: Option<&str>,
site_registry: Option<&SiteRegistry>,
) -> Result<Reference> {
let reference: Reference = image.parse().context("Invalid image reference")?;
if reference.registry() == SITE_IMAGE_HOST {
let site_registry =
site_registry.context("Site registry endpoint is not configured on this node")?;
return site_registry.resolve(&reference);
}
let Some(mirror) = mirror else {
return Ok(reference);
};
if reference.registry() != DOCKER_HUB_REGISTRY {
return Ok(reference);
}

let tag_or_digest = match (reference.tag(), reference.digest()) {
(_, Some(digest)) => format!("@{digest}"),
(Some(tag), None) => format!(":{tag}"),
(None, None) => String::new(),
};
let mirrored = format!("{mirror}/{}{tag_or_digest}", reference.repository());
mirrored.parse().context("Invalid mirrored image reference")
}

impl Default for RootfsBuilder {
fn default() -> Self {
Self::new()
fn auth_for(reference: &Reference, site_registry: Option<&SiteRegistry>) -> RegistryAuth {
match site_registry {
Some(site) if reference.registry() == site.endpoint() => site.auth(),
_ => RegistryAuth::Anonymous,
}
}

pub struct RootfsBuilder {
client: Client,
registry_mirror: Option<String>,
site_registry: Option<SiteRegistry>,
}

impl RootfsBuilder {
pub fn new() -> Self {
pub fn new(site_registry: Option<SiteRegistry>) -> Self {
let registry_mirror = std::env::var("CSFX_REGISTRY_MIRROR")
.ok()
.filter(|value| !value.is_empty());

let protocol = match &registry_mirror {
Some(mirror) => ClientProtocol::HttpsExcept(vec![mirror.clone()]),
None => ClientProtocol::Https,
let insecure_hosts: Vec<String> = registry_mirror
.iter()
.cloned()
.chain(site_registry.iter().map(|site| site.endpoint().to_string()))
.collect();
let protocol = if insecure_hosts.is_empty() {
ClientProtocol::Https
} else {
ClientProtocol::HttpsExcept(insecure_hosts)
};

Self {
Expand All @@ -88,24 +126,10 @@ impl RootfsBuilder {
..Default::default()
}),
registry_mirror,
site_registry,
}
}

fn resolve_reference(&self, image: &str) -> Result<Reference> {
let reference: Reference = image.parse().context("Invalid image reference")?;
let Some(mirror) = &self.registry_mirror else {
return Ok(reference);
};

let tag_or_digest = match (reference.tag(), reference.digest()) {
(_, Some(digest)) => format!("@{digest}"),
(Some(tag), None) => format!(":{tag}"),
(None, None) => String::new(),
};
let mirrored = format!("{mirror}/{}{tag_or_digest}", reference.repository());
mirrored.parse().context("Invalid mirrored image reference")
}

async fn resolve_digest(&self, reference: &Reference, auth: &RegistryAuth) -> Result<String> {
if let Some(digest) = reference.digest() {
return Ok(digest.to_string());
Expand All @@ -124,8 +148,12 @@ impl RootfsBuilder {
}

pub async fn ensure_rootfs(&self, image: &str) -> Result<PathBuf> {
let reference = self.resolve_reference(image)?;
let auth = RegistryAuth::Anonymous;
let reference = resolve_reference(
image,
self.registry_mirror.as_deref(),
self.site_registry.as_ref(),
)?;
let auth = auth_for(&reference, self.site_registry.as_ref());
let digest = self.resolve_digest(&reference, &auth).await?;

let guest_init_version = guest_init_binary_version().await?;
Expand Down Expand Up @@ -538,6 +566,45 @@ mod tests {
use super::*;
use std::io::Write;

#[test]
fn docker_hub_images_are_routed_through_the_mirror() {
let reference = resolve_reference("nginx:1.27", Some("localhost:5050"), None).unwrap();
assert_eq!(reference.registry(), "localhost:5050");
assert_eq!(reference.repository(), "library/nginx");
assert_eq!(reference.tag(), Some("1.27"));
}

#[test]
fn site_images_resolve_to_the_site_registry_even_with_a_mirror() {
let site = SiteRegistry::from_gateway("https://10.0.0.5:8000", "key").unwrap();
let image = "sites.csfx.invalid/abc:12345678";
let reference = resolve_reference(image, Some("localhost:5050"), Some(&site)).unwrap();
assert_eq!(reference.whole(), "10.0.0.5:5060/abc:12345678");
assert!(matches!(
auth_for(&reference, Some(&site)),
RegistryAuth::Basic(user, _) if user == "agent"
));
assert!(resolve_reference(image, None, None).is_err());
}

#[test]
fn credentials_are_never_sent_to_other_registries() {
let site = SiteRegistry::from_gateway("https://10.0.0.5:8000", "key").unwrap();
let other: Reference = "ghcr.io/org/app:1".parse().unwrap();
assert!(matches!(
auth_for(&other, Some(&site)),
RegistryAuth::Anonymous
));
}

#[test]
fn other_registries_bypass_the_mirror() {
let image = "ghcr.io/csfx-cloud/csfx-site-builder:1";
let reference = resolve_reference(image, Some("localhost:5050"), None).unwrap();
assert_eq!(reference.registry(), "ghcr.io");
assert_eq!(reference.whole(), image);
}

fn layer_header(path: &str, entry_type: tar::EntryType, size: u64, mode: u32) -> tar::Header {
let mut header = tar::Header::new_gnu();
header.set_path(path).unwrap();
Expand Down
Loading
Loading