Compare commits

...

21 Commits

Author SHA1 Message Date
1b88c80669 chore: update tasks 2025-05-17 15:16:11 +00:00
af579d1a7f chore: remove access and authorized_keys files from project 2025-05-17 15:15:51 +00:00
e6fff0c044 feat: add validate_command_access.sh with its tests 2025-05-17 15:12:18 +00:00
ba052d78d4 feat: add gitops ability to update authorized_keys 2025-05-17 15:10:44 +00:00
bc1cbfc772 chore: change repo for validate_command and gitconfig to workspaces 2025-05-17 15:10:08 +00:00
f8b38996df feat: add package go-yq to build image 2025-05-17 14:43:42 +00:00
d488a87dd4 feat: upgrade gitops ability to fetch files from different repos 2025-05-17 14:42:06 +00:00
3e361cd03c feat: separate access validation logic for gitops commands into a separate file 2025-05-17 14:38:56 +00:00
fd0c07e954 feat: remove buildah scripts and adapt project to use Containerfile 2025-05-17 13:21:36 +00:00
d3f5e93ad8 feat: add containerfile for our builds 2025-05-17 13:12:00 +00:00
c9460b8ebc chore: update command access for pallav 2025-05-17 11:37:50 +00:00
9629c3253e fix: validate commands, improve remove container logic, standardize logs in gitops router 2025-05-17 11:37:06 +00:00
6e11d19510 feat: update authorized_keys file 2025-05-17 10:04:00 +00:00
a4bfe5a5c0 feat: use %h instead of hard coded paths in authorized_keys 2025-05-17 09:44:31 +00:00
54a42ad4d5 fix: use $HOME variable in place of hard coded path values 2025-05-17 09:39:07 +00:00
21eee8c3ec feat: remove access to host podman socket 2025-05-17 09:35:48 +00:00
ff72c95012 style: beatify and optimize ssh router with chatgpt 2025-05-17 09:21:48 +00:00
3576bf93c2 style: beautify and optimize gitops router with chatgpt 2025-05-17 09:15:50 +00:00
d7c7686a9e feat: generate gitconfig on the fly before creating container 2025-05-17 09:04:26 +00:00
a179a3ad23 chore: add task to copy gitconfig.template 2025-05-17 09:02:58 +00:00
56744155cb feat: add gitops function to update gitconfig template 2025-05-17 08:59:58 +00:00
10 changed files with 502 additions and 306 deletions

52
.vscode/tasks.json vendored
View File

@ -17,6 +17,14 @@
"problemMatcher": [],
"detail": "build podman image using buildah"
},
{
"label": "GitOps(Build): all images",
"type": "shell",
"command": ".bin/gitops build all",
"group": "build",
"problemMatcher": [],
"detail": "build podman image using buildah"
},
{
"label": "GitOps: Clean dangling images",
"type": "shell",
@ -25,20 +33,12 @@
"detail": "Clean podman images"
},
{
"label": "Gitops(Update): build-base.sh",
"label": "GitOps(Update): Containerfile",
"type": "shell",
"command": ".bin/gitops update base",
"command": ".bin/gitops update containerfile",
"group": "build",
"problemMatcher": [],
"detail": "Copy build-base.sh to /home/infilytics/.local/bin/"
},
{
"label": "GitOps(Update): build-workspace.sh",
"type": "shell",
"command": ".bin/gitops update workspace",
"group": "build",
"problemMatcher": [],
"detail": "Copy build-workspace.sh to /home/infilytics/.local/bin/"
"detail": "Copy Containerfile to $HOME/"
},
{
"label": "GitOps(Update): ssh_router.sh",
@ -46,15 +46,7 @@
"command": ".bin/gitops update ssh_router",
"group": "build",
"problemMatcher": [],
"detail": "Copy ssh_router.sh to /home/infilytics/.local/bin/"
},
{
"label": "GitOps(Update): access.yml",
"type": "shell",
"command": ".bin/gitops update access",
"group": "build",
"problemMatcher": [],
"detail": "Copy access.yml to /home/infilytics/"
"detail": "Copy ssh_router.sh to $HOME/.local/bin/"
},
{
"label": "GitOps(Update): gitops_router.sh",
@ -62,7 +54,7 @@
"command": ".bin/gitops update gitops_router",
"group": "build",
"problemMatcher": [],
"detail": "Copy gitops_router.sh to /home/infilytics/.local/bin"
"detail": "Copy gitops_router.sh to $HOME/.local/bin"
},
{
"label": "GitOps(Update): home.tar.gz",
@ -70,7 +62,23 @@
"command": ".bin/gitops update home_tar",
"group": "build",
"problemMatcher": [],
"detail": "Copy home.tar.gz to /home/infilytics/"
"detail": "Copy home.tar.gz to $HOME/"
},
{
"label": "GitOps(Update): gitconfig.template",
"type": "shell",
"command": ".bin/gitops update gitconfig",
"group": "build",
"problemMatcher": [],
"detail": "Copy gitconfig.template to $HOME/"
},
{
"label": "GitOps(Update): validate_command_access.sh",
"type": "shell",
"command": ".bin/gitops update validate_command",
"group": "build",
"problemMatcher": [],
"detail": "Copy validate_command_access.sh to $HOME/.local/bin"
},
{
"label": "Create home tarball",

60
Containerfile Normal file
View File

@ -0,0 +1,60 @@
# ───────────────────
# Stage 1: Base Image
# ───────────────────
FROM archlinux:base-devel-20250511.0.348143 as base
ARG DEV_USER=devuser
ARG DEV_UID=1001
ARG DEV_GID=1001
# Install all necessary packages and clean up cache
RUN pacman -Sy --noconfirm && \
pacman -S --noconfirm --needed \
base-devel neovim git git-lfs fish tmux go-yq \
nodejs python podman fzf fd ripgrep jdk-openjdk fisher yazi less \
lazygit luarocks python-pynvim npm bash-completion tree-sitter-cli kitty-terminfo \
lua51 openssh && \
pacman -Scc --noconfirm && \
rm -rf /var/cache/pacman/pkg/*
# Create user/groups as per your script, with -l to avoid system user quirks
RUN groupadd -g $DEV_GID $DEV_USER && \
groupadd -g 1002 secproc && \
useradd -l -ms /bin/fish -G secproc -u $DEV_UID -g $DEV_GID $DEV_USER
# ────────────────────────
# Stage 2: Workspace Image
# ────────────────────────
FROM base as workspace
ARG DEV_USER=devuser
ARG DEV_UID=1001
ARG DEV_GID=1001
ARG DEV_HOME=/home/$DEV_USER
# Use ADD for extracting archives
ADD home.tar.gz $DEV_HOME
# Prepare .ssh and known_hosts, and fix permissions only if dirs exist
RUN mkdir -p $DEV_HOME/.ssh && \
ssh-keyscan -p 2222 10.88.0.1 >> $DEV_HOME/.ssh/known_hosts && \
ssh-keyscan -p 22 github.com >> $DEV_HOME/.ssh/known_hosts && \
for d in $DEV_HOME/.local \
$DEV_HOME/.config/fish/completions \
$DEV_HOME/.config/fish/functions \
$DEV_HOME/.config/fish/fish_variables \
$DEV_HOME/.ssh; do \
if [ -e "$d" ]; then chown -R $DEV_USER:$DEV_USER "$d"; fi; \
done && \
for d in $DEV_HOME/.local \
$DEV_HOME/.config \
$DEV_HOME/.config/fish \
$DEV_HOME/.config/tmux; do \
if [ -e "$d" ]; then chown $DEV_USER:$DEV_USER "$d"; fi; \
done
WORKDIR /app
ENV CONTAINER_HOST=unix:///run/podman/podman.sock
USER $DEV_USER
CMD ["/home/devuser/start.sh"]

View File

@ -1,30 +0,0 @@
pallav:
name: Pallav Vasa
email: pallav@infilytics.in
rw:
- darshan
- param
- palak
darshan:
name: Darshan Parmar
email: darshan@infilytics.in
rw:
- param
ro:
- pallav
param:
name: Param Makawana
email: param@infilytics.in
ro:
- pallav
- darshan
palak:
name: Palak Vasa
email: pakak@infilytics.in
ro:
- pallav
- param
- darshan

View File

@ -1 +0,0 @@
command="/home/infilytics/ssh_router.sh pallav",no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK0il/OJiXygyPWYBt05+OQYjJPxgGuP3kP9hLsD/C7x phoenix@sphinx

View File

@ -1,24 +0,0 @@
#!/bin/bash
set -euo pipefail
BASE_IMG_NAME="analytics-backend-base"
DEV_USER=devuser
DEV_UID=1001
DEV_GID=1001
ctr=$(buildah from archlinux)
buildah run "$ctr" -- bash -c "
pacman -Sy --noconfirm && \
pacman -S --noconfirm --needed base-devel neovim git git-lfs fish tmux \
nodejs python podman fzf fd ripgrep jdk-openjdk fisher yazi less buildah \
lazygit luarocks python-pynvim npm bash-completion tree-sitter-cli kitty-terminfo \
lua51 openssh && \
pacman -Scc --noconfirm && \
groupadd -g $DEV_GID $DEV_USER && \
groupadd -g 1002 secproc && \
useradd -ms /bin/fish -G secproc -u $DEV_UID -g $DEV_GID $DEV_USER
"
buildah commit "$ctr" $BASE_IMG_NAME
echo "$BASE_IMG_NAME built."

View File

@ -1,32 +0,0 @@
#!/bin/bash
set -euo pipefail
BASE_IMG_NAME="analytics-backend-base"
IMG_NAME="analytics-backend-workspace"
DEV_USER=devuser
DEV_HOME=/home/$DEV_USER
ctr=$(buildah from "$BASE_IMG_NAME")
buildah add "$ctr" home.tar.gz $DEV_HOME
# shellcheck disable=SC2016
buildah run "$ctr" -- fish -c '
set -gx HOME '"$DEV_HOME"';
ssh-keyscan -p 2222 10.88.0.1 >> $HOME/.ssh/known_hosts;
ssh-keyscan -p 22 github.com >> $HOME/.ssh/known_hosts;
chown -R '"$DEV_USER"':'"$DEV_USER"' $HOME/.local $HOME/.config/fish/completions \
$HOME/.config/fish/functions $HOME/.config/fish/fish_variables $HOME/.ssh;
chown '"$DEV_USER"':'"$DEV_USER"' $HOME/.config $HOME/.config/fish \
$HOME/.config/tmux;
'
buildah config \
--user $DEV_USER \
--workingdir /app \
--env CONTAINER_HOST=unix:///run/podman/podman.sock \
--cmd "[\"$DEV_HOME/start.sh\"]" \
"$ctr"
buildah commit "$ctr" $IMG_NAME
echo "$IMG_NAME built from $BASE_IMG_NAME."

View File

@ -1,114 +1,178 @@
#!/usr/bin/env bash
set -euo pipefail
PERSON="$1"
PERSON="${1:?Missing PERSON argument}"
HOST="alps:3222"
PROTOCOL="http"
REPO="babbarc/workspaces"
REPO=("babbarc/workspaces" "babbarc/workspaces-sec-alps-infilytics")
BRANCH="master"
LOG_FILE="/tmp/.gitops-router-${PERSON}.log"
# ─────────────────────────────────────────────
# ANSI color codes
readonly C_RESET='\033[0m'
readonly C_INFO='\033[1;34m' # bold blue
readonly C_WARN='\033[1;33m' # bold yellow
readonly C_ERROR='\033[1;31m' # bold red
# ─────────────────────────────────────────────
# log <level> <message...> with emojis
log() {
local level="${1^^}" # convert to uppercase
local lvl="${1^^}"
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
local icon color
case "$lvl" in
INFO) icon="" color="$C_INFO" ;;
WARN) icon="⚠️" color="$C_WARN" ;;
ERROR) icon="❌" color="$C_ERROR" ;;
*) icon="🔹" color="$C_RESET" ;;
esac
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
printf '%b%s [%s] [%s] %s%b\n' \
"$color" "$icon" "$ts" "$lvl" "$*" "$C_RESET" |
tee -a "$LOG_FILE"
}
log info "Received SSH_ORIGINAL_COMMAND: $SSH_ORIGINAL_COMMAND"
# Ensure the variable is set
if [[ -z "${SSH_ORIGINAL_COMMAND:-}" ]]; then
log error "No SSH_ORIGINAL_COMMAND provided."
exit 1
fi
# ─────────────────────────────────────────────
# Build the raw URL for fetching files
geturl() {
echo "$PROTOCOL://$HOST/$REPO/$1/branch/$BRANCH/$2"
local repo="$1" type="$2" file="$3"
printf '%s://%s/%s/%s/branch/%s/%s\n' \
"$PROTOCOL" "$HOST" "${REPO[$repo]}" "$type" "$BRANCH" "$file"
}
function run() {
"$HOME"/.local/bin/"$1"
# ─────────────────────────────────────────────
# Run a local script
run() {
local script="$1"
"$HOME/.local/bin/$script"
}
function update() {
type=${4:-raw}
fname=$(basename "$1")
output_path="$HOME/$2/$fname"
url=$(geturl "$type" "$1")
# ─────────────────────────────────────────────
# Download & install an artifact
# update <repo> <file> <target-dir> <mode> [<type>]
update() {
local repo="$1" file="$2" dir="$3" mode="$4" type="${5:-raw}"
local url out
[ -f "$output_path" ] && chmod 700 "$output_path"
curl -fsSL "$url" -o "$output_path" && log info "Downloaded $url to $output_path"
chmod "$3" "$output_path"
}
out="$HOME/$dir/$(basename "$file")"
url="$(geturl "$repo" "$type" "$file")"
clean_images() {
# Get list of image IDs with <none> tag (dangling images)
dangling_images=$(podman images -f "dangling=true" -q)
[[ -f "$out" ]] && chmod 700 "$out"
if [ -z "$dangling_images" ]; then
echo "✅ No dangling images to remove."
if curl -fsSL "$url" -o "$out"; then
log INFO "Downloaded $url$out"
chmod "$mode" "$out"
else
echo "⚠️ Removing dangling images..."
echo "$dangling_images" | xargs podman rmi
echo "🧹 Done!"
log ERROR "Failed to download $url"
return 1
fi
}
# Strip arguments and parse command
read -r command args <<<"$SSH_ORIGINAL_COMMAND"
# ─────────────────────────────────────────────
# Clean up dangling podman images
clean_images() {
local dangling
dangling="$(podman images -f dangling=true -q)"
if [[ -z "$dangling" ]]; then
log INFO "No dangling images to remove."
else
log WARN "Removing dangling images..."
echo "$dangling" | xargs podman rmi
log INFO "Dangling images removed."
fi
}
# Define command routing
case "$command" in
# ─────────────────────────────────────────────
# Remove host podman containers
remove_containers() {
local tokens=("$@")
local flags=() patterns=() containers=()
local valid='^[A-Za-z0-9._-]+$'
# allow unmatched globs to disappear
shopt -s nullglob
# separate flags (-f, etc.) from name patterns
for tok in "${tokens[@]}"; do
if [[ "$tok" == -* ]]; then
flags+=("$tok")
else
patterns+=("$tok")
fi
done
# validate & expand each pattern
for pat in "${patterns[@]}"; do
if [[ ! "$pat" =~ $valid ]]; then
log ERROR "Invalid container name: '$pat'"
shopt -u nullglob
return 1
fi
containers+=("$pat")
done
shopt -u nullglob
if ((${#containers[@]} == 0)); then
log WARN "No containers matched: ${patterns[*]}"
return 0
fi
# pass flags *then* containers to podman rm
podman rm "${flags[@]}" "${containers[@]}"
}
# ─────────────────────────────────────────────
# validate_command <workspace> <cmd> [<tok1> <tok2> …]
source "$HOME"/.local/bin/validate_command_access.sh
# ─────────────────────────────────────────────
# Entry & command parsing
if [[ -z "${SSH_ORIGINAL_COMMAND:-}" ]]; then
log ERROR "No SSH_ORIGINAL_COMMAND provided."
exit 1
fi
log INFO "SSH_ORIGINAL_COMMAND: $SSH_ORIGINAL_COMMAND"
read -ra parts <<<"$SSH_ORIGINAL_COMMAND"
cmd="${parts[0]}"
args=("${parts[@]:1}")
validate_command "$PERSON" "$cmd" "${args[@]}"
# ─────────────────────────────────────────────
# Dispatch
case "$cmd" in
build)
case "$args" in
base)
run build-base.sh
;;
workspace)
run build-workspace.sh
;;
*)
log error "Invalid arguments for build command: $args"
;;
case "${args[0]}" in
base) podman build --target base -t analytics-backend-base . ;;
workspace) podman build --target base -t analytics-backend-base . ;;
all) podman build -t analytics-backend-workspace . ;;
*) log ERROR "build: invalid arg '${args[0]}'" ;;
esac
;;
update)
case "$args" in
workspace)
update build-workspace.sh .local/bin 500
;;
base)
update build-base.sh .local/bin 500
;;
access)
update access.yml . 400
;;
ssh_router)
update ssh_router.sh .local/bin 500
;;
gitops_router)
update gitops_router.sh .local/bin 500
;;
home_tar)
update home.tar.gz . 500 media
;;
*)
log error "Invalid arguments for update command: $args"
;;
case "${args[0]}" in
containerfile) update 0 Containerfile . 500 ;;
access) update 1 access.yml . 400 ;;
authorized_keys) update 1 access.yml . 400 ;;
ssh_router) update 0 ssh_router.sh .local/bin 500 ;;
gitops_router) update 0 gitops_router.sh .local/bin 500 ;;
validate_command) update 0 validate_command_access.sh .local/bin 500 ;;
home_tar) update 0 home.tar.gz . 500 media ;;
gitconfig) update 0 gitconfig.template . 500 ;;
*) log ERROR "update: invalid arg '${args[0]}'" ;;
esac
;;
clean)
clean_images
;;
status)
podman images
;;
remove)
podman rm "$args"
;;
clean) clean_images ;;
status) podman images ;;
remove) remove_containers "${args[@]}" ;;
*)
log error "Unknown command: $command"
log ERROR "Unknown command: '$cmd'"
exit 127
;;
esac

View File

@ -1,187 +1,208 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
PERSON="$1"
WORKSPACE="$SSH_ORIGINAL_COMMAND"
PERSON="${1:?Usage: $0 <person>}"
WORKSPACE="${SSH_ORIGINAL_COMMAND:-}"
IMAGE="localhost/analytics-backend-workspace:latest"
DEV_USER="devuser"
XDG_RUNTIME_DIR="/run/user/$(id -u)"
LOG_FILE="/tmp/.ssh-router-${PERSON}.log"
# ─────────────────────────────────────────────
# ANSI colors & emojis
readonly C_RESET='\033[0m'
readonly C_INFO='\033[1;34m' # blue
readonly C_WARN='\033[1;33m' # yellow
readonly C_ERROR='\033[1;31m' # red
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >>"$LOG_FILE"
local level="${1^^}"
shift
local icon color
case "$level" in
INFO) icon="" color="$C_INFO" ;;
WARN) icon="⚠️" color="$C_WARN" ;;
ERROR) icon="❌" color="$C_ERROR" ;;
*) icon="🔹" color="$C_RESET" ;;
esac
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
printf '%b%s [%s] %s%b\n' \
"$color" "$icon" "$ts" "[$level] $*" "$C_RESET" |
tee -a "$LOG_FILE"
}
# ─────────────────────────────────────────────
# Check for interactive TTY
if [[ ! -t 0 ]]; then
log "No TTY allocatedrefusing to run tmux without an interactive terminal"
log ERROR "No TTY allocatedrefusing to run without an interactive terminal"
echo "Error: No TTY. Use 'ssh -t'" >&2
exit 1
fi
# log "🧩 IMAGE = '$IMAGE'"
# log "🧩 WORKSPACE = '$WORKSPACE'"
# log "🧩 PERSON = '$PERSON'"
# Fallbacks
if [[ -z "${WORKSPACE:-}" ]]; then
# ─────────────────────────────────────────────
# Default WORKSPACE if empty
if [[ -z "$WORKSPACE" ]]; then
WORKSPACE="$PERSON"
log " Defaulted WORKSPACE to $WORKSPACE"
log INFO "Defaulted WORKSPACE $WORKSPACE"
fi
TMUX_SESSION="${WORKSPACE}|analytics-backend"
TMUX_SESSION="$WORKSPACE|analytics-backend"
# Start podman socket service if it's not running
if [[ ! -S "$XDG_RUNTIME_DIR/podman/podman.sock" ]]; then
log "🔄 Starting Podman socket service for user $USER"
# ─────────────────────────────────────────────
# Ensure Podman socket is up
ensure_podman() {
local sock="$XDG_RUNTIME_DIR/podman/podman.sock"
if [[ ! -S "$sock" ]]; then
log INFO "Starting podman.socket for user $(id -un)"
systemctl --user start podman.socket || {
log "Failed to start podman.socket via systemd"
log ERROR "Failed to start podman.socket"
exit 1
}
# Wait briefly for socket to appear
sleep 1
fi
if [[ ! -S "$XDG_RUNTIME_DIR/podman/podman.sock" ]]; then
log "❌ Podman socket still missing after startup attempt"
exit 1
fi
# Check if image exists locally
if ! podman image exists "$IMAGE"; then
log "📦 Image $IMAGE not found locally. Pulling from registry..."
# Attempt to pull the image from the local registry (insecure HTTP)
if ! podman pull --tls-verify=false "$IMAGE"; then
log "❌ Failed to pull image from $IMAGE"
exit 1
fi
[[ -S "$sock" ]] || {
log ERROR "Podman socket still missing"
exit 1
}
}
ensure_podman
log "✅ Successfully pulled $IMAGE"
fi
# ─────────────────────────────────────────────
# Ensure IMAGE is present
ensure_image() {
if ! podman image exists "$IMAGE"; then
log WARN "Image $IMAGE not found—pulling"
podman pull --tls-verify=false "$IMAGE" || {
log ERROR "Failed to pull $IMAGE"
exit 1
}
log INFO "Pulled $IMAGE"
fi
}
ensure_image
# ─────────────────────────────────────────────
# Disallow file transfers
case "$SSH_ORIGINAL_COMMAND" in
*scp* | *sftp* | *rsync* | *tar*)
log "File transfers are disabled"
log ERROR "File transfers are disabled"
exit 1
;;
esac
# Function to start the container if not running
# ─────────────────────────────────────────────
# Generate per-user gitconfig
generate_gitconfig() {
local access="$HOME/access.yml"
local template="$HOME/gitconfig.template"
local userdir="$HOME/secrets/$PERSON"
local name email
name=$(yq -r ".\"$PERSON\".name" "$access" 2>/dev/null || echo)
email=$(yq -r ".\"$PERSON\".email" "$access" 2>/dev/null || echo)
if [[ -z "$name" || -z "$email" ]]; then
log ERROR "Missing name/email for '$PERSON' in $access"
exit 1
fi
mkdir -p "$userdir"
GIT_NAME="$name" GIT_EMAIL="$email" \
envsubst <"$template" >"$userdir/gitconfig"
log INFO ".gitconfig created → $userdir/gitconfig"
}
# ─────────────────────────────────────────────
# Start container if absent or stopped
start_container_if_needed() {
if ! podman container exists "$WORKSPACE"; then
log "🚀 Creating container $WORKSPACE..."
log INFO "Creating container '$WORKSPACE'"
generate_gitconfig
podman run -dit \
--userns=keep-id \
--name "$WORKSPACE" \
--userns=keep-id \
--user "$DEV_USER" \
--hostname "$WORKSPACE" \
--label auto-cleanup=true \
-v "${XDG_RUNTIME_DIR}"/podman/podman.sock:/run/podman/podman.sock \
-v /home/infilytics/data/"$WORKSPACE":/app \
-v /home/infilytics/secrets/"$WORKSPACE"/gitconfig:/home/"$DEV_USER"/.gitconfig:ro \
-v /home/infilytics/secrets/"$WORKSPACE"/id_ed25519:/home/"$DEV_USER"/.ssh/id_ed25519:ro \
-v /home/infilytics/secrets/"$WORKSPACE"/id_ed25519.pub:/home/"$DEV_USER"/.ssh/id_ed25519.pub:ro \
-v "$HOME/data/$WORKSPACE:/app:Z" \
-v "$HOME/secrets/$WORKSPACE/gitconfig:/home/$DEV_USER/.gitconfig:ro,Z" \
-v "$HOME/secrets/$WORKSPACE/id_ed25519:/home/$DEV_USER/.ssh/id_ed25519:ro,Z" \
-v "$HOME/secrets/$WORKSPACE/id_ed25519.pub:/home/$DEV_USER/.ssh/id_ed25519.pub:ro,Z" \
--entrypoint "/home/$DEV_USER/start.sh" \
"$IMAGE" "${TMUX_SESSION}"
"$IMAGE" "$TMUX_SESSION"
elif ! podman inspect -f '{{.State.Running}}' "$WORKSPACE" | grep -q true; then
log "Starting existing container $WORKSPACE..."
podman start "$WORKSPACE" >/dev/null 2>&1
log INFO "Starting existing container '$WORKSPACE'"
podman start "$WORKSPACE" >/dev/null
fi
sleep 1
}
# After devuser exits...
# ─────────────────────────────────────────────
# Detach logic: stop container when devuser has left
check_devuser_attached() {
# Get list of clients
client_users=$(podman exec "$WORKSPACE" tmux list-clients -t "$TMUX_SESSION" -F "#{client_user}" 2>/dev/null)
if echo "$client_users" | grep -q "$DEV_USER"; then
log "💡 devuser still attached — container stays running"
return 0
local clients
clients=$(podman exec "$WORKSPACE" tmux list-clients -t "$TMUX_SESSION" -F "#{client_user}" 2>/dev/null)
if grep -q "^${DEV_USER}\$" <<<"$clients"; then
log INFO "devuser still attached—keeping container running"
else
log "🏃 $PERSON has logged out — stopping container"
podman stop "$WORKSPACE" >/dev/null 2>&1
return 1
log INFO "devuser detached—stopping container"
podman stop "$WORKSPACE" >/dev/null
fi
}
# ─────────────────────────────────────────────
# Determine access mode (rw|ro) or exit
get_access_mode() {
local yaml_file="access.yml"
local workspace="$1"
local person="$2"
if [[ ! "$workspace" =~ ^[a-zA-Z0-9._-]+$ ]]; then
log "❌ Invalid container name: $WORKSPACE"
local yaml="access.yml" user="$PERSON" ws="$WORKSPACE"
[[ ! "$ws" =~ ^[A-Za-z0-9._-]+$ ]] && {
log ERROR "Invalid workspace name"
exit 1
}
if [[ "$user" == "$ws" ]]; then
echo rw
elif yq -e '.["'"$user"'"].rw[]?' "$yaml" | grep -qx "$ws"; then
echo rw
elif yq -e '.["'"$user"'"].ro[]?' "$yaml" | grep -qx "$ws"; then
echo ro
else
log ERROR "$user has no access to $ws"
exit 1
fi
# Special case: user accessing their own workspace
if [[ "$workspace" == "$person" ]]; then
echo "access=rw"
return 0
fi
# Check rw
if yq '.["'"$person"'"].rw // []' "$yaml_file" | grep -q "\b$workspace\b"; then
echo "access=rw"
return 0
fi
# Check ro
if yq '.["'"$person"'"].ro // []' "$yaml_file" | grep -q "\b$workspace\b"; then
echo "access=ro"
return 0
fi
# No access → exit with error
log "$person has no access to $workspace" >&2
exit 1
}
# === Main ===
read -r access_line < <(get_access_mode "$WORKSPACE" "$PERSON") || exit 1
MODE="${access_line#access=}"
MODE="$(get_access_mode)"
# ─────────────────────────────────────────────
# Main dispatch
case "$MODE" in
rw)
start_container_if_needed
# Run tmux session inside the container
if ! podman exec -it --user "$DEV_USER" "$WORKSPACE" tmux has-session -t "$TMUX_SESSION" >/dev/null 2>&1; then
if ! podman exec -it -e EDITOR=nvim --user "$DEV_USER" "$WORKSPACE" tmux new-session -d -s "$TMUX_SESSION" >/dev/null 2>&1; then
log "❌ Could not create new tmux session. Please contact admin or try again later."
exit 1
fi
# Ensure tmux session exists
if ! podman exec -it --user "$DEV_USER" "$WORKSPACE" tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
podman exec -it --user "$DEV_USER" "$WORKSPACE" \
tmux new-session -d -s "$TMUX_SESSION"
fi
log "$PERSON is working on $WORKSPACE's workspace"
if ! podman exec -it -e TERM="$TERM" --user "$DEV_USER" "$WORKSPACE" tmux attach -t "$TMUX_SESSION"; then
log "❌ Could not attach to tmux session. Please contact admin or try again later."
exit 1
fi
log "$PERSON finished working on $WORKSPACE's worksapce"
log INFO "$PERSON attaching to workspace '$WORKSPACE'"
podman exec -it -e TERM="$TERM" --user "$DEV_USER" "$WORKSPACE" \
tmux attach -t "$TMUX_SESSION"
log INFO "$PERSON detached from '$WORKSPACE'"
check_devuser_attached
exit 0
;;
ro)
if (podman container exists "$WORKSPACE" && podman inspect -f '{{.State.Running}}' "$WORKSPACE" | grep -q true) >/dev/null 2>&1; then
log "📜 $PERSON is viewing $WORKSPACE's workspace"
if ! podman exec -it -e TERM="$TERM" --user "$DEV_USER" "$WORKSPACE" tmux attach -r -t "$TMUX_SESSION"; then
log "❌ Could not attach to tmux session. Please contact admin or try again later."
exit 1
fi
log "🏃 $PERSON stopped viewing $WORKSPACE's workspace"
exit 0
if podman inspect -f '{{.State.Running}}' "$WORKSPACE" 2>/dev/null | grep -q true; then
log INFO "$PERSON viewing workspace '$WORKSPACE'"
podman exec -it -e TERM="$TERM" --user "$DEV_USER" "$WORKSPACE" \
tmux attach -r -t "$TMUX_SESSION"
log INFO "$PERSON stopped viewing '$WORKSPACE'"
else
log "Workspace for $WORKSPACE does not exist."
log ERROR "Workspace '$WORKSPACE' is not running"
exit 1
fi
;;
*)
log "❌ Invalid access mode: $MODE"
log ERROR "Unknown access mode: '$MODE'"
exit 1
;;
esac

View File

@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -e
cat >access.yml <<EOF
pallav:
fixedArgsCommands:
build:
- base
- workspace
- all
clean:
status:
multiArgsCommands:
remove:
- palak
- param
- darshan
EOF
source ./validate_command_access.sh
testcase() {
local desc="$1"
shift
if validate_command pallav "$@"; then
echo "PASS: $desc"
else
echo "FAIL: $desc"
fi
}
testcase "build base (valid)" build base
testcase "build all (valid)" build all
testcase "build base workspace (invalid)" build base workspace || true
testcase "build (no arg, invalid)" build || true
testcase "clean (zero-arg, valid)" clean
testcase "clean with arg (invalid)" clean foo || true
testcase "remove palak (valid)" remove palak
testcase "remove param palak (valid, any order)" remove param palak
testcase "remove palak param darshan (valid, any order)" remove palak param darshan
testcase "remove (no arg, invalid)" remove || true
testcase "remove foo (invalid)" remove foo || true
testcase "remove palak palak (duplicate, invalid)" remove palak palak || true
testcase "status (zero-arg, valid)" status
testcase "status foo (invalid)" status foo || true

View File

@ -0,0 +1,83 @@
#!/usr/bin/env bash
validate_command() {
local PERSON="$1"
local cmd="$2"
shift 2
local tokens=("$@")
local yaml="access.yml"
# Check if fixedArgsCommands.<cmd> exists
local is_fixed
is_fixed="$(yq e ".\"$PERSON\".fixedArgsCommands | has(\"$cmd\")" "$yaml")"
# Check if multiArgsCommands.<cmd> exists
local is_multi
is_multi="$(yq e ".\"$PERSON\".multiArgsCommands | has(\"$cmd\")" "$yaml")"
if [[ "$is_fixed" != "true" && "$is_multi" != "true" ]]; then
echo "ERROR: Command '$cmd' not allowed for $PERSON" >&2
return 1
fi
# Exclude flags from positional args
local args=()
for tok in "${tokens[@]}"; do
[[ "$tok" == -* ]] && continue
args+=("$tok")
done
if [[ "$is_fixed" == "true" ]]; then
mapfile -t allowed < <(yq e ".\"$PERSON\".fixedArgsCommands.\"$cmd\"[]" "$yaml" 2>/dev/null)
local n_allowed="${#allowed[@]}"
if [[ $n_allowed -eq 0 ]]; then
# zero-arg command
if [[ ${#args[@]} -ne 0 ]]; then
echo "ERROR: Command '$cmd' takes no arguments" >&2
return 1
fi
else
# depth is 1: only one of the allowed choices must be present
if [[ ${#args[@]} -ne 1 ]]; then
echo "ERROR: Command '$cmd' requires exactly 1 argument: (${allowed[*]})" >&2
return 1
fi
local found=0
for want in "${allowed[@]}"; do
[[ "${args[0]}" == "$want" ]] && found=1 && break
done
if [[ $found -eq 0 ]]; then
echo "ERROR: Invalid argument '${args[0]}' for '$cmd'; allowed: (${allowed[*]})" >&2
return 1
fi
fi
return 0
fi
if [[ "$is_multi" == "true" ]]; then
mapfile -t allowed < <(yq e ".\"$PERSON\".multiArgsCommands.\"$cmd\"[]" "$yaml" 2>/dev/null)
local n_allowed="${#allowed[@]}"
if [[ ${#args[@]} -lt 1 || ${#args[@]} -gt $n_allowed ]]; then
echo "ERROR: Command '$cmd' requires 1 to $n_allowed arguments: (${allowed[*]})" >&2
return 1
fi
# Order doesn't matter, but all must be unique and from allowed.
# Build a set of allowed args.
declare -A allowed_set=()
for want in "${allowed[@]}"; do allowed_set["$want"]=1; done
declare -A seen=()
for a in "${args[@]}"; do
[[ -z "${allowed_set[$a]}" ]] && {
echo "ERROR: Invalid argument '$a' for '$cmd'; allowed: (${allowed[*]})" >&2
return 1
}
[[ -n "${seen[$a]}" ]] && {
echo "ERROR: Duplicate argument '$a' for '$cmd'" >&2
return 1
}
seen["$a"]=1
done
return 0
fi
}