#![cfg(target_os = "windows")]
use zlayer_builder::backend::hcs::HcsBackend;
use zlayer_builder::backend::BuildBackend;
use zlayer_builder::windows::deps::{validate_dockerfile, DepsError};
use zlayer_builder::{BuildError, BuildOptions, Dockerfile};
use zlayer_paths::ZLayerDirs;
fn scratch_storage_root(slot: &str) -> std::path::PathBuf {
ZLayerDirs::system_default()
.tmp()
.join(format!("zlayer-builder-e2e-{slot}"))
}
#[tokio::test]
#[ignore = "requires Windows host with HCS + MCR network access + SeBackupPrivilege"]
async fn hcs_backend_round_trip_nanoserver_copy() {
let tmp = ZLayerDirs::system_default()
.scratch_dir("hcs-backend-round-trip-nanoserver-copy-")
.expect("tempdir for build context");
let context = tmp.path();
std::fs::write(
context.join("hello.txt"),
b"hello from zlayer-builder L-8 e2e\n",
)
.expect("write COPY source");
let dockerfile = Dockerfile::parse(
r#"
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
COPY hello.txt C:\hello.txt
WORKDIR C:\app
CMD ["cmd", "/c", "type", "C:\\hello.txt"]
"#,
)
.expect("parse Dockerfile");
let backend = HcsBackend::with_storage_root(scratch_storage_root("round-trip"))
.await
.expect("construct HCS backend");
let opts = BuildOptions {
tags: vec!["zlayer-windows-e2e:round-trip".to_string()],
..BuildOptions::default()
};
let built = backend
.build_image(context, &dockerfile, &opts, None)
.await
.expect("HCS build to succeed");
assert!(
built.image_id.starts_with("sha256:"),
"image_id should be a sha256 reference, got {}",
built.image_id
);
assert!(
built.layer_count >= 2,
"expected at least base + new diff layer, got {}",
built.layer_count
);
assert!(
built.size > 0,
"final OCI layout should occupy non-zero bytes"
);
assert!(
built.build_time_ms > 0,
"build_time_ms should be populated, got {}",
built.build_time_ms
);
assert_eq!(
built.tags,
vec!["zlayer-windows-e2e:round-trip".to_string()],
"tag list should survive the build"
);
}
#[tokio::test]
#[ignore = "requires Windows host to construct the HcsBackend (no network needed once constructed)"]
async fn hcs_backend_rejects_multi_stage() {
let dockerfile = Dockerfile::parse(
r"
FROM mcr.microsoft.com/windows/servercore:ltsc2022 AS builder
RUN echo built > C:\artifact.txt
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
COPY --from=builder C:\artifact.txt C:\artifact.txt
",
)
.expect("parse multi-stage Dockerfile");
assert_eq!(
dockerfile.stages.len(),
2,
"sanity: parser produces two stages"
);
let tmp = ZLayerDirs::system_default()
.scratch_dir("hcs-backend-rejects-multi-stage-")
.expect("tempdir for build context");
let backend = HcsBackend::with_storage_root(scratch_storage_root("reject-multi-stage"))
.await
.expect("construct HCS backend");
let err = backend
.build_image(tmp.path(), &dockerfile, &BuildOptions::default(), None)
.await
.expect_err("multi-stage Windows builds are deferred — must error");
match err {
BuildError::NotSupported { operation } => {
assert!(
operation.contains("multi-stage"),
"error message should name the deferred capability, got: {operation}"
);
assert!(
operation.contains("HCS"),
"error message should identify the HCS backend, got: {operation}"
);
}
other => panic!("expected BuildError::NotSupported, got {other:?}"),
}
}
#[test]
#[ignore = "kept under the Windows e2e gate for symmetry; the validator itself is cross-platform and already unit-tested in windows::deps"]
fn hcs_backend_rejects_choco_on_nanoserver() {
let dockerfile = Dockerfile::parse(
r"
FROM mcr.microsoft.com/windows/nanoserver:ltsc2022
RUN choco install nginx -y
",
)
.expect("parse nanoserver+choco Dockerfile");
let err =
validate_dockerfile(&dockerfile).expect_err("choco on nanoserver must be rejected early");
let DepsError::ChocoOnNanoserver {
instruction_index,
package_manager,
} = err;
assert_eq!(package_manager, "choco", "detected pm should be `choco`");
assert_eq!(
instruction_index, 0,
"offending instruction is the first RUN in the stage"
);
}
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use zlayer_agent::runtimes::hcs::{HcsConfig, HcsRuntime};
use zlayer_agent::{ContainerId, ContainerState, Runtime};
use zlayer_builder::windows_builder::{BuildContext, WindowsBuildConfig, WindowsBuilder};
use zlayer_registry::RegistryAuth;
use zlayer_spec::{DeploymentSpec, ServiceSpec};
fn local_registry_addr() -> String {
std::env::var("ZLAYER_E2E_REGISTRY").unwrap_or_else(|_| "localhost:5000".to_string())
}
#[allow(clippy::cast_possible_truncation)]
fn unique_tag_suffix() -> String {
let pid = std::process::id();
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
% 1_000_000;
format!("{pid}-{ts}")
}
fn builder_config(slot: &str) -> (WindowsBuildConfig, PathBuf) {
let cache_dir = ZLayerDirs::system_default()
.tmp()
.join(format!("zlayer-wcow-4f-{slot}"));
let cfg = WindowsBuildConfig {
cache_dir: cache_dir.clone(),
registry_auth: RegistryAuth::Anonymous,
platform: WindowsBuildConfig::default_platform().to_string(),
os_version_override: None,
scratch_size_gb: 0,
};
(cfg, cache_dir)
}
async fn fresh_runtime(slot: &str) -> (Arc<HcsRuntime>, PathBuf) {
let storage_root = std::env::temp_dir().join(format!("zlayer-builder-4f-rt-{slot}"));
let cfg = HcsConfig {
storage_root: storage_root.clone(),
default_scratch_size_gb: 20,
..HcsConfig::default()
};
let rt = HcsRuntime::new(cfg)
.await
.expect("HcsRuntime::new must succeed on a Windows host with HCS available");
(Arc::new(rt), storage_root)
}
fn write_build_context(context_dir: &std::path::Path) {
std::fs::create_dir_all(context_dir).expect("mk build context dir");
std::fs::write(context_dir.join("hello.txt"), b"hello\n").expect("write COPY source");
std::fs::write(
context_dir.join("Dockerfile"),
"FROM mcr.microsoft.com/windows/nanoserver:ltsc2022\n\
COPY hello.txt C:\\hello.txt\n\
CMD [\"cmd\", \"/c\", \"ping\", \"-n\", \"60\", \"127.0.0.1\"]\n",
)
.expect("write Dockerfile");
}
fn long_lived_spec(tag: &str) -> ServiceSpec {
let yaml = format!(
r#"
version: v1
deployment: zlayer-wcow-4f
services:
longlived:
rtype: service
image:
name: {tag}
command:
entrypoint: ["cmd", "/c", "ping", "-n", "60", "127.0.0.1"]
endpoints:
- name: dummy
protocol: tcp
port: 8080
scale:
mode: fixed
replicas: 1
"#
);
serde_yaml::from_str::<DeploymentSpec>(&yaml)
.expect("test YAML must parse into DeploymentSpec")
.services
.remove("longlived")
.expect("longlived service must exist in the YAML")
}
async fn wait_for_state(
runtime: &dyn Runtime,
id: &ContainerId,
expected: ContainerState,
budget: Duration,
) -> Result<ContainerState, String> {
let start = std::time::Instant::now();
let poll = Duration::from_millis(200);
let mut last: Option<ContainerState> = None;
while start.elapsed() < budget {
match runtime.container_state(id).await {
Ok(state) => {
let matches = match (&state, &expected) {
(ContainerState::Exited { .. }, ContainerState::Exited { .. }) => true,
(a, b) => a == b,
};
if matches {
return Ok(state);
}
last = Some(state);
}
Err(e) => return Err(format!("container_state error: {e}")),
}
tokio::time::sleep(poll).await;
}
Err(format!(
"timed out after {budget:?} waiting for {expected:?}; last observed = {last:?}"
))
}
fn rm_dir(path: &PathBuf) {
let _ = std::fs::remove_dir_all(path);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires Windows host with HCS + a local Docker registry at ZLAYER_E2E_REGISTRY (default localhost:5000) + nanoserver:ltsc2022 already mirrored into it; see file-level docs for setup"]
async fn windows_build_e2e_full_round_trip() {
let outcome = tokio::time::timeout(Duration::from_secs(900), async {
let suffix = unique_tag_suffix();
let tag = format!("{}/zlayer-wcow-4f:{suffix}", local_registry_addr());
let scratch = ZLayerDirs::system_default()
.scratch_dir("zlayer-wcow-4f-ctx-")
.expect("scratch dir for build context");
let context_dir = scratch.path().join("ctx");
write_build_context(&context_dir);
let (cfg, cache_dir) = builder_config(&format!("round-trip-{suffix}"));
let builder = WindowsBuilder::new(cfg);
let ctx = BuildContext {
context_dir,
dockerfile_path: PathBuf::from("Dockerfile"),
build_args: HashMap::new(),
tag: tag.clone(),
ltsc: None,
};
builder
.build_and_push(&ctx)
.await
.expect("WindowsBuilder::build_and_push must succeed against the local registry");
let (runtime, storage_root) = fresh_runtime(&format!("round-trip-{suffix}")).await;
runtime
.pull_image(&tag)
.await
.expect("pull_image must succeed against the local registry");
let id = ContainerId::new(format!("wcow-4f-{suffix}"), 1);
let spec = long_lived_spec(&tag);
let runtime_body = runtime.clone();
let id_body = id.clone();
let body = async move {
runtime_body
.create_container(&id_body, &spec)
.await
.expect("create_container must succeed");
runtime_body
.start_container(&id_body)
.await
.expect("start_container must succeed");
wait_for_state(
runtime_body.as_ref(),
&id_body,
ContainerState::Running,
Duration::from_secs(60),
)
.await
.expect("container must reach Running within 60s");
let cmd: Vec<String> = vec![
"cmd".into(),
"/c".into(),
"type".into(),
"C:\\hello.txt".into(),
];
let (exit_code, _stdout, _stderr) = runtime_body
.exec(&id_body, &cmd)
.await
.expect("exec must succeed against a Running container");
assert_eq!(
exit_code, 0,
"type C:\\hello.txt must exit 0 — the COPY-produced file did not survive the round-trip"
);
};
body.await;
let _ = runtime.stop_container(&id, Duration::from_secs(5)).await;
let _ = runtime.remove_container(&id).await;
rm_dir(&storage_root);
rm_dir(&cache_dir);
})
.await;
outcome.expect("windows_build_e2e_full_round_trip exceeded the 900s outer budget");
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires Windows host with HCS + a local Docker registry at ZLAYER_E2E_REGISTRY; see file-level docs for setup"]
async fn windows_build_e2e_foreign_layer_round_trips() {
let outcome = tokio::time::timeout(Duration::from_secs(900), async {
let suffix = unique_tag_suffix();
let registry = local_registry_addr();
let tag = format!("{registry}/zlayer-wcow-4f-foreign:{suffix}");
let scratch = ZLayerDirs::system_default()
.scratch_dir("zlayer-wcow-4f-foreign-ctx-")
.expect("scratch dir");
let context_dir = scratch.path().join("ctx");
write_build_context(&context_dir);
let (cfg, cache_dir) = builder_config(&format!("foreign-{suffix}"));
let builder = WindowsBuilder::new(cfg);
let ctx = BuildContext {
context_dir,
dockerfile_path: PathBuf::from("Dockerfile"),
build_args: HashMap::new(),
tag: tag.clone(),
ltsc: None,
};
builder
.build_and_push(&ctx)
.await
.expect("build_and_push must succeed for the foreign-layer assertion");
let scheme =
std::env::var("ZLAYER_E2E_REGISTRY_SCHEME").unwrap_or_else(|_| "https".to_string());
let (repo, tag_ref) = tag
.strip_prefix(&format!("{registry}/"))
.and_then(|rest| {
let (name, t) = rest.rsplit_once(':')?;
Some((name.to_string(), t.to_string()))
})
.expect("tag must be in <registry>/<name>:<tag> shape");
let manifest_url = format!("{scheme}://{registry}/v2/{repo}/manifests/{tag_ref}");
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true) .build()
.expect("reqwest client");
let resp = client
.get(&manifest_url)
.header(
reqwest::header::ACCEPT,
"application/vnd.oci.image.manifest.v1+json, \
application/vnd.docker.distribution.manifest.v2+json",
)
.send()
.await
.expect("GET manifest must succeed");
assert!(
resp.status().is_success(),
"manifest GET failed: {} {}",
resp.status(),
manifest_url
);
let body: serde_json::Value = resp
.json()
.await
.expect("manifest GET response must be valid JSON");
let layers = body["layers"]
.as_array()
.expect("manifest must carry a `layers` array");
assert!(!layers.is_empty(), "manifest must carry at least one layer");
let foreign_layer = &layers[0];
let media_type = foreign_layer["mediaType"]
.as_str()
.expect("layer 0 must have a mediaType");
assert_eq!(
media_type, "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip",
"layer 0 must be a foreign Windows base layer"
);
let urls = foreign_layer["urls"]
.as_array()
.expect("foreign layer 0 must carry a non-empty urls[] array");
assert!(
!urls.is_empty(),
"foreign layer urls[] must survive the push verbatim"
);
assert!(
urls.iter()
.any(|u| u.as_str().is_some_and(|s| s.contains("mcr.microsoft.com"))),
"foreign layer urls[] must contain an MCR mirror URL; got {urls:?}"
);
rm_dir(&cache_dir);
})
.await;
outcome.expect("windows_build_e2e_foreign_layer_round_trips exceeded the 900s outer budget");
}