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
104 changes: 104 additions & 0 deletions core/crates/ployz-build/src/capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! The CLI writes the capture and the Build Machine reads it, so the names are a
//! client-to-daemon contract that releases keep.

use std::path::{Path, PathBuf};

pub const SOURCE: &str = "source";
/// The directory of material that is no source: registry configuration, recipes and Sealed values.
pub const PRIVATE: &str = "private";
pub const COMPOSE: &str = "compose.yaml";
/// The Build's Sealed values, still sealed, as a file under [`PRIVATE`].
pub const SEALED_RECORD: &str = "sealed.json";
const SEALED_ARGS: &str = "sealed-args.json";
const RAILPACK_PLAN: &str = "railpack.json";
const RAILPACK_SCRATCH: &str = "railpack";
const DOCKER: &str = "docker";
const DOCKER_CONFIG: &str = "config.json";

/// The directories a capture starts with, parents first.
#[must_use]
pub fn directories(root: &Path) -> [PathBuf; 3] {
[source(root), private(root), docker(root)]
}

#[must_use]
pub fn source(root: &Path) -> PathBuf {
root.join(SOURCE)
}

#[must_use]
pub fn private(root: &Path) -> PathBuf {
root.join(PRIVATE)
}

#[must_use]
pub fn compose(root: &Path) -> PathBuf {
root.join(COMPOSE)
}

#[must_use]
pub fn private_file(root: &Path, name: &str) -> PathBuf {
private(root).join(name)
}

/// The directory the Docker client reads its configuration from.
#[must_use]
pub fn docker(root: &Path) -> PathBuf {
private(root).join(DOCKER)
}

#[must_use]
pub fn docker_config(root: &Path) -> PathBuf {
docker(root).join(DOCKER_CONFIG)
}

/// The Railpack recipes the capture carries.
#[must_use]
pub fn railpack_plan(root: &Path) -> PathBuf {
private_file(root, RAILPACK_PLAN)
}

#[must_use]
pub fn sealed_record(root: &Path) -> PathBuf {
private_file(root, SEALED_RECORD)
}

/// The names of the Sealed values each target declares as Build arguments.
#[must_use]
pub fn sealed_args(root: &Path) -> PathBuf {
private_file(root, SEALED_ARGS)
}

/// The scratch directory a Railpack build writes its plan and images to. It is not
/// [`railpack_plan`], which the capture carries.
#[must_use]
pub fn railpack_scratch(root: &Path) -> PathBuf {
private_file(root, RAILPACK_SCRATCH)
}

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

#[test]
fn the_layout_keeps_the_names_releases_exchange() {
let root = Path::new("/capture");
let relative = |path: PathBuf| {
path.strip_prefix(root)
.unwrap()
.to_str()
.unwrap()
.to_owned()
};
assert_eq!(
directories(root).map(relative),
["source", "private", "private/docker"]
);
assert_eq!(relative(compose(root)), "compose.yaml");
assert_eq!(relative(docker_config(root)), "private/docker/config.json");
assert_eq!(relative(railpack_plan(root)), "private/railpack.json");
assert_eq!(relative(railpack_scratch(root)), "private/railpack");
assert_eq!(relative(sealed_record(root)), "private/sealed.json");
assert_eq!(relative(sealed_args(root)), "private/sealed-args.json");
}
}
9 changes: 6 additions & 3 deletions core/crates/ployz-build/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ pub(crate) fn build(
let mut refs = Vec::new();
let mut tags = Vec::new();
for (number, platform) in target.target.platforms.iter().enumerate() {
let archive = request.working_dir.join("private/railpack/variant.tar");
let metadata = request.working_dir.join("private/railpack/variant.json");
let archive =
crate::capture::railpack_scratch(request.working_dir).join("variant.tar");
let metadata =
crate::capture::railpack_scratch(request.working_dir).join("variant.json");
let mut args = bake_arguments(
request,
crate::Produce::Output(crate::Output::Load),
Expand Down Expand Up @@ -145,7 +147,8 @@ pub(crate) fn build(
tag,
],
)?;
let archive = request.working_dir.join("private/railpack/complete.tar");
let archive =
crate::capture::railpack_scratch(request.working_dir).join("complete.tar");
output(
"copy assembled Railpack image",
&[
Expand Down
1 change: 1 addition & 0 deletions core/crates/ployz-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//! bounded attempt, and one image bound to the content that attempt produced.

mod builder;
pub mod capture;
mod execution;
mod fence;
pub use fence::{CgroupPath, Fence, FenceInstaller};
Expand Down
2 changes: 1 addition & 1 deletion core/crates/ployz-build/src/railpack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub(crate) fn prepare(
}
}
}
let directory = request.working_dir.join("private/railpack");
let directory = crate::capture::railpack_scratch(request.working_dir);
fs::DirBuilder::new()
.mode(0o700)
.create(&directory)
Expand Down
21 changes: 13 additions & 8 deletions core/crates/ployz-build/src/received_recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub fn validate_capture(
.copied()
.chain(definition.image_contexts.keys().map(String::as_str))
.collect();
let bytes = fs::read(root.join("compose.yaml")).map_err(|_| "Build recipe is missing")?;
let bytes = fs::read(crate::capture::compose(root)).map_err(|_| "Build recipe is missing")?;
let document: Value =
serde_norway::from_slice(&bytes).map_err(|_| "invalid captured Build recipe")?;
no_interpolation(&document)?;
Expand Down Expand Up @@ -141,7 +141,7 @@ pub fn validate_capture(
}
default_ssh |= id == "default";
for path in paths.split(',') {
contained(root, Path::new(path), "private", true)?;
contained(root, Path::new(path), crate::capture::PRIVATE, true)?;
}
}
}
Expand All @@ -161,7 +161,12 @@ pub fn validate_capture(
return Err("remote Dockerfile path escapes its context".into());
}
} else {
contained(root, &Path::new(context).join(recipe), "source", true)?;
contained(
root,
&Path::new(context).join(recipe),
crate::capture::SOURCE,
true,
)?;
}
}
if let Some(contexts) = build.get("additional_contexts") {
Expand Down Expand Up @@ -249,7 +254,7 @@ pub fn validate_capture(
}
}
let railpack = railpack(root, definition, services, sealed)?;
let config_path = root.join("private/docker/config.json");
let config_path = crate::capture::docker_config(root);
let mut config: serde_json::Value = serde_json::from_slice(
&fs::read(&config_path).map_err(|_| "Build registry configuration is missing")?,
)
Expand Down Expand Up @@ -279,7 +284,7 @@ fn railpack(
services: &Mapping,
sealed: SealedFiles,
) -> Result<Vec<crate::Railpack>, InputError> {
let bytes = match fs::read(root.join("private/railpack.json")) {
let bytes = match fs::read(crate::capture::railpack_plan(root)) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(_) => return Err("cannot read captured Railpack recipes".into()),
Expand All @@ -296,7 +301,7 @@ fn railpack(
{
return Err("Railpack recipe does not identify one admitted target".into());
}
contained(root, &recipe.context, "source", false)?;
contained(root, &recipe.context, crate::capture::SOURCE, false)?;
if !root.join(&recipe.context).is_dir() {
return Err("Railpack context must be a captured directory".into());
}
Expand Down Expand Up @@ -339,7 +344,7 @@ fn secret_file(root: &Path, path: &Path, sealed: SealedFiles) -> Result<(), Inpu
.filter(|name| crate::valid_name(name))
.map(|_| ())
.ok_or_else(|| "Build secret is not a sealed value's file".into()),
_ => contained(root, path, "private", true),
_ => contained(root, path, crate::capture::PRIVATE, true),
}
}

Expand Down Expand Up @@ -394,7 +399,7 @@ fn context_path(
}
return Ok(());
}
contained(root, Path::new(value), "source", false)?;
contained(root, Path::new(value), crate::capture::SOURCE, false)?;
if !root.join(value).is_dir() {
return Err("Build source context is not a directory".into());
}
Expand Down
2 changes: 1 addition & 1 deletion core/crates/ployz-build/src/sealed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl Staged {
targets: &[Target],
opened: &OpenedSealed,
) -> Result<Option<PathBuf>, BuildError> {
let bytes = match fs::read(root.join("private/sealed-args.json")) {
let bytes = match fs::read(crate::capture::sealed_args(root)) {
Ok(bytes) => bytes,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(io_error(error)),
Expand Down
8 changes: 4 additions & 4 deletions core/crates/ployz-build/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ impl AdmittedUpload {
/// # Errors
/// Reports a file that exists but cannot be read.
pub fn private_file(&self, name: &str) -> io::Result<Option<Vec<u8>>> {
match fs::read(self.upload.root.join("private").join(name)) {
match fs::read(crate::capture::private_file(&self.upload.root, name)) {
Ok(bytes) => Ok(Some(bytes)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
Expand Down Expand Up @@ -232,10 +232,10 @@ impl Upload {
.filter(|bytes| *bytes <= PRIVATE_LIMIT as usize)
.ok_or("Build entry metadata exceeds byte limit")?;
let relative = received_path(path)?;
let source = relative.starts_with("source");
let source = relative.starts_with(crate::capture::SOURCE);
if !source
&& !relative.starts_with("private")
&& relative != Path::new("compose.yaml")
&& !relative.starts_with(crate::capture::PRIVATE)
&& relative != Path::new(crate::capture::COMPOSE)
{
return Err("Build entry is outside source and private material".into());
}
Expand Down
51 changes: 44 additions & 7 deletions core/crates/ployz-core/src/domain/ingress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,35 @@ use crate::{
tunnel_service_spec,
};

const CADDY_INGRESS_COMMAND: [&str; 4] = ["caddy", "run", "-c", "/config/caddy/Caddyfile"];
const CADDY_INGRESS_ADMIN: &str = "unix//run/ingress/caddy/admin.sock";
pub const INGRESS_CONFIG_MOUNT: &str = "/config";
const INGRESS_DATA_MOUNT: &str = "/data";
pub const INGRESS_RUNTIME_MOUNT: &str = "/run/ingress";
/// The port of Caddy's loopback listener, which the Tunnel Connector dials.
pub const INGRESS_LOOPBACK_PORT: u16 = 8480;
pub const INGRESS_LOOPBACK_BIND: &str = "127.0.0.1";
pub const INGRESS_LOOPBACK_BIND_V6: &str = "::1";
/// Environment variable selecting Caddy's administrative endpoint.
pub const CADDY_ADMIN_ENV: &str = "CADDY_ADMIN";
const INGRESS_PROXY_DATA_PATH: &str = "/var/lib/ployz/ingress";
const INGRESS_PROXY_RUNTIME_PATH: &str = "/run/ployz/ingress";

#[must_use]
pub fn ingress_certs_dir() -> String {
format!("{INGRESS_CONFIG_MOUNT}/caddy/certs")
}

fn caddy_ingress_command() -> Vec<String> {
["caddy", "run", "-c"]
.map(str::to_owned)
.into_iter()
.chain([format!("{INGRESS_CONFIG_MOUNT}/caddy/Caddyfile")])
.collect()
}

fn caddy_ingress_admin() -> String {
format!("unix/{INGRESS_RUNTIME_MOUNT}/caddy/admin.sock")
}

/// The `ip:port` token an Ingress Proxy config names one upstream by.
#[must_use]
pub fn ingress_upstream(address: ContainerAddress, port: NonZeroU16) -> String {
Expand Down Expand Up @@ -206,9 +228,9 @@ fn holder_container(image: String) -> ServiceContainerSpec {
pub(crate) fn caddy_container(image: String) -> ServiceContainerSpec {
ServiceContainerSpec {
image,
command: CADDY_INGRESS_COMMAND.map(str::to_owned).into(),
command: caddy_ingress_command(),
entrypoint: Vec::new(),
environment: BTreeMap::from([(CADDY_ADMIN_ENV.into(), CADDY_INGRESS_ADMIN.into())]),
environment: BTreeMap::from([(CADDY_ADMIN_ENV.into(), caddy_ingress_admin().into())]),
sealed_identity: None,
labels: Default::default(),
hostname: None,
Expand Down Expand Up @@ -255,9 +277,9 @@ fn caddy_volume_graph() -> ServiceVolumeGraph {
bind_volume(runtime.clone(), INGRESS_PROXY_RUNTIME_PATH),
],
vec![
mount(&data, "/config"),
mount(&data, "/data"),
mount(&runtime, "/run/ingress"),
mount(&data, INGRESS_CONFIG_MOUNT),
mount(&data, INGRESS_DATA_MOUNT),
mount(&runtime, INGRESS_RUNTIME_MOUNT),
],
)
.expect("static Caddy Volume graph is valid")
Expand Down Expand Up @@ -301,6 +323,21 @@ mod tests {

use super::*;

#[test]
fn the_proxy_paths_and_loopback_port_keep_their_values() {
assert_eq!(ingress_certs_dir(), "/config/caddy/certs");
assert_eq!(
caddy_ingress_command(),
["caddy", "run", "-c", "/config/caddy/Caddyfile"]
);
assert_eq!(caddy_ingress_admin(), "unix//run/ingress/caddy/admin.sock");
assert_eq!(INGRESS_LOOPBACK_PORT, 8480);
assert_eq!(INGRESS_LOOPBACK_BIND, "127.0.0.1");
assert_eq!(INGRESS_LOOPBACK_BIND_V6, "::1");
assert_eq!(INGRESS_PROXY_DATA_PATH, "/var/lib/ployz/ingress");
assert_eq!(INGRESS_PROXY_RUNTIME_PATH, "/run/ployz/ingress");
}

fn edge() -> BTreeSet<PlacementConstraint> {
[PlacementConstraint::parse("node.labels.edge==true").unwrap()].into()
}
Expand Down
21 changes: 15 additions & 6 deletions core/crates/ployz-core/src/domain/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
use std::collections::BTreeMap;

use crate::{
Ciphertext, ContainerLabels, ContainerPath, MachinePath, QualifiedService, RawVolumeSource,
RequestedServiceSpec, ServiceMode, ServiceMount, ServiceMountGraph, ServiceNetwork,
ServiceVolume, ServiceVolumeGraph, ServiceVolumeReference, UpdateConfig, caddy_container,
Ciphertext, ContainerLabels, ContainerPath, INGRESS_LOOPBACK_BIND, INGRESS_LOOPBACK_PORT,
MachinePath, QualifiedService, RawVolumeSource, RequestedServiceSpec, ServiceMode,
ServiceMount, ServiceMountGraph, ServiceNetwork, ServiceVolume, ServiceVolumeGraph,
ServiceVolumeReference, UpdateConfig, caddy_container,
};

/// The label on the Tunnel Connector's spec that carries the sealed tunnel token, so
Expand All @@ -22,7 +23,10 @@ const TUNNEL_TOKEN_MOUNT: &str = "/run/tunnel";
/// The uid the pinned cloudflared image runs as. The token file is owned by it.
pub const TUNNEL_USER_ID: u32 = 65532;
/// Caddy's loopback listener the connector proxies every request to.
pub const TUNNEL_ORIGIN: &str = "http://127.0.0.1:8480";
#[must_use]
pub fn tunnel_origin() -> String {
format!("http://{INGRESS_LOOPBACK_BIND}:{INGRESS_LOOPBACK_PORT}")
}
/// Why a Machine orders no certificate while every Server uses the Tunnel Door. The
/// daemon records it on each wanted hostname, and a domain's status reads it back.
pub const TUNNEL_SERVES_CERTIFICATE: &str =
Expand All @@ -42,7 +46,7 @@ pub fn tunnel_service_spec(image: &str, sealed_token: &Ciphertext) -> RequestedS
"--no-autoupdate",
"run",
"--url",
TUNNEL_ORIGIN,
&tunnel_origin(),
"--token-file",
&format!("{TUNNEL_TOKEN_MOUNT}/{TUNNEL_TOKEN_FILE}"),
]
Expand Down Expand Up @@ -112,6 +116,11 @@ mod tests {
Ciphertext::from_bytes(vec![9; 48])
}

#[test]
fn the_origin_is_the_loopback_listener() {
assert_eq!(tunnel_origin(), "http://127.0.0.1:8480");
}

#[test]
fn the_connector_joins_the_holder_and_carries_its_sealed_token_as_a_label() {
let spec = tunnel_service_spec("cloudflared:2026", &sealed());
Expand All @@ -122,7 +131,7 @@ mod tests {
spec.container
.command
.join(" ")
.contains(&format!("--url {TUNNEL_ORIGIN}"))
.contains("--url http://127.0.0.1:8480")
);
let [mount] = spec.mount_graph.volume_graph().mounts() else {
panic!("the connector has one mount");
Expand Down
Loading
Loading