isoloom_render/
constraints.rs1use std::collections::HashSet;
15
16pub use isoloom_plan::images::rewrite_image;
17use isoloom_spec::model::{Constraints, Mirrors, Spec};
18use isoloom_spec::shell::quote;
19use isoloom_spec::validate::docker_cidrs;
20
21#[must_use]
23pub fn of(spec: &Spec) -> Option<&Constraints> {
24 spec.constraints
25 .as_ref()
26 .filter(|c| c.proxy.is_some() || !c.registries.is_empty() || c.mirrors.is_some() || !c.ntp.is_empty())
27}
28
29fn proxies(c: &Constraints) -> Option<(&str, &str)> {
32 let p = c.proxy.as_ref()?;
33 let http = p.http.as_deref()?;
34 Some((http, p.https.as_deref().unwrap_or(http)))
35}
36
37#[must_use]
40pub fn no_proxy(spec: &Spec) -> Vec<String> {
41 let local = ["localhost", "127.0.0.1", "::1"].map(str::to_owned);
42 let names = spec.machines.iter().flat_map(|(name, m)| std::iter::once(name).chain(&m.aliases).cloned());
43 let blocks = spec.networks.values().map(|n| n.cidr.clone());
44 let docker = docker_cidrs(spec).into_iter().map(|(_, c)| c.to_string());
45 let own = spec.constraints.iter().flat_map(|c| &c.proxy).flat_map(|p| p.no_proxy.iter().cloned());
46 let mut seen = HashSet::new();
47 local
48 .into_iter()
49 .chain(names)
50 .chain(blocks)
51 .chain(docker)
52 .chain(own)
53 .filter(|x| seen.insert(x.clone()))
54 .collect()
55}
56
57fn proxy_env(spec: &Spec, (http, https): (&str, &str)) -> Vec<(String, String)> {
60 let np = no_proxy(spec).join(",");
61 [
62 ("HTTP_PROXY", "http_proxy", http),
63 ("HTTPS_PROXY", "https_proxy", https),
64 ("NO_PROXY", "no_proxy", np.as_str()),
65 ]
66 .into_iter()
67 .flat_map(|(upper, lower, value)| [(upper.to_owned(), value.to_owned()), (lower.to_owned(), value.to_owned())])
68 .collect()
69}
70
71#[must_use]
74pub fn machine_env(spec: &Spec) -> Vec<(String, String)> {
75 let Some(c) = of(spec) else { return Vec::new() };
76 let proxy = proxies(c).map(|urls| proxy_env(spec, urls)).unwrap_or_default();
77 let mirrors = c.mirrors.iter().flat_map(|m| {
78 [("PIP_INDEX_URL", &m.pypi), ("NPM_CONFIG_REGISTRY", &m.npm)]
79 .into_iter()
80 .filter_map(|(k, url)| Some((k.to_owned(), url.clone()?)))
81 });
82 proxy.into_iter().chain(mirrors).collect()
83}
84
85#[must_use]
88pub fn build_args(spec: &Spec) -> Vec<(String, String)> {
89 machine_env(spec)
90}
91
92#[must_use]
95pub fn linux_prelude(spec: &Spec) -> Option<String> {
96 let c = of(spec)?;
97 let mirrors = c.mirrors.as_ref().map(mirror_lines).unwrap_or_default();
98 Some(format!("set -e\n{}{mirrors}{}", proxy(spec, c), ntp(&c.ntp)))
99}
100
101fn mirror_lines(m: &Mirrors) -> String {
104 let sources = "/etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources";
105 let debian = m.debian.as_deref().map(|u| {
106 format!(
107 "# Debian's packages from the mirror.\n. /etc/os-release; if [ \"$ID\" = debian ]; then for f in {sources}; do if [ -f \"$f\" ]; then sed -i -E 's#https?://(deb|httpredir|ftp)\\.debian\\.org/debian/?([[:space:]]|$)#{}/\\2#g' \"$f\"; fi; done; fi\n",
108 u.trim_end_matches('/')
109 )
110 });
111 let ubuntu = m.ubuntu.as_deref().map(|u| {
112 format!(
113 "# Ubuntu's packages from the mirror.\n. /etc/os-release; if [ \"$ID\" = ubuntu ]; then for f in {sources}; do if [ -f \"$f\" ]; then sed -i -E 's#https?://([a-z0-9-]+\\.)?(archive|security)\\.ubuntu\\.com/ubuntu/?([[:space:]]|$)#{}/\\3#g' \"$f\"; fi; done; fi\n",
114 u.trim_end_matches('/')
115 )
116 });
117 let pypi = m
118 .pypi
119 .as_deref()
120 .map(|u| format!("printf '[global]\\nindex-url = %s\\n' {} > /etc/pip.conf\n", quote(u)));
121 let npm = m.npm.as_deref().map(|u| format!("printf 'registry=%s\\n' {} > /etc/npmrc\n", quote(u)));
122 [debian, ubuntu, pypi, npm].into_iter().flatten().collect()
123}
124
125fn ntp(servers: &[String]) -> String {
127 if servers.is_empty() {
128 return String::new();
129 }
130 format!(
131 "# Time from the site's servers: chrony when it's there, else systemd-timesyncd.\n\
132 if [ -d /etc/chrony ]; then mkdir -p /etc/chrony/conf.d /etc/chrony/sources.d; printf '%s\\n' {} > /etc/chrony/sources.d/isoloom.sources; systemctl restart chrony 2>/dev/null || systemctl restart chronyd 2>/dev/null || true; \
133 else mkdir -p /etc/systemd/timesyncd.conf.d && printf '[Time]\\nNTP=%s\\n' {} > /etc/systemd/timesyncd.conf.d/isoloom.conf && (systemctl restart systemd-timesyncd 2>/dev/null || true); fi\n",
134 servers.iter().map(|s| quote(&format!("server {s} iburst"))).collect::<Vec<_>>().join(" "),
135 quote(&servers.join(" ")),
136 )
137}
138
139fn upper(k: &str) -> bool {
141 k.chars().all(|ch| ch.is_ascii_uppercase() || ch == '_')
142}
143
144fn proxy(spec: &Spec, c: &Constraints) -> String {
147 let Some(urls) = proxies(c) else { return String::new() };
148 let env = proxy_env(spec, urls);
149 let lines: String = env.iter().map(|(k, v)| format!("{k}={v}\n")).collect::<Vec<_>>().concat();
150 let exports: String = env.iter().map(|(k, v)| format!("export {k}={}\n", quote(v))).collect::<Vec<_>>().concat();
151 let systemd = env
152 .iter()
153 .filter(|(k, _)| upper(k))
154 .map(|(k, v)| format!("\"{k}={v}\""))
155 .collect::<Vec<_>>()
156 .join(" ");
157 format!(
158 "# The proxy, for every login shell, systemd's services and sudo.\n\
159 grep -v -i -E '^(http|https|no)_proxy=' /etc/environment > /etc/environment.isoloom 2>/dev/null || true\n\
160 printf %s {} >> /etc/environment.isoloom && mv /etc/environment.isoloom /etc/environment\n\
161 printf %s {} > /etc/profile.d/isoloom-proxy.sh\n\
162 mkdir -p /etc/systemd/system.conf.d && printf '[Manager]\\nDefaultEnvironment=%s\\n' {} > /etc/systemd/system.conf.d/isoloom-proxy.conf\n\
163 if [ -d /etc/sudoers.d ]; then printf 'Defaults env_keep += \"http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY\"\\n' > /etc/sudoers.d/isoloom-proxy && chmod 0440 /etc/sudoers.d/isoloom-proxy; fi\n{}",
164 quote(&lines),
165 quote(&exports),
166 quote(&systemd),
167 package_proxies(urls, &env),
168 )
169}
170
171fn package_proxies((http, https): (&str, &str), env: &[(String, String)]) -> String {
174 let apt = format!("Acquire::http::Proxy \"{http}\";\nAcquire::https::Proxy \"{https}\";\n");
175 let docker: String = env
176 .iter()
177 .filter(|(k, _)| upper(k))
178 .map(|(k, v)| format!("Environment=\"{k}={v}\"\n"))
179 .collect::<Vec<_>>()
180 .concat();
181 format!(
182 "# apt, dnf/yum and Docker's daemon through it.\n\
183 if [ -d /etc/apt/apt.conf.d ]; then printf %s {} > /etc/apt/apt.conf.d/95isoloom-proxy; fi\n\
184 for f in /etc/dnf/dnf.conf /etc/yum.conf; do if [ -f \"$f\" ]; then sed -i '/^proxy=/d' \"$f\" && printf 'proxy=%s\\n' {} >> \"$f\"; fi; done\n\
185 mkdir -p /etc/systemd/system/docker.service.d && printf '[Service]\\n%s' {} > /etc/systemd/system/docker.service.d/isoloom-proxy.conf\n\
186 systemctl daemon-reload 2>/dev/null || true; if systemctl is-active --quiet docker 2>/dev/null; then systemctl restart docker; fi\n",
187 quote(&apt),
188 quote(http),
189 quote(&docker),
190 )
191}
192
193#[cfg(test)]
194mod tests;