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
17 changes: 9 additions & 8 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Environment-derived configuration for both modes.

use crate::env::Env;
use crate::error::{Error, Result};
use crate::gh_oidc::{Principal, PrincipalKind};
use base64::Engine as _;
Expand All @@ -26,17 +27,17 @@ impl CfCreds {

/// Configuration shared between modes.
pub struct Common {
pub env_label: String,
pub env: Env,
pub port: u16,
pub owner: Principal,
pub vm_name: String,
}

impl Common {
pub fn from_env() -> Result<Self> {
let env_label = std::env::var("DD_ENV").map_err(|_| {
let env = Env::parse(&std::env::var("DD_ENV").map_err(|_| {
Error::Internal("DD_ENV required (dev / staging / production / pr-*)".into())
})?;
})?)?;
let port = std::env::var("DD_PORT")
.ok()
.and_then(|s| s.parse().ok())
Expand Down Expand Up @@ -79,7 +80,7 @@ impl Common {
.unwrap_or_else(|| "unknown".into())
});
Ok(Self {
env_label,
env,
port,
owner,
vm_name,
Expand Down Expand Up @@ -119,7 +120,7 @@ impl ItaMode {
}

impl Ita {
pub fn from_env(env_label: &str) -> Result<Self> {
pub fn from_env(env: &Env) -> Result<Self> {
let get = |k: &str| {
std::env::var(k)
.ok()
Expand All @@ -134,7 +135,7 @@ impl Ita {
{
"" | "intel" => ItaMode::Intel,
"local" => {
if env_label == "production" || env_label == "staging" {
if env.requires_intel_ita() {
return Err(Error::Internal(
"DD_ITA_MODE=local is not allowed for production or staging".into(),
));
Expand Down Expand Up @@ -201,7 +202,7 @@ impl Cp {
"DD_SCRAPER_SHARD_INDEX ({scraper_shard_index}) must be less than DD_SCRAPER_SHARD_TOTAL ({scraper_shard_total})"
)));
}
let ita = Ita::from_env(&common.env_label)?;
let ita = Ita::from_env(&common.env)?;
Ok(Self {
common,
cf,
Expand Down Expand Up @@ -295,7 +296,7 @@ impl Agent {
.unwrap_or_else(|_| "/var/lib/easyenclave/data/dd-agent/devices.json".into())
.into();
let oracles = parse_oracles()?;
let ita = Ita::from_env(&common.env_label)?;
let ita = Ita::from_env(&common.env)?;
Ok(Self {
common,
cp_url,
Expand Down
26 changes: 13 additions & 13 deletions src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ pub async fn run() -> Result<()> {
// safe to run unconditionally — it doesn't touch the poisonable
// hostname at all.
let store: Store = Arc::new(Mutex::new(HashMap::new()));
let predecessor_prefix = cf::cp_prefix(&cfg.common.env_label);
let predecessor_prefix = cf::cp_prefix(cfg.common.env.label());
let has_predecessor = match cf::list(&http, &cfg.cf).await {
Ok(tunnels) => tunnels.iter().any(|t| {
t["name"]
Expand All @@ -156,7 +156,7 @@ pub async fn run() -> Result<()> {
// for `cfg.hostname` → our tunnel id; traffic moves to us the
// moment CF's edge propagates that change.
eprintln!("cp: self-provisioning tunnel for {}", cfg.hostname);
let self_name = cf::cp_tunnel_name(&cfg.common.env_label);
let self_name = cf::cp_tunnel_name(cfg.common.env.label());
let cp_extras: Vec<(String, u16)> = vec![("shell".into(), 7682)];
let tunnel = match cf::create(&http, &cfg.cf, &self_name, &cfg.hostname, &cp_extras).await {
Ok(t) => t,
Expand Down Expand Up @@ -184,7 +184,7 @@ pub async fn run() -> Result<()> {
match cf::delete_cp_access_apps(
&http,
&cfg.cf,
&cfg.common.env_label,
cfg.common.env.label(),
&cfg.hostname,
&cp_labels,
)
Expand Down Expand Up @@ -217,7 +217,7 @@ pub async fn run() -> Result<()> {
collector::Agent {
agent_id: "control-plane".into(),
hostname: cfg.hostname.clone(),
vm_name: format!("dd-{}-cp", cfg.common.env_label),
vm_name: format!("dd-{}-cp", cfg.common.env.label()),
attestation_type: "tdx".into(),
status: "healthy".into(),
last_seen: chrono::Utc::now(),
Expand Down Expand Up @@ -309,7 +309,7 @@ pub async fn run() -> Result<()> {
tokio::spawn(collector::run(
store.clone(),
cfg.cf.clone(),
cfg.common.env_label.clone(),
cfg.common.env.label().to_string(),
cfg.hostname.clone(),
ee.clone(),
verifier.clone(),
Expand Down Expand Up @@ -467,7 +467,7 @@ async fn health(
"ok": true,
"service": "cp",
"hostname": s.cfg.hostname,
"env": s.cfg.common.env_label,
"env": s.cfg.common.env.label(),
// Commit this binary was built from, baked at compile time
// (`DD_BUILD_SHA` env in the release build step). "dev" for local
// builds. Lets a deploy confirm which build is actually live —
Expand Down Expand Up @@ -541,7 +541,7 @@ async fn register(
}

let http = cf::http_client();
let name = cf::agent_tunnel_name(&s.cfg.common.env_label);
let name = cf::agent_tunnel_name(s.cfg.common.env.label());
let agent_hostname = format!("{name}.{}", s.cfg.cf.domain);
let extras: Vec<(String, u16)> = req
.extra_ingress
Expand All @@ -561,7 +561,7 @@ async fn register(
if let Err(e) = cf::delete_agent_access_apps(
&http,
&s.cfg.cf,
&s.cfg.common.env_label,
s.cfg.common.env.label(),
&agent_hostname,
&labels,
)
Expand Down Expand Up @@ -694,7 +694,7 @@ async fn ingress_replace(
if let Err(e) = cf::delete_agent_access_apps(
&http,
&s.cfg.cf,
&s.cfg.common.env_label,
s.cfg.common.env.label(),
&hostname,
&labels,
)
Expand Down Expand Up @@ -980,7 +980,7 @@ fn fleet_json(s: &St, by_id: &[(String, collector::Agent)]) -> serde_json::Value
let oracle_total: usize = by_id.iter().map(|(_, a)| a.oracles.len()).sum();
serde_json::json!({
"generated_at": chrono::Utc::now().to_rfc3339(),
"env": s.cfg.common.env_label,
"env": s.cfg.common.env.label(),
"hostname": s.cfg.hostname,
"summary": {
"agents": by_id.len(),
Expand Down Expand Up @@ -1111,7 +1111,7 @@ async fn fleet(State(s): State<St>, headers: HeaderMap, uri: Uri) -> Response {
Err(resp) => return resp,
};
let by_id = scoped_snapshot(&s, &session).await;
let body = render_fleet_body(&s.cfg.hostname, &s.cfg.common.env_label, &by_id);
let body = render_fleet_body(&s.cfg.hostname, s.cfg.common.env.label(), &by_id);
Html(shell(
"DD Fleet",
&html::nav(&[("Fleet", "/", true)]),
Expand Down Expand Up @@ -1141,7 +1141,7 @@ async fn fleet_fragment(State(s): State<St>, headers: HeaderMap, uri: Uri) -> Re
} else {
Html(render_fleet_body(
&s.cfg.hostname,
&s.cfg.common.env_label,
s.cfg.common.env.label(),
&by_id,
))
.into_response()
Expand Down Expand Up @@ -1304,7 +1304,7 @@ async fn cf_snapshot_handler(
let snap = crate::cf_snapshot::snapshot(
&http,
&s.cfg.cf,
&s.cfg.common.env_label,
s.cfg.common.env.label(),
&s.cfg.hostname,
&s.store,
)
Expand Down
156 changes: 156 additions & 0 deletions src/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//! First-class environment identity for the control plane.
//!
//! Every DD installation — production, staging, dev, a per-PR preview, or
//! a named long-lived install like `bot` / `dogfood` — is one [`Env`].
//! It is the single axis that distinguishes installations: all Cloudflare
//! resource names derive from [`Env::label`] (see [`crate::cf`]), and
//! env-intrinsic behaviour (Intel ITA enforcement, ephemerality) lives
//! here as methods rather than scattered `== "production"` string checks.

use crate::error::{Error, Result};

/// Well-known classes of environment. Anything that validates as a label
/// but isn't one of these is [`EnvKind::Named`] — so a legitimately-named
/// install (`bot`, `dogfood`, a future env) is never rejected at boot,
/// only genuinely malformed `DD_ENV` values are.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum EnvKind {
Production,
Staging,
Dev,
/// A per-PR ephemeral preview, `pr-<n>`.
Preview,
/// A valid but non-well-known label (e.g. `bot`, `dogfood`).
Named,
}

/// A parsed, validated environment label.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Env {
label: String,
kind: EnvKind,
}

impl Env {
/// Parse and validate the `DD_ENV` label.
///
/// The label is embedded directly into Cloudflare resource names
/// (`dd-{label}-cp-…`) and hostnames, so it must be a lowercase
/// DNS-style token: `^[a-z0-9][a-z0-9-]*$`, at most 40 chars. This
/// rejects empty/whitespace/uppercase values and junk like
/// `invalid name {uuid}` that would silently break collector
/// discovery, while still accepting any well-formed install name.
pub fn parse(s: &str) -> Result<Self> {
let label = s.trim();
let valid = !label.is_empty()
&& label.len() <= 40
&& label
.bytes()
.next()
.is_some_and(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
&& label
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
if !valid {
return Err(Error::Internal(format!(
"DD_ENV {s:?} invalid: expected a lowercase DNS label \
(e.g. production, staging, dev, pr-42, bot)"
)));
}
let kind = match label {
"production" => EnvKind::Production,
"staging" => EnvKind::Staging,
"dev" => EnvKind::Dev,
// Per-PR previews are `pr-<n>`. Any other label is a named
// long-lived install.
_ if label.starts_with("pr-") && label.len() > 3 => EnvKind::Preview,
_ => EnvKind::Named,
};
Ok(Self {
label: label.to_string(),
kind,
})
}

/// The canonical env label string (what every CF name derives from).
pub fn label(&self) -> &str {
&self.label
}

/// The classified kind.
pub fn kind(&self) -> &EnvKind {
&self.kind
}

/// Production and staging must use real Intel TDX attestation; other
/// envs may run a signed local token on hosts without Intel PCCS
/// collateral. Mirrors the prior `== "production" || == "staging"`
/// check that gated `DD_ITA_MODE=local`.
pub fn requires_intel_ita(&self) -> bool {
matches!(self.kind, EnvKind::Production | EnvKind::Staging)
}

/// Per-PR previews are torn down with the PR; every other install is
/// persistent. Drives lifecycle policy (libvirt autostart, CF GC TTL).
pub fn is_ephemeral(&self) -> bool {
matches!(self.kind, EnvKind::Preview)
}
}

impl std::fmt::Display for Env {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.label)
}
}

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

#[test]
fn well_known_kinds() {
assert_eq!(
*Env::parse("production").unwrap().kind(),
EnvKind::Production
);
assert_eq!(*Env::parse("staging").unwrap().kind(), EnvKind::Staging);
assert_eq!(*Env::parse("dev").unwrap().kind(), EnvKind::Dev);
assert_eq!(*Env::parse("pr-42").unwrap().kind(), EnvKind::Preview);
// Named long-lived installs are accepted, not rejected.
assert_eq!(*Env::parse("bot").unwrap().kind(), EnvKind::Named);
assert_eq!(*Env::parse("dogfood").unwrap().kind(), EnvKind::Named);
}

#[test]
fn label_roundtrips_and_trims() {
assert_eq!(Env::parse(" production ").unwrap().label(), "production");
assert_eq!(Env::parse("pr-7").unwrap().label(), "pr-7");
}

#[test]
fn rejects_malformed() {
assert!(Env::parse("").is_err());
assert!(Env::parse(" ").is_err());
assert!(Env::parse("Production").is_err()); // uppercase
assert!(Env::parse("invalid name").is_err()); // whitespace
assert!(Env::parse("-leading-hyphen").is_err());
assert!(Env::parse("pr_42").is_err()); // underscore not allowed
assert!(Env::parse(&"x".repeat(41)).is_err()); // too long
}

#[test]
fn requires_intel_ita_matches_prior_behaviour() {
assert!(Env::parse("production").unwrap().requires_intel_ita());
assert!(Env::parse("staging").unwrap().requires_intel_ita());
assert!(!Env::parse("dev").unwrap().requires_intel_ita());
assert!(!Env::parse("pr-42").unwrap().requires_intel_ita());
assert!(!Env::parse("bot").unwrap().requires_intel_ita());
}

#[test]
fn ephemerality() {
assert!(Env::parse("pr-42").unwrap().is_ephemeral());
assert!(!Env::parse("production").unwrap().is_ephemeral());
assert!(!Env::parse("bot").unwrap().is_ephemeral());
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod config;
pub mod cp;
pub mod devices;
pub mod ee;
pub mod env;
pub mod error;
pub mod gh_oidc;
pub mod html;
Expand Down
Loading