1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! `pelagos build` — build an image from a Remfile.
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, clap::Args)]
pub struct BuildArgs {
/// Tag for the built image (e.g. myapp:latest)
#[clap(long, short = 't')]
pub tag: String,
/// Path to Remfile (default: Remfile in context dir)
#[clap(long, short = 'f')]
pub file: Option<String>,
/// Network mode for RUN steps: auto | bridge (root) | pasta (rootless) |
/// host (share parent/pod network, for in-pod builds) | none (isolated)
#[clap(long, default_value = "auto")]
pub network: String,
/// Disable build cache (re-run all steps)
#[clap(long)]
pub no_cache: bool,
/// DNS backend: builtin (default) or dnsmasq
#[clap(long = "dns-backend", value_name = "BACKEND")]
pub dns_backend: Option<String>,
/// Set build-time variables (e.g. --build-arg VERSION=1.0)
#[clap(long = "build-arg")]
pub build_arg: Vec<String>,
/// Override the subnet used when bootstrapping pelagos0 for the first time.
/// Has no effect once pelagos0 already exists.
#[clap(long = "default-subnet", value_name = "CIDR")]
pub default_subnet: Option<String>,
/// Build context directory (default: current directory)
#[clap(default_value = ".")]
pub context: String,
}
pub fn cmd_build(args: BuildArgs) -> Result<(), Box<dyn std::error::Error>> {
use pelagos::build;
use pelagos::network::NetworkMode;
// Set DNS backend env var before any DNS calls so active_backend() picks it up.
if let Some(ref backend) = args.dns_backend {
// SAFETY: called early in single-threaded CLI startup, before spawning threads.
unsafe { std::env::set_var("PELAGOS_DNS_BACKEND", backend) };
}
// Bootstrap pelagos0 with the configured (or CLI-overridden) default subnet.
// No-op when the network already exists on disk.
{
let cfg = pelagos::config::PelagosConfig::load();
let subnet = if let Some(ref cidr) = args.default_subnet {
pelagos::network::Ipv4Net::from_cidr(cidr)
.map_err(|e| format!("--default-subnet '{}': {}", cidr, e))?
} else {
cfg.network.default_subnet_parsed()
};
let _ = pelagos::network::bootstrap_default_network(Some(&subnet));
}
let context_dir = PathBuf::from(&args.context)
.canonicalize()
.map_err(|e| format!("cannot access build context '{}': {}", args.context, e))?;
// Determine Remfile path.
let remfile_path = if let Some(ref f) = args.file {
PathBuf::from(f)
} else {
context_dir.join("Remfile")
};
if !remfile_path.is_file() {
return Err(format!(
"Remfile not found: {} (use -f to specify a different path)",
remfile_path.display()
)
.into());
}
let content = std::fs::read_to_string(&remfile_path)?;
let instructions = build::parse_remfile(&content)?;
if instructions.is_empty() {
return Err("Remfile is empty".into());
}
// Determine network mode.
let network_mode = match args.network.as_str() {
"bridge" => NetworkMode::Bridge,
"pasta" => NetworkMode::Pasta,
"none" => NetworkMode::Loopback,
// Share the parent's network namespace (no isolation) — like
// `docker build --network host`. This is what makes `pelagos build`
// work INSIDE a pod (in-cluster CI): RUN steps inherit the pod's
// network (which already has internet), so no pasta/bridge is needed —
// pasta's netns bind is EPERM'd nested-in-pod and bridge needs host
// CAP_NET_ADMIN. See #430.
"host" => NetworkMode::None,
"auto" => {
if pelagos::paths::is_rootless() {
NetworkMode::Pasta
} else {
NetworkMode::Bridge
}
}
name => {
// Check if it's a named network.
let config = pelagos::paths::network_config_dir(name).join("config.json");
if config.exists() {
NetworkMode::BridgeNamed(name.to_string())
} else {
return Err(format!(
"unknown network '{}' — use a mode (none, host, bridge, pasta, auto) \
or create it first: pelagos network create {} --subnet CIDR",
name, name
)
.into());
}
}
};
// Parse --build-arg KEY=VALUE pairs into a map.
let mut build_args_map = HashMap::new();
for arg in &args.build_arg {
if let Some((k, v)) = arg.split_once('=') {
build_args_map.insert(k.to_string(), v.to_string());
} else {
// Bare name with no value — use empty string (matches Docker behaviour).
build_args_map.insert(arg.clone(), String::new());
}
}
eprintln!("Building {} from {}", args.tag, remfile_path.display());
let manifest = build::execute_build(
&instructions,
&context_dir,
&args.tag,
network_mode,
!args.no_cache,
&build_args_map,
Some(&|reference| {
super::image::cmd_image_pull(reference, None, None, false, false)
.map_err(|e| e.to_string())
}),
)
.map_err(|e| {
// Propagate as-is, but add a setup hint when the layer store is not writable.
let msg = e.to_string();
if msg.contains("Permission denied") || msg.contains("os error 13") {
format!(
"{}\nhint: the pelagos layer store requires write access.\n\
Run 'sudo ./scripts/setup.sh' to fix permissions, or build with sudo.",
msg
)
} else {
msg
}
})?;
eprintln!(
"Successfully built {} ({} layers)",
args.tag,
manifest.layers.len()
);
Ok(())
}