Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ jobs:
cp target/release/ldcache_hook "dist/hooks-${{ matrix.arch }}/ldcache_hook-${{ github.ref_name }}"
cp target/release/pce_hook "dist/hooks-${{ matrix.arch }}/pce_hook-${{ github.ref_name }}"
cp target/release/mps_hook "dist/hooks-${{ matrix.arch }}/mps_hook-${{ github.ref_name }}"
cp target/release/sethomevar "dist/hooks-${{ matrix.arch }}/sethomevar-${{ github.ref_name }}"
tar -C dist/hooks-${{ matrix.arch }} -czf "dist/hooks-${{ matrix.arch }}.tar.gz" .
ls -lah target/release/
ls -lah dist/
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["crates/pce_hook", "crates/mps_hook", "crates/ldcache_hook"]
members = ["crates/pce_hook", "crates/mps_hook", "crates/ldcache_hook", "crates/sethomevar"]
resolver = "2"

[profile.release]
Expand Down
10 changes: 10 additions & 0 deletions crates/sethomevar/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "sethomevar"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
users = "0.11.0"
28 changes: 28 additions & 0 deletions crates/sethomevar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Set HOME variable - Precreate Hook

Update container environment replacing HOME variable for running user with the one from the host system.

**What it does**

* Reads the **container config JSON** from `stdin` and emits the updated config to `stdout`.
* Reads running user uid from container config.
* Find HOME host value via getpwuid_r
* Replace HOME entry in container config env
* Pretty-prints output and exits non-zero on validation/parse errors (errors go to `stderr`).

## Usage as a Podman hook

Add a `precreate` hook entry similar to:

```json
{
"version": "1.0.0",
"hook": {
"path": "/opt/hooks/sethomevar"
},
"when": {
"always": true
},
"stages": ["precreate"]
}
```
219 changes: 219 additions & 0 deletions crates/sethomevar/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
use std::{
io::{self, Read, Write},
process,
};

use serde_json::{Map, map::Entry, Value, json};
use users::get_user_by_uid;
use users::os::unix::UserExt;

fn main() -> io::Result<()> {
// we go for run
if let Err(e) = run() {
// we output errors to stderr
eprintln!("{e}");

// we return failure status
process::exit(1);
}

Ok(())
}

fn run() -> Result<(), String> {
// Read and parse stdin JSON
let mut value = read_stdin_json()?;
let obj = ensure_obj(value.as_object_mut(), "top-level JSON must be an object")?;

let env_entries_raw = vec![get_home_env_entry(obj)?];

// Validate env entries and merge as strings
let env_entries = validate_env_strings(env_entries_raw)?;
if !env_entries.is_empty() {
merge_process_env_strings(obj, env_entries)?;
}

// Pretty-print output JSON with trailing newline
let mut stdout = io::stdout().lock();

serde_json::to_writer_pretty(&mut stdout, &value)
.map_err(|e| format!("Failed to write JSON to stdout: {e}"))?;

stdout
.write_all(b"\n")
.map_err(|e| format!("Failed to write newline to stdout: {e}"))?;
stdout
.flush()
.map_err(|e| format!("Failed to flush stdout: {e}"))?;

Ok(())
}

// Returning ENV HOME entry from the system /etc/passwd
// 1. get process.user.uid entry from json obj
// 2. get user entry from uid through getpwuid_r
// 3. get homedir from user entry
// 4. build HOME entry string and return it
fn get_home_env_entry(obj: &mut Map<String, Value>) -> Result<String, String> {

// Ensure "process" exists
let process_val = obj.entry("process".to_string());
match process_val {
Entry::Vacant(_) => return Err(format!("Validation error: 'process' doesn't exist.")),
Entry::Occupied(_) => {},
}

// Ensure "process" is an object
let process_obj = process_val
.or_insert_with(|| json!({}))
.as_object_mut()
.ok_or_else(|| "Validation error: 'process' exists but is not an object.".to_string())?;

// Ensure "user" exists
let user_val = process_obj.entry("user".to_string());
match user_val {
Entry::Vacant(_) => return Err(format!("Validation error: 'process.user' doesn't exist.")),
Entry::Occupied(_) => {},
}

let user_obj = user_val
.or_insert_with(|| json!({}))
.as_object_mut()
.ok_or_else(|| "Validation error: 'process.user' exists but is not an object.".to_string())?;

// Ensure "uid" exists
let uid_val = user_obj.entry("uid".to_string());
match uid_val {
Entry::Vacant(_) => return Err(format!("Validation error: 'process.user.uid' doesn't exist.")),
Entry::Occupied(_) => {},
}

// Ensure "uid" is a number
let uid: u32 = uid_val
.or_insert_with(|| json!(0))
.as_number()
.ok_or_else(|| "Validation error: 'process.user.uid' exists but is not a number.".to_string())?
.as_u64()
.ok_or_else(|| "Validation error: 'process.user.uid' is a number but doesn't fit u64.".to_string())?
.try_into()
.map_err(|e| format!("Validation error: 'process.user.uid' is a number but doesn't fit u32: {e}"))?;

let user = get_user_by_uid(uid)
.ok_or_else(|| "Unknown UID: cannot find User by UID {uid}".to_string())
.unwrap();
let homedir = user.home_dir().display();

let home_env_entry = format!("HOME={homedir}");

Ok(home_env_entry)
}

// Precreate takes as stdin the container config json
// We return error if we cannot read or
// if we cannot parse a valid input json
fn read_stdin_json() -> Result<Value, String> {
let mut input = String::new();

io::stdin()
.read_to_string(&mut input)
.map_err(|e| format!("Failed to read from stdin: {e}"))?;

serde_json::from_str(&input).map_err(|e| format!("Invalid JSON: {e}"))
}

/// Ensure a `Value` is an object and return it as a mutable map.
fn ensure_obj<'a>(
candidate: Option<&'a mut Map<String, Value>>,
err: &str,
) -> Result<&'a mut Map<String, Value>, String> {
candidate.ok_or_else(|| format!("Validation error: {err}."))
}

fn ensure_array_field<'a>(
obj: &'a mut Map<String, Value>,
field: &str,
) -> Result<&'a mut Vec<Value>, String> {
use serde_json::map::Entry;

// before we return the field, we check if the entry is empty/vacant, if so we create the
// field, otherwise, we check it needs to be an array or we return error.
// TODO: can we have non-array env and mounts?
match obj.entry(field.to_string()) {
Entry::Vacant(v) => {
// Insert an empty array and return a mutable ref to it.
let val = v.insert(Value::Array(Vec::new()));
Ok(val.as_array_mut().expect("we just inserted an Array"))
}
Entry::Occupied(e) => {
// Tie the borrow to `obj` by consuming the entry.
let v = e.into_mut(); // &'a mut Value
match v {
Value::Array(arr) => Ok(arr),
_ => Err(format!(
"Validation error: '{field}' exists but is not an array."
)),
}
}
}
}

/// Validate a list of "KEY=value" strings.
fn validate_env_strings(entries: Vec<String>) -> Result<Vec<String>, String> {
for s in &entries {
validate_kv_format(s)?;
}

Ok(entries)
}

fn validate_kv_format(s: &str) -> Result<(), String> {
if let Some((k, _v)) = s.split_once('=') {
if k.is_empty() {
return Err("Empty environment variable name before '='".into());
}
Ok(())
} else {
Err(format!("Invalid env entry (expected KEY=VALUE): {s}"))
}
}

// merging envs into the container config json is as follows
// 1. we need to add envs into the process object
// 1.5 we create process if it is not there
// 2. we validate out envs
// 3 new env entries are added using two rules
// 3.1 we append if the env var is new
// 3.2 we replace if we find it duplicated
fn merge_process_env_strings(
obj: &mut Map<String, Value>,
env_entries: Vec<String>,
) -> Result<(), String> {
// Ensure "process" is an object
let process_val = obj
.entry("process".to_string())
.or_insert_with(|| json!({}));
let process_obj = process_val
.as_object_mut()
.ok_or_else(|| "Validation error: 'process' exists but is not an object.".to_string())?;

let env_arr = ensure_array_field(process_obj, "env")?;

// logic to add new envs
for new in env_entries {
// Safe: already validated as KEY=value in main
let (new_key, _) = new.split_once('=').unwrap();

// We scan to find if we have a duplicate, if so we overwrite with new
if let Some(idx) = env_arr.iter().rposition(|v| {
v.as_str()
.and_then(|s| s.split_once('=').map(|(k, _)| k))
.is_some_and(|k| k == new_key)
}) {
env_arr[idx] = Value::String(new);
} else {
env_arr.push(Value::String(new));
}
}

Ok(())
}