Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,7 @@ fn apply_contexts(

/// Build merged context chain from root down to the given dir.
/// Returns one DirContext per language group, with child values winning over parent.
fn build_ancestor_chain(
dir: &str,
contexts: &HashMap<String, Vec<DirContext>>,
) -> Vec<DirContext> {
fn build_ancestor_chain(dir: &str, contexts: &HashMap<String, Vec<DirContext>>) -> Vec<DirContext> {
let normalized = normalize_dir(dir);
let mut ancestor_dirs: Vec<String> = Vec::new();

Expand Down Expand Up @@ -456,11 +453,14 @@ fn build_ancestor_chain(
for dir_key in &ancestor_dirs {
if let Some(dir_contexts) = contexts.get(dir_key) {
for ctx in dir_contexts {
if let Some(existing) = result.iter_mut().find(|c| match (c.language, ctx.language)
if let Some(existing) =
result
.iter_mut()
.find(|c| match (c.language, ctx.language) {
(Some(a), Some(b)) => a.same_group(b),
(Some(_), None) | (None, Some(_)) | (None, None) => true,
})
{
(Some(a), Some(b)) => a.same_group(b),
(Some(_), None) | (None, Some(_)) | (None, None) => true,
}) {
// Child overrides parent: child keeps its values, fills gaps from parent
let mut child = ctx.clone();
child.merge(existing);
Expand Down
27 changes: 19 additions & 8 deletions src/signals/docker_compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,15 @@ fn parse_memory(s: &str) -> Option<u32> {
.map(|n| (n * multiplier as f64).ceil() as u32)
}

fn parse_healthcheck(hc: &HealthcheckConfig) -> Option<String> {
fn parse_healthcheck(hc: &HealthcheckConfig) -> Option<Healthcheck> {
let test = hc.test.as_ref()?;
match test {
StringOrArray::String(s) => {
let s = s.trim();
if s.eq_ignore_ascii_case("NONE") {
None
} else {
Some(s.to_string())
Some(Healthcheck::Command(s.to_string()))
}
}
StringOrArray::Array(arr) => {
Expand All @@ -225,7 +225,11 @@ fn parse_healthcheck(hc: &HealthcheckConfig) -> Option<String> {
0
};
let cmd = arr[start..].join(" ");
if cmd.is_empty() { None } else { Some(cmd) }
if cmd.is_empty() {
None
} else {
Some(Healthcheck::Command(cmd))
}
}
}
}
Expand Down Expand Up @@ -923,8 +927,10 @@ services:
"#,
)]);
assert_eq!(
services[0].healthcheck.as_deref(),
Some("curl -f http://localhost:3000/health")
services[0].healthcheck,
Some(Healthcheck::Command(
"curl -f http://localhost:3000/health".into()
))
);
}

Expand All @@ -941,8 +947,10 @@ services:
"#,
)]);
assert_eq!(
services[0].healthcheck.as_deref(),
Some("curl -f http://localhost:3000/health")
services[0].healthcheck,
Some(Healthcheck::Command(
"curl -f http://localhost:3000/health".into()
))
);
}

Expand All @@ -958,7 +966,10 @@ services:
test: ["CMD-SHELL", "pg_isready"]
"#,
)]);
assert_eq!(services[0].healthcheck.as_deref(), Some("pg_isready"));
assert_eq!(
services[0].healthcheck,
Some(Healthcheck::Command("pg_isready".into()))
);
}

// -- Skip variants --
Expand Down
59 changes: 46 additions & 13 deletions src/signals/dockerfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,17 @@ fn parse_dockerfile(text: &str) -> ParsedDockerfile {
let args = misc.arguments.to_string();
let args = args.trim();
if !args.eq_ignore_ascii_case("NONE") {
// Strip --interval/--timeout/etc. flags, keep CMD portion
if let Some(cmd_pos) = args.find("CMD") {
let cmd_part = args[cmd_pos + 3..].trim();
if !cmd_part.is_empty() {
parsed.healthcheck = Some(cmd_part.to_string());
}
// Strip --interval/--timeout/etc. flags, keep CMD portion.
// Check CMD-SHELL before CMD to avoid matching the substring.
let cmd_part = if let Some(pos) = args.find("CMD-SHELL") {
args[pos + 9..].trim()
} else if let Some(pos) = args.find("CMD") {
args[pos + 3..].trim()
} else {
""
};
if !cmd_part.is_empty() {
parsed.healthcheck = Some(cmd_part.to_string());
}
}
}
Expand Down Expand Up @@ -336,10 +341,9 @@ impl Signal for DockerfileSignal {
let (runtimes, start_ctx) = if let Some(lang) = language {
let rt = Runtime {
language: lang,
name: runtime.as_ref().map_or_else(
|| default_runtime_name(lang).into(),
|ri| ri.name.clone(),
),
name: runtime
.as_ref()
.map_or_else(|| default_runtime_name(lang).into(), |ri| ri.name.clone()),
version: runtime.as_ref().and_then(|ri| ri.version.clone()),
version_source: runtime.as_ref().and_then(|ri| ri.source.clone()),
package_manager: None,
Expand Down Expand Up @@ -384,7 +388,7 @@ impl Signal for DockerfileSignal {
None
},
exec_mode: Some(ExecMode::Daemon),
healthcheck: parsed.healthcheck,
healthcheck: parsed.healthcheck.map(Healthcheck::Command),
volumes,
detected_by: vec![dockerfile_path],
..Service::default()
Expand Down Expand Up @@ -823,8 +827,10 @@ CMD node app.js"#,
b"FROM node:20\nHEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:3000/health\nCMD node app.js",
)]);
assert_eq!(
services[0].healthcheck.as_deref(),
Some("curl -f http://localhost:3000/health")
services[0].healthcheck,
Some(Healthcheck::Command(
"curl -f http://localhost:3000/health".into()
))
);
}

Expand All @@ -836,4 +842,31 @@ CMD node app.js"#,
)]);
assert!(services[0].healthcheck.is_none());
}

#[test]
fn healthcheck_cmd_shell_with_flags() {
// Regression: CMD-SHELL must not be parsed as CMD + "-SHELL ..."
let services = run_signal(&[(
"Dockerfile",
b"FROM node:20\nHEALTHCHECK --interval=30s --timeout=5s CMD-SHELL curl -f http://localhost/health\nCMD node app.js",
)]);
assert_eq!(
services[0].healthcheck,
Some(Healthcheck::Command(
"curl -f http://localhost/health".into()
))
);
}

#[test]
fn healthcheck_cmd_shell_no_flags() {
let services = run_signal(&[(
"Dockerfile",
b"FROM node:20\nHEALTHCHECK CMD-SHELL pg_isready\nCMD node app.js",
)]);
assert_eq!(
services[0].healthcheck,
Some(Healthcheck::Command("pg_isready".into()))
);
}
}
51 changes: 51 additions & 0 deletions src/signals/fly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ struct FlyBuild {
struct FlyHttpService {
min_machines_running: Option<u32>,
processes: Option<Vec<String>>,
#[serde(default)]
checks: Vec<FlyHttpCheck>,
}

#[derive(Deserialize)]
struct FlyHttpCheck {
path: Option<String>,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -133,6 +140,14 @@ impl Signal for FlySignal {
.and_then(|h| h.min_machines_running)
.filter(|&r| r > 0);

// Health check path from http_service.checks
let healthcheck = config
.http_service
.as_ref()
.and_then(|h| h.checks.first())
.and_then(|c| c.path.clone())
.map(Healthcheck::Path);

// Dockerfile
let dockerfile = config.build.as_ref().and_then(|b| b.dockerfile.clone());

Expand Down Expand Up @@ -188,6 +203,7 @@ impl Signal for FlySignal {
dockerfile: dockerfile.clone(),
resources: resources.clone(),
replicas,
healthcheck: healthcheck.clone(),
env: env.clone(),
volumes,
detected_by: vec!["fly".into()],
Expand Down Expand Up @@ -218,6 +234,7 @@ impl Signal for FlySignal {
dockerfile,
resources,
replicas,
healthcheck,
env,
volumes,
detected_by: vec!["fly".into()],
Expand Down Expand Up @@ -749,4 +766,38 @@ app = "my-api"
assert_eq!(svcs.len(), 1);
assert_eq!(svcs[0].name, "my-api");
}

#[test]
fn http_service_healthcheck() {
let svcs = discover(&[(
"fly.toml",
br#"
app = "myapp"

[http_service]
[[http_service.checks]]
path = "/health"
interval = "10s"
timeout = "2s"
"#,
)]);
assert_eq!(
svcs[0].healthcheck,
Some(Healthcheck::Path("/health".into()))
);
}

#[test]
fn http_service_no_healthcheck() {
let svcs = discover(&[(
"fly.toml",
br#"
app = "myapp"

[http_service]
min_machines_running = 1
"#,
)]);
assert!(svcs[0].healthcheck.is_none());
}
}
40 changes: 8 additions & 32 deletions src/signals/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,7 @@ mod tests {
let svc = &disc.services[0];
assert_eq!(svc.framework(), Some("nuxt"));
assert_eq!(svc.build(), Some("nuxt build"));
assert_eq!(
svc.start(),
Some("node .output/server/index.mjs")
);
assert_eq!(svc.start(), Some("node .output/server/index.mjs"));
assert_eq!(svc.dev(), Some("nuxt dev"));
}

Expand Down Expand Up @@ -280,14 +277,8 @@ mod tests {
Some("python manage.py collectstatic --noinput")
);
// Package's python module sets start and dev (registered before framework)
assert_eq!(
svc.start(),
Some("python manage.py runserver")
);
assert_eq!(
svc.dev(),
Some("python manage.py runserver")
);
assert_eq!(svc.start(), Some("python manage.py runserver"));
assert_eq!(svc.dev(), Some("python manage.py runserver"));
}

// -- Rails --
Expand All @@ -302,19 +293,13 @@ mod tests {
assert_eq!(svc.framework(), Some("rails"));
assert_eq!(svc.dir, ".");
// Framework fills build (Package's ruby module only sets build when asset pipeline is present)
assert_eq!(
svc.build(),
Some("rails assets:precompile")
);
assert_eq!(svc.build(), Some("rails assets:precompile"));
// Package's ruby module sets start and dev (registered before framework)
assert_eq!(
svc.start(),
Some("bundle exec bin/rails server -b 0.0.0.0 -p ${PORT:-3000} -e $RAILS_ENV")
);
assert_eq!(
svc.dev(),
Some("bundle exec bin/rails server")
);
assert_eq!(svc.dev(), Some("bundle exec bin/rails server"));
}

#[test]
Expand Down Expand Up @@ -345,10 +330,7 @@ mod tests {
assert_eq!(svc.framework(), Some("laravel"));
assert!(svc.build().is_none());
// Package's php module sets start with --host flag (registered before framework)
assert_eq!(
svc.start(),
Some("php artisan serve --host=0.0.0.0")
);
assert_eq!(svc.start(), Some("php artisan serve --host=0.0.0.0"));
assert_eq!(svc.dev(), Some("php artisan serve"));
}

Expand Down Expand Up @@ -513,10 +495,7 @@ mod tests {
assert_eq!(svc.framework(), Some("symfony"));
assert!(svc.build().is_none());
// Package's php module sets start (registered before framework)
assert_eq!(
svc.start(),
Some("php -S 0.0.0.0:8000 -t public")
);
assert_eq!(svc.start(), Some("php -S 0.0.0.0:8000 -t public"));
}

#[test]
Expand Down Expand Up @@ -579,10 +558,7 @@ mod tests {
assert_eq!(svc.framework(), Some("pelican"));
assert_eq!(svc.build(), Some("pelican content"));
assert_eq!(svc.start(), Some("pelican --listen"));
assert_eq!(
svc.dev(),
Some("pelican --listen --autoreload")
);
assert_eq!(svc.dev(), Some("pelican --listen --autoreload"));
}

#[test]
Expand Down
Loading
Loading