Skip to content

Commit b81944a

Browse files
committed
feat(caddy): add ingress apply and stop commands
1 parent f7689d0 commit b81944a

5 files changed

Lines changed: 377 additions & 4 deletions

File tree

coold/src/grpc/handlers.rs

Lines changed: 245 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use std::sync::Arc;
22

3-
use anyhow::Result;
3+
use anyhow::{anyhow, Context, Result};
44
use futures_util::future::join_all;
55
use tokio::sync::mpsc;
6+
use tokio::{fs, process::Command};
67
use tracing::{debug, warn};
78

89
use crate::builder::BuilderCtx;
910
use crate::grpc::proto::{
10-
client_msg, response, server_msg, BuildResponseBody, ClientMsg, ContainerSummary, Error,
11-
ListContainersResp, Response,
11+
client_msg, response, server_msg, ApplyCaddyIngressResp, BuildResponseBody,
12+
CaddyAppIngressFile, ClientMsg, ContainerSummary, Error, ListContainersResp, Response,
13+
StopCaddyIngressResp,
1214
};
1315
use crate::podman::PodmanClient;
1416

@@ -37,6 +39,40 @@ pub async fn handle(
3739
)
3840
.await;
3941
}
42+
server_msg::Command::ApplyCaddyIngress(req) => {
43+
let body = match apply_caddy_ingress(req.caddyfile, req.apps, req.mesh_network).await {
44+
Ok(output) => response::Body::ApplyCaddyIngress(ApplyCaddyIngressResp { output }),
45+
Err(e) => response::Body::Error(Error {
46+
code: 500,
47+
message: format!("{e:#}"),
48+
}),
49+
};
50+
send_response(
51+
&tx,
52+
Response {
53+
request_id,
54+
body: Some(body),
55+
},
56+
)
57+
.await;
58+
}
59+
server_msg::Command::StopCaddyIngress(_) => {
60+
let body = match stop_caddy_ingress().await {
61+
Ok(output) => response::Body::StopCaddyIngress(StopCaddyIngressResp { output }),
62+
Err(e) => response::Body::Error(Error {
63+
code: 500,
64+
message: format!("{e:#}"),
65+
}),
66+
};
67+
send_response(
68+
&tx,
69+
Response {
70+
request_id,
71+
body: Some(body),
72+
},
73+
)
74+
.await;
75+
}
4076
server_msg::Command::Build(req) => match builder_ctx {
4177
Some(ctx) => ctx.dispatch(request_id, req, tx),
4278
None => {
@@ -80,6 +116,212 @@ async fn send_response(tx: &mpsc::Sender<ClientMsg>, response: Response) {
80116
}
81117
}
82118

119+
async fn apply_caddy_ingress(
120+
caddyfile: String,
121+
apps: Vec<CaddyAppIngressFile>,
122+
mesh_network: String,
123+
) -> Result<String> {
124+
if caddyfile.trim().is_empty() {
125+
return Err(anyhow!("caddyfile is empty"));
126+
}
127+
128+
if caddyfile.len() > 256 * 1024 || apps.iter().any(|app| app.caddyfile.len() > 256 * 1024) {
129+
return Err(anyhow!("caddyfile is too large"));
130+
}
131+
132+
if !is_valid_podman_network_name(&mesh_network) {
133+
return Err(anyhow!("invalid mesh network name"));
134+
}
135+
136+
let base_path = std::path::Path::new("/data/coolify/v5/ingress/caddy");
137+
let apps_path = base_path.join("apps");
138+
let caddyfile_path = base_path.join("Caddyfile");
139+
let temp_caddyfile_path = base_path.join("Caddyfile.tmp");
140+
141+
fs::create_dir_all(&apps_path)
142+
.await
143+
.context("create Caddy app config directory")?;
144+
fs::create_dir_all(base_path.join("data"))
145+
.await
146+
.context("create Caddy data directory")?;
147+
fs::create_dir_all(base_path.join("config"))
148+
.await
149+
.context("create Caddy config directory")?;
150+
151+
let mut expected_files = std::collections::HashSet::new();
152+
for app in apps {
153+
let file_name = caddy_app_file_name(&app.name)?;
154+
expected_files.insert(file_name.clone());
155+
fs::write(apps_path.join(file_name), app.caddyfile)
156+
.await
157+
.context("write Caddy app config")?;
158+
}
159+
160+
let mut entries = fs::read_dir(&apps_path)
161+
.await
162+
.context("read Caddy app config directory")?;
163+
while let Some(entry) = entries
164+
.next_entry()
165+
.await
166+
.context("read Caddy app config")?
167+
{
168+
let file_name = entry.file_name().to_string_lossy().to_string();
169+
if file_name.ends_with(".caddy") && !expected_files.contains(&file_name) {
170+
fs::remove_file(entry.path())
171+
.await
172+
.context("remove stale Caddy app config")?;
173+
}
174+
}
175+
176+
fs::write(&temp_caddyfile_path, caddyfile)
177+
.await
178+
.context("write temporary Caddyfile")?;
179+
180+
run_command(Command::new("podman").args(["pull", "docker.io/library/caddy:2-alpine"]))
181+
.await
182+
.context("pull Caddy image")?;
183+
184+
run_command(Command::new("podman").args([
185+
"run",
186+
"--rm",
187+
"-v",
188+
&format!("{}:/etc/caddy:ro", base_path.display()),
189+
"docker.io/library/caddy:2-alpine",
190+
"caddy",
191+
"validate",
192+
"--config",
193+
"/etc/caddy/Caddyfile.tmp",
194+
]))
195+
.await
196+
.context("validate Caddyfile")?;
197+
198+
fs::rename(&temp_caddyfile_path, &caddyfile_path)
199+
.await
200+
.context("install Caddyfile")?;
201+
202+
start_or_reload_caddy(base_path, &mesh_network).await
203+
}
204+
205+
async fn start_or_reload_caddy(base_path: &std::path::Path, mesh_network: &str) -> Result<String> {
206+
if run_command(Command::new("podman").args(["container", "exists", "coolify-v5-caddy"]))
207+
.await
208+
.is_ok()
209+
{
210+
return run_command(Command::new("podman").args([
211+
"exec",
212+
"coolify-v5-caddy",
213+
"caddy",
214+
"reload",
215+
"--config",
216+
"/etc/caddy/Caddyfile",
217+
]))
218+
.await
219+
.map(|output| {
220+
if output.trim().is_empty() {
221+
"Caddy ingress applied.".into()
222+
} else {
223+
output
224+
}
225+
})
226+
.context("reload Caddy ingress");
227+
}
228+
229+
let output = run_command(Command::new("podman").args([
230+
"run",
231+
"-d",
232+
"--replace",
233+
"--name",
234+
"coolify-v5-caddy",
235+
"--network",
236+
mesh_network,
237+
"--restart",
238+
"unless-stopped",
239+
"-p",
240+
"80:80",
241+
"-p",
242+
"443:443",
243+
"-p",
244+
"443:443/udp",
245+
"-v",
246+
&format!(
247+
"{}:/etc/caddy/Caddyfile:ro",
248+
base_path.join("Caddyfile").display()
249+
),
250+
"-v",
251+
&format!("{}:/etc/caddy/apps:ro", base_path.join("apps").display()),
252+
"-v",
253+
&format!("{}:/data", base_path.join("data").display()),
254+
"-v",
255+
&format!("{}:/config", base_path.join("config").display()),
256+
"docker.io/library/caddy:2-alpine",
257+
]))
258+
.await
259+
.context("start Caddy ingress")?;
260+
261+
Ok(if output.trim().is_empty() {
262+
"Caddy ingress applied.".into()
263+
} else {
264+
output
265+
})
266+
}
267+
268+
fn caddy_app_file_name(value: &str) -> Result<String> {
269+
if value.is_empty()
270+
|| value.len() > 128
271+
|| !value
272+
.bytes()
273+
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
274+
{
275+
return Err(anyhow!("invalid Caddy app file name"));
276+
}
277+
278+
Ok(format!("{value}.caddy"))
279+
}
280+
281+
async fn stop_caddy_ingress() -> Result<String> {
282+
let output = run_command(Command::new("podman").args(["rm", "-f", "coolify-v5-caddy"]))
283+
.await
284+
.context("stop Caddy ingress")?;
285+
286+
Ok(if output.trim().is_empty() {
287+
"Caddy ingress stopped.".into()
288+
} else {
289+
output
290+
})
291+
}
292+
293+
fn is_valid_podman_network_name(value: &str) -> bool {
294+
!value.is_empty()
295+
&& value.len() <= 128
296+
&& value
297+
.bytes()
298+
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
299+
}
300+
301+
async fn run_command(command: &mut Command) -> Result<String> {
302+
let output = command.output().await.context("run command")?;
303+
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
304+
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
305+
let combined = [stdout.as_str(), stderr.as_str()]
306+
.into_iter()
307+
.filter(|part| !part.is_empty())
308+
.collect::<Vec<_>>()
309+
.join("\n");
310+
311+
if !output.status.success() {
312+
return Err(anyhow!(
313+
"{}",
314+
if combined.is_empty() {
315+
format!("command exited with {}", output.status)
316+
} else {
317+
combined
318+
}
319+
));
320+
}
321+
322+
Ok(combined)
323+
}
324+
83325
async fn list_containers(podman: &PodmanClient) -> Result<ListContainersResp> {
84326
let containers = podman.list_containers().await?;
85327

flux/examples/fake_coold.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,34 @@ async fn main() -> Result<()> {
7575
})
7676
.await?;
7777
}
78+
server_msg::Command::ApplyCaddyIngress(_) => {
79+
let resp = Response {
80+
request_id,
81+
body: Some(response::Body::ApplyCaddyIngress(
82+
coolify_proto::agent::v1::ApplyCaddyIngressResp {
83+
output: "Caddy ingress applied.".into(),
84+
},
85+
)),
86+
};
87+
tx.send(ClientMsg {
88+
payload: Some(client_msg::Payload::Response(resp)),
89+
})
90+
.await?;
91+
}
92+
server_msg::Command::StopCaddyIngress(_) => {
93+
let resp = Response {
94+
request_id,
95+
body: Some(response::Body::StopCaddyIngress(
96+
coolify_proto::agent::v1::StopCaddyIngressResp {
97+
output: "Caddy ingress stopped.".into(),
98+
},
99+
)),
100+
};
101+
tx.send(ClientMsg {
102+
payload: Some(client_msg::Payload::Response(resp)),
103+
})
104+
.await?;
105+
}
78106
server_msg::Command::Build(_) | server_msg::Command::CancelBuild(_) => {
79107
// fake_coold does not implement the builder capability.
80108
}

flux/src/envelope.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ pub struct DispatchEnvelope {
3232
#[serde(tag = "type", rename_all = "snake_case")]
3333
pub enum CommandPayload {
3434
ListContainers,
35+
ApplyCaddyIngress {
36+
caddyfile: String,
37+
#[serde(default)]
38+
apps: Vec<CaddyAppIngressFile>,
39+
#[serde(default = "default_caddy_mesh_network")]
40+
mesh_network: String,
41+
},
42+
StopCaddyIngress,
43+
}
44+
45+
#[derive(Debug, Deserialize)]
46+
pub struct CaddyAppIngressFile {
47+
pub name: String,
48+
pub caddyfile: String,
49+
}
50+
51+
fn default_caddy_mesh_network() -> String {
52+
"coolify-default-mesh".into()
3553
}
3654

3755
#[derive(Debug, Serialize)]
@@ -72,6 +90,12 @@ impl ResponseBody {
7290
.unwrap_or(serde_json::Value::Null);
7391
Some(ResponseBody::Ok { data })
7492
}
93+
Some(Body::ApplyCaddyIngress(r)) => Some(ResponseBody::Ok {
94+
data: serde_json::json!({ "output": r.output }),
95+
}),
96+
Some(Body::StopCaddyIngress(r)) => Some(ResponseBody::Ok {
97+
data: serde_json::json!({ "output": r.output }),
98+
}),
7599
Some(Body::Error(e)) => Some(ResponseBody::Error {
76100
code: e.code,
77101
message: e.message,

0 commit comments

Comments
 (0)