use super::design::Design;
use super::{checkpipe, sbom};
use std::path::Path;
const PORT: u16 = 8000;
pub fn run_package(
root: &Path,
design: &Design,
docker: bool,
k8s: bool,
systemd: bool,
binary: bool,
) -> Result<(Vec<String>, String), String> {
let report = checkpipe::run_all(root, design, None).map_err(|e| e.to_string())?;
if !report.ok {
return Err(format!(
"check failed ({} diagnostics) — fix before packaging",
report.diagnostics.len()
));
}
let mut artifacts = Vec::new();
let mut text_targets = Vec::new();
if docker {
text_targets.push("docker");
}
if k8s {
text_targets.push("k8s");
}
if systemd {
text_targets.push("systemd");
}
if !text_targets.is_empty() {
artifacts.extend(emit_text_artifacts(root, design, &text_targets)?);
}
if binary {
artifacts.push(build_binary(root, design)?);
}
let deploy = root.join("deploy");
std::fs::create_dir_all(&deploy).map_err(|e| e.to_string())?;
let sbom = sbom::generate(root, "app")?;
std::fs::write(deploy.join("sbom.json"), &sbom).map_err(|e| e.to_string())?;
artifacts.push("deploy/sbom.json".to_string());
Ok((artifacts, "deploy/sbom.json".to_string()))
}
pub fn dockerfile(design: &Design) -> String {
let name = &design.name;
format!(
r#"# GENERATED by jerrycan package — hardened, multi-stage, non-root.
# Requires `jerrycan` published to crates.io (or vendored into this context): the
# in-container `cargo build` below fetches it like any dependency.
FROM rust:1-bookworm AS build
WORKDIR /build
RUN rustup target add x86_64-unknown-linux-musl && \
(apt-get update && apt-get install -y musl-tools || true)
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl -p app
RUN cp target/x86_64-unknown-linux-musl/release/app /build/{name}
FROM gcr.io/distroless/static:nonroot
COPY --from=build /build/{name} /usr/local/bin/{name}
USER nonroot
EXPOSE {PORT}
ENV JERRYCAN_ADDR=0.0.0.0:{PORT}
ENTRYPOINT ["/usr/local/bin/{name}"]
"#
)
}
pub fn k8s_manifests(design: &Design) -> String {
let name = &design.name;
format!(
r#"# GENERATED by jerrycan package — hardened manifests. Edit the image, then `kubectl apply -f k8s.yaml`.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {name}
labels:
app: {name}
spec:
replicas: 2
selector:
matchLabels:
app: {name}
template:
metadata:
labels:
app: {name}
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: {name}
image: {name}:latest
ports:
- containerPort: {PORT}
env:
- name: JERRYCAN_ADDR
value: "0.0.0.0:{PORT}"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
livenessProbe:
httpGet:
path: /healthz
port: {PORT}
initialDelaySeconds: 2
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: {PORT}
initialDelaySeconds: 1
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 500m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: {name}
spec:
selector:
app: {name}
ports:
- port: 80
targetPort: {PORT}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {name}
spec:
podSelector:
matchLabels:
app: {name}
policyTypes:
- Ingress
ingress:
- ports:
- protocol: TCP
port: {PORT}
"#
)
}
pub fn systemd_unit(design: &Design) -> String {
let name = &design.name;
format!(
r#"# GENERATED by jerrycan package. Install: copy the binary to /usr/local/bin/{name},
# this file to /etc/systemd/system/{name}.service, then `systemctl enable --now {name}`.
[Unit]
Description={name} (jerrycan)
After=network.target
[Service]
ExecStart=/usr/local/bin/{name}
Environment=JERRYCAN_ADDR=0.0.0.0:{PORT}
Environment=JERRYCAN_ENV=prod
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
"#
)
}
pub fn emit_text_artifacts(
app_root: &Path,
design: &Design,
targets: &[&str],
) -> Result<Vec<String>, String> {
let deploy = app_root.join("deploy");
std::fs::create_dir_all(&deploy).map_err(|e| e.to_string())?;
let mut written = Vec::new();
let mut write = |rel: &str, content: &str| -> Result<(), String> {
let path = deploy.join(rel);
std::fs::write(&path, content).map_err(|e| format!("write deploy/{rel}: {e}"))?;
written.push(format!("deploy/{rel}"));
Ok(())
};
if targets.contains(&"docker") {
write("Dockerfile", &dockerfile(design))?;
}
if targets.contains(&"k8s") {
write("k8s.yaml", &k8s_manifests(design))?;
}
if targets.contains(&"systemd") {
write(&format!("{}.service", design.name), &systemd_unit(design))?;
}
Ok(written)
}
pub fn build_binary(app_root: &Path, design: &Design) -> Result<String, String> {
let musl = "x86_64-unknown-linux-musl";
let musl_ok = std::process::Command::new("rustc")
.args(["--print", "target-list"])
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.any(|t| t == musl)
})
.unwrap_or(false)
&& target_installed(musl);
let (target_args, built_path) = if musl_ok {
(vec!["--target", musl], format!("target/{musl}/release/app"))
} else {
eprintln!(
"jerrycan package: musl target unavailable — building a host-target binary (not fully static). Install with: rustup target add {musl}"
);
(vec![], "target/release/app".to_string())
};
let mut args = vec!["build", "--release", "-p", "app"];
args.extend(target_args);
let status = std::process::Command::new("cargo")
.current_dir(app_root)
.args(&args)
.status()
.map_err(|e| format!("cargo build failed to run: {e}"))?;
if !status.success() {
return Err("release build failed".to_string());
}
let deploy = app_root.join("deploy");
std::fs::create_dir_all(&deploy).map_err(|e| e.to_string())?;
let dest = deploy.join(&design.name);
std::fs::copy(app_root.join(&built_path), &dest).map_err(|e| format!("copy binary: {e}"))?;
Ok(format!("deploy/{}", design.name))
}
fn target_installed(target: &str) -> bool {
std::process::Command::new("rustup")
.args(["target", "list", "--installed"])
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.any(|t| t == target)
})
.unwrap_or(false)
}