Skip to content

Commit 6f7f557

Browse files
authored
Merge pull request #71 from nao1215/feat/issues-62-65-66
feat: configurable keep-alive, validate subcommand, enhanced health checks
2 parents f899544 + 3d85cb1 commit 6f7f557

File tree

8 files changed

+663
-13
lines changed

8 files changed

+663
-13
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ categories = ["command-line-utilities", "multimedia::images", "web-programming::
1313
[features]
1414
default = ["cli", "server", "svg", "webp-lossy"]
1515
cli = ["dep:clap", "dep:clap_complete", "dep:ureq", "server"]
16-
server = ["dep:hmac", "dep:hex", "dep:serde_json", "dep:sha2", "dep:subtle", "dep:ureq", "dep:url", "dep:uuid", "svg"]
16+
server = ["dep:hmac", "dep:hex", "dep:serde_json", "dep:sha2", "dep:subtle", "dep:ureq", "dep:url", "dep:uuid", "dep:libc", "svg"]
1717
s3 = ["server", "dep:aws-sdk-s3", "dep:aws-config", "dep:tokio"]
1818
gcs = ["server", "dep:google-cloud-storage", "dep:google-cloud-auth", "dep:tokio"]
1919
azure = ["server", "dep:azure_storage_blob", "dep:azure_core", "dep:futures", "dep:tokio"]
@@ -60,6 +60,7 @@ google-cloud-auth = { version = "1", optional = true }
6060
azure_storage_blob = { version = "0.9", optional = true }
6161
azure_core = { version = "0.32", optional = true }
6262
futures = { version = "0.3", optional = true }
63+
libc = { version = "0.2", optional = true }
6364

6465
[profile.release]
6566
lto = "thin"

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ See the [Docker](#docker) section for running with Docker instead.
203203
| `convert` | Convert and transform an image file (can be omitted; see above) |
204204
| `inspect` | Show metadata (format, dimensions, alpha) of an image |
205205
| `serve` | Start the HTTP image-transform server (implied when server flags are used at the top level) |
206+
| `validate` | Validate server configuration without starting the server (useful in CI/CD) |
206207
| `sign` | Generate a signed public URL for the server |
207208
| `completions` | Generate shell completion scripts |
208209
| `version` | Print version information |
@@ -246,6 +247,12 @@ By default, the server listens on `127.0.0.1:8080`. Configuration can be supplie
246247
truss serve --bind 0.0.0.0:8080 --storage-root /var/images
247248
```
248249

250+
To validate the server configuration without starting the server (useful in CI/CD pipelines):
251+
252+
```sh
253+
truss validate
254+
```
255+
249256
### Core settings
250257

251258
| Variable | Description |
@@ -259,6 +266,9 @@ truss serve --bind 0.0.0.0:8080 --storage-root /var/images
259266
| `TRUSS_MAX_INPUT_PIXELS` | Max input image pixels before decode; excess images receive 422 (default: `40000000`, range: 1-100000000) |
260267
| `TRUSS_MAX_UPLOAD_BYTES` | Max upload body size in bytes; excess requests receive 413 (default: `104857600` = 100 MB, range: 1-10737418240) |
261268
| `TRUSS_STORAGE_TIMEOUT_SECS` | Download timeout for object storage backends in seconds (default: `30`, range: 1-300) |
269+
| `TRUSS_KEEP_ALIVE_MAX_REQUESTS` | Max requests per keep-alive connection before the server closes it (default: `100`, range: 1-100000) |
270+
| `TRUSS_HEALTH_CACHE_MIN_FREE_BYTES` | Minimum free bytes on cache disk; `/health/ready` returns 503 when breached (disabled by default) |
271+
| `TRUSS_HEALTH_MAX_MEMORY_BYTES` | Maximum process RSS in bytes; `/health/ready` returns 503 when breached (disabled by default, Linux only) |
262272

263273
`TRUSS_STORAGE_BACKEND` selects the source for public `GET /images/by-path`. When set to `s3`, `gcs`, or `azure`, the `path` query parameter is used as the object key. Only one backend can be active at a time. Private endpoints can still use `kind: storage` regardless of this setting.
264274

ROADMAP.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Roadmap
2+
3+
Items planned for future releases, roughly in priority order.
4+
5+
## Health Check Hardening
6+
7+
- [#72 — Add hysteresis to readiness probe resource checks to prevent flapping](https://github.com/nao1215/truss/issues/72)
8+
- [#73 — Consider authentication for /health diagnostic endpoint](https://github.com/nao1215/truss/issues/73)
9+
- [#74 — Cache syscall results in health check endpoints](https://github.com/nao1215/truss/issues/74)

doc/openapi.yaml

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,18 @@ paths:
348348
checks:
349349
- name: storageRoot
350350
status: ok
351+
- name: cacheDiskFree
352+
status: ok
353+
freeBytes: 53687091200
354+
thresholdBytes: 1073741824
351355
- name: transformCapacity
352356
status: ok
357+
current: 2
358+
max: 64
359+
- name: memoryUsage
360+
status: ok
361+
rssBytes: 134217728
362+
thresholdBytes: 536870912
353363
maxInputPixels: 40000000
354364

355365
/health/live:
@@ -382,8 +392,17 @@ paths:
382392
Readiness probe for container orchestrators (e.g. Kubernetes
383393
`readinessProbe`). Checks that the server is able to accept traffic by
384394
verifying the storage root is accessible, the cache root (if configured)
385-
is accessible, and the object-storage backend (if configured) is
386-
network-reachable.
395+
is accessible, the object-storage backend (if configured) is
396+
network-reachable, cache disk free space is above the configured
397+
threshold (`TRUSS_HEALTH_CACHE_MIN_FREE_BYTES`), process memory
398+
usage is below the configured limit (`TRUSS_HEALTH_MAX_MEMORY_BYTES`),
399+
and transform capacity is not exhausted.
400+
401+
**Platform note:** RSS memory and cache disk free space are read on
402+
Linux only. On other platforms `TRUSS_HEALTH_MAX_MEMORY_BYTES` and
403+
`TRUSS_HEALTH_CACHE_MIN_FREE_BYTES` are effectively no-ops — those
404+
checks are omitted from the response and the remaining checks
405+
(object-storage reachability, transform capacity, etc.) still apply.
387406
388407
The object-storage reachability check (S3, GCS, or Azure) confirms
389408
that the configured bucket/container exists and the service responds to
@@ -1150,6 +1169,33 @@ components:
11501169
status:
11511170
type: string
11521171
enum: [ok, fail]
1172+
freeBytes:
1173+
type: integer
1174+
format: int64
1175+
minimum: 0
1176+
description: Free bytes on the cache disk (cacheDiskFree check only).
1177+
thresholdBytes:
1178+
type: integer
1179+
format: int64
1180+
minimum: 1
1181+
description: >-
1182+
Configured threshold in bytes. Present on cacheDiskFree and
1183+
memoryUsage checks when a threshold is configured.
1184+
rssBytes:
1185+
type: integer
1186+
format: int64
1187+
minimum: 0
1188+
description: Process resident set size in bytes (memoryUsage check only, Linux).
1189+
current:
1190+
type: integer
1191+
format: int64
1192+
minimum: 0
1193+
description: Current number of in-flight transforms (transformCapacity check only).
1194+
max:
1195+
type: integer
1196+
format: int64
1197+
minimum: 1
1198+
description: Maximum concurrent transforms allowed (transformCapacity check only).
11531199
ProblemDetail:
11541200
type: object
11551201
description: RFC 7807 Problem Details

src/adapters/cli.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ COMMANDS:
5151
convert Convert and transform an image file
5252
inspect Show metadata (format, dimensions, alpha) of an image
5353
serve Start the HTTP image-transform server
54+
validate Check server configuration without starting the server
5455
sign Generate a signed public URL for the server
5556
completions Generate shell completion scripts
5657
help Show help for a command (e.g. truss help convert)
@@ -299,6 +300,19 @@ EXAMPLES:
299300
--expires 1700000000 --width 640 --format webp
300301
";
301302

303+
const HELP_VALIDATE: &str = "\
304+
truss validate - check server configuration without starting the server
305+
306+
USAGE:
307+
truss validate
308+
309+
Parses and validates all environment variables used by `truss serve`.
310+
Exits 0 when the configuration is valid, or exits 1 with a description
311+
of each error found.
312+
313+
Useful in CI/CD pipelines to catch configuration mistakes early.
314+
";
315+
302316
const HELP_COMPLETIONS: &str = "\
303317
truss completions - generate shell completion scripts
304318
@@ -358,6 +372,9 @@ enum CliSubcommand {
358372
Help { topic: Option<String> },
359373
/// Print version information
360374
Version,
375+
/// Validate server configuration without starting the server
376+
#[command(disable_help_flag = true)]
377+
Validate(ClapValidateArgs),
361378
/// Generate shell completion scripts
362379
#[command(disable_help_flag = true)]
363380
Completions {
@@ -483,6 +500,13 @@ struct ClapServeArgs {
483500
help: bool,
484501
}
485502

503+
#[derive(clap::Args)]
504+
struct ClapValidateArgs {
505+
/// Show help for validate
506+
#[arg(short = 'h', long = "help")]
507+
help: bool,
508+
}
509+
486510
#[derive(clap::Args)]
487511
struct ClapSignArgs {
488512
/// CDN base URL for the signed request
@@ -716,6 +740,7 @@ enum HelpTopic {
716740
Convert,
717741
Inspect,
718742
Serve,
743+
Validate,
719744
Sign,
720745
Completions,
721746
Version,
@@ -726,6 +751,7 @@ enum Command {
726751
Help(HelpTopic),
727752
Version,
728753
Serve(ServeCommand),
754+
Validate,
729755
Inspect(InspectCommand),
730756
Convert(ConvertCommand),
731757
Sign(SignCommand),
@@ -816,6 +842,7 @@ where
816842
HelpTopic::Convert => HELP_CONVERT.to_string(),
817843
HelpTopic::Inspect => HELP_INSPECT.to_string(),
818844
HelpTopic::Serve => help_serve(),
845+
HelpTopic::Validate => HELP_VALIDATE.to_string(),
819846
HelpTopic::Sign => HELP_SIGN.to_string(),
820847
HelpTopic::Completions => HELP_COMPLETIONS.to_string(),
821848
HelpTopic::Version => HELP_VERSION.to_string(),
@@ -833,6 +860,10 @@ where
833860
Ok(()) => EXIT_SUCCESS,
834861
Err(error) => write_error(stderr, error),
835862
},
863+
Ok(Command::Validate) => match execute_validate(stdout) {
864+
Ok(()) => EXIT_SUCCESS,
865+
Err(error) => write_error(stderr, error),
866+
},
836867
Ok(Command::Inspect(command)) => match execute_inspect(command, stdin, stdout) {
837868
Ok(()) => EXIT_SUCCESS,
838869
Err(error) => write_error(stderr, error),
@@ -861,6 +892,7 @@ const KNOWN_SUBCOMMANDS: &[&str] = &[
861892
"convert",
862893
"inspect",
863894
"serve",
895+
"validate",
864896
"sign",
865897
"help",
866898
"completions",
@@ -989,6 +1021,7 @@ where
9891021
Some(CliSubcommand::Convert(args)) => convert_from_clap(args),
9901022
Some(CliSubcommand::Inspect(args)) => inspect_from_clap(args),
9911023
Some(CliSubcommand::Serve(args)) => serve_from_clap(args),
1024+
Some(CliSubcommand::Validate(args)) => validate_from_clap(args),
9921025
Some(CliSubcommand::Sign(args)) => sign_from_clap(args),
9931026
}
9941027
}
@@ -1017,6 +1050,7 @@ fn parse_help_topic(topic: Option<String>) -> Result<Command, CliError> {
10171050
Some("convert") => Ok(Command::Help(HelpTopic::Convert)),
10181051
Some("inspect") => Ok(Command::Help(HelpTopic::Inspect)),
10191052
Some("serve") => Ok(Command::Help(HelpTopic::Serve)),
1053+
Some("validate") => Ok(Command::Help(HelpTopic::Validate)),
10201054
Some("sign") => Ok(Command::Help(HelpTopic::Sign)),
10211055
Some("completions") => Ok(Command::Help(HelpTopic::Completions)),
10221056
Some("version") => Ok(Command::Help(HelpTopic::Version)),
@@ -1025,7 +1059,8 @@ fn parse_help_topic(topic: Option<String>) -> Result<Command, CliError> {
10251059
message: format!("unknown help topic '{other}'"),
10261060
usage: None,
10271061
hint: Some(
1028-
"available topics: convert, inspect, serve, sign, completions, version".to_string(),
1062+
"available topics: convert, inspect, serve, validate, sign, completions, version"
1063+
.to_string(),
10291064
),
10301065
}),
10311066
}
@@ -1173,6 +1208,13 @@ fn serve_from_clap(args: ClapServeArgs) -> Result<Command, CliError> {
11731208
}))
11741209
}
11751210

1211+
fn validate_from_clap(args: ClapValidateArgs) -> Result<Command, CliError> {
1212+
if args.help {
1213+
return Ok(Command::Help(HelpTopic::Validate));
1214+
}
1215+
Ok(Command::Validate)
1216+
}
1217+
11761218
fn sign_from_clap(args: ClapSignArgs) -> Result<Command, CliError> {
11771219
if args.help {
11781220
return Ok(Command::Help(HelpTopic::Sign));
@@ -1446,6 +1488,24 @@ fn execute_serve(command: ServeCommand) -> Result<(), CliError> {
14461488
.map_err(|error| runtime_error(EXIT_RUNTIME, &format!("server runtime failed: {error}")))
14471489
}
14481490

1491+
fn execute_validate<W: Write>(stdout: &mut W) -> Result<(), CliError> {
1492+
match ServerConfig::from_env() {
1493+
Ok(config) => {
1494+
writeln!(stdout, "configuration is valid").map_err(|error| {
1495+
runtime_error(EXIT_RUNTIME, &format!("failed to write stdout: {error}"))
1496+
})?;
1497+
writeln!(stdout, " storage root: {}", config.storage_root.display()).map_err(
1498+
|error| runtime_error(EXIT_RUNTIME, &format!("failed to write stdout: {error}")),
1499+
)?;
1500+
Ok(())
1501+
}
1502+
Err(error) => Err(runtime_error(
1503+
EXIT_USAGE,
1504+
&format!("invalid configuration: {error}"),
1505+
)),
1506+
}
1507+
}
1508+
14491509
fn resolve_server_config(command: ServeCommand) -> Result<ServerConfig, CliError> {
14501510
let mut config = ServerConfig::from_env().map_err(|error| {
14511511
runtime_error(
@@ -1843,6 +1903,7 @@ mod tests {
18431903
use crate::{Fit, MediaType, RawArtifact, SignedUrlSource, TransformOptions, sniff_artifact};
18441904
use image::codecs::png::PngEncoder;
18451905
use image::{ColorType, ImageEncoder, Rgba, RgbaImage};
1906+
use serial_test::serial;
18461907
use std::env;
18471908
use std::fs;
18481909
use std::io::{Cursor, Read, Write};
@@ -2382,6 +2443,61 @@ mod tests {
23822443
assert_eq!(result.unwrap(), Command::Help(HelpTopic::Serve));
23832444
}
23842445

2446+
// ===== Additional test: help validate =====
2447+
2448+
#[test]
2449+
fn help_validate_shows_validate_help() {
2450+
let result = parse_args(vec![
2451+
"truss".to_string(),
2452+
"help".to_string(),
2453+
"validate".to_string(),
2454+
]);
2455+
assert_eq!(result.unwrap(), Command::Help(HelpTopic::Validate));
2456+
}
2457+
2458+
#[test]
2459+
fn parse_args_validate() {
2460+
let result =
2461+
parse_args(vec!["truss".to_string(), "validate".to_string()]).expect("parse validate");
2462+
assert_eq!(result, Command::Validate);
2463+
}
2464+
2465+
#[test]
2466+
fn validate_help_flag() {
2467+
let result = parse_args(vec![
2468+
"truss".to_string(),
2469+
"validate".to_string(),
2470+
"--help".to_string(),
2471+
]);
2472+
assert_eq!(result.unwrap(), Command::Help(HelpTopic::Validate));
2473+
}
2474+
2475+
#[test]
2476+
#[serial]
2477+
fn validate_invalid_config() {
2478+
// SAFETY: test-only, single-threaded access to this env var.
2479+
unsafe { env::set_var("TRUSS_MAX_CONCURRENT_TRANSFORMS", "invalid") };
2480+
let mut stdout = Vec::new();
2481+
let result = super::execute_validate(&mut stdout);
2482+
unsafe { env::remove_var("TRUSS_MAX_CONCURRENT_TRANSFORMS") };
2483+
assert!(result.is_err());
2484+
}
2485+
2486+
#[test]
2487+
#[serial]
2488+
fn validate_valid_config() {
2489+
let dir = tempfile::tempdir().expect("create temp dir");
2490+
let mut stdout = Vec::new();
2491+
// SAFETY: test-only, single-threaded access to this env var.
2492+
unsafe { env::set_var("TRUSS_STORAGE_ROOT", dir.path().to_str().unwrap()) };
2493+
let result = super::execute_validate(&mut stdout);
2494+
unsafe { env::remove_var("TRUSS_STORAGE_ROOT") };
2495+
assert!(result.is_ok());
2496+
let output = String::from_utf8(stdout).expect("valid utf-8");
2497+
assert!(output.contains("configuration is valid"));
2498+
assert!(output.contains("storage root:"));
2499+
}
2500+
23852501
// ===== Additional test: help sign =====
23862502

23872503
#[test]

0 commit comments

Comments
 (0)