328 lines
8.2 KiB
Bash
Executable File
328 lines
8.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
source './parse_image_ref.sh'
|
|
|
|
REGISTRY="registry.tomastm.com"
|
|
REGISTRY_ABBR="tm0"
|
|
PROJECT_DIR="$HOME/projects"
|
|
PROJECT_ABBR="p"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: dev <command> [options] <name>
|
|
|
|
Commands:
|
|
create -i, --image <image> -p, --project <path> <name>
|
|
exec <name> [-- <cmd>...]
|
|
connect <name>
|
|
list
|
|
info <name>
|
|
stop [--kill] <name>
|
|
rm [--force|-f] <name>
|
|
|
|
Notes:
|
|
- 'exec' treats the LAST argument as <name>; everything before it is the command to run.
|
|
- If already inside tmux, 'connect' switches to the session; otherwise it attaches.
|
|
- New tmux panes/windows created in a session always run inside the container.
|
|
- within tmux that need <name>: info, stop, rm, restore, connect
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
fail() {
|
|
printf 'Error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
resolve_path() {
|
|
local path="$1"
|
|
if command -v realpath >/dev/null 2>&1; then
|
|
realpath "$path"
|
|
else
|
|
echo "$(cd "$(dirname "$path")" && pwd)/$(basename "$path")"
|
|
fi
|
|
}
|
|
|
|
docker_container_exists() {
|
|
local name="$1"
|
|
docker container ls -a --format '{{.Names}}' | grep -Fqx "$name"
|
|
}
|
|
|
|
docker_container_running() {
|
|
local name="$1"
|
|
docker container ls --format '{{.Names}}' | grep -Fqx "$name"
|
|
}
|
|
|
|
docker_image_present() {
|
|
local ref="$1"
|
|
docker image inspect "$ref" >/dev/null 2>&1
|
|
}
|
|
|
|
cmd_create() {
|
|
local image_arg="" project_arg=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-i | --image)
|
|
[[ $# -ge 2 ]] || fail "Missing value for $1"
|
|
image_arg="$2"
|
|
shift 2
|
|
;;
|
|
-p | --project)
|
|
[[ $# -ge 2 ]] || fail "Missing value for $1"
|
|
project_arg="$2"
|
|
shift 2
|
|
;;
|
|
-*) usage ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
|
|
# Check args
|
|
local name_arg="${1:-}"
|
|
if [[ -z "$name_arg" || -z "$image_arg" || -z "$project_arg" ]]; then
|
|
fail "Missing arguments"
|
|
fi
|
|
|
|
# Check container name
|
|
local cname="dev-$name_arg"
|
|
if docker_container_exists "$cname"; then
|
|
fail "Container already exists: "$cname" (from name "$name_arg")"
|
|
fi
|
|
|
|
# Check project path
|
|
local project_path
|
|
project_path="$(resolve_path "$project_arg")"
|
|
if [[ ! -d "$project_path" ]]; then
|
|
fail "Invalid project path: $project_path"
|
|
fi
|
|
|
|
# Check image
|
|
IFS=' ' read -r image_ref image_repo image_tag image_label <<<"$(parse_image_ref "$image_arg")"
|
|
if ! docker_image_present "$image_ref"; then
|
|
fail $'Image not found locally.\nTry:\n\t- docker pull '"$image_ref"
|
|
fi
|
|
|
|
# Run (= create and start container)
|
|
cmd=(
|
|
docker run -d
|
|
--name "$cname"
|
|
--label dev=true
|
|
--network host
|
|
--init # run tini as PID 1 to handle signals & reap zombies for cleaner container shutdown
|
|
-v "$project_path:/workspace"
|
|
-v /var/run/docker.sock:/var/run/docker.sock
|
|
)
|
|
|
|
[[ -d "$HOME/.ssh" ]] && cmd+=(-v "$HOME/.ssh:$CONTAINER_HOME/.ssh:ro")
|
|
[[ -f "$HOME/.npmrc" ]] && cmd+=(-v "$HOME/.npmrc:$CONTAINER_HOME/.npmrc:ro")
|
|
[[ -d "$HOME/.npm" ]] && cmd+=(-v "$HOME/.npm:$CONTAINER_HOME/.npm")
|
|
|
|
docker_gid="$(getent group docker | cut -d: -f3 || true)"
|
|
[[ -n "$docker_gid" ]] && cmd+=(--group-add "$docker_gid")
|
|
|
|
cmd+=("$image_ref" sleep infinity)
|
|
"${cmd[@]}"
|
|
|
|
echo "$cname"
|
|
}
|
|
|
|
cmd_exec() {
|
|
# usage: exec <name> [-- <cmd>...]
|
|
|
|
local name="$1"
|
|
[[ -n "$name" ]] || fail "Missing project name"
|
|
shift
|
|
|
|
local cname="dev-$name"
|
|
if ! docker_container_running "$cname"; then
|
|
fail "Container $cname not running"
|
|
fi
|
|
|
|
if [[ "$1" == "--" ]]; then
|
|
shift
|
|
local args=("$@")
|
|
if [[ -t 1 ]]; then
|
|
docker exec -it "$cname" "${args[@]}"
|
|
else
|
|
docker exec "$cname" "${args[@]}"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
# No command provided -> open a shell
|
|
docker exec -it "$cname" zsh -l ||
|
|
docker exec -it "$cname" bash -l ||
|
|
docker exec -it "$cname" sh
|
|
}
|
|
|
|
cmd_connect() {
|
|
# usage: connect [--from] <name>
|
|
|
|
local from_name=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-f | --from)
|
|
[[ $# -ge 2 ]] || fail "Missing value for $1"
|
|
from_name="$2"
|
|
shift 2
|
|
;;
|
|
-*) usage ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
|
|
local name="${1:from_name}"
|
|
[[ -n "$name" ]] || fail "Missing project name"
|
|
|
|
local cname="dev-$name"
|
|
if ! docker_container_exists "$cname"; then
|
|
fail "Container does not exist: ${cname}. Run: dev create ..."
|
|
fi
|
|
|
|
if ! docker_container_running "$cname"; then
|
|
docker start "$cname" >/dev/null
|
|
fi
|
|
|
|
if ! command -v tmux >/dev/null 2>&1; then
|
|
echo "tmux not found; falling back to direct exec"
|
|
exec "$0" exec "$name"
|
|
fi
|
|
|
|
local image_ref
|
|
image_ref="$(docker container inspect "$cname" --format '{{ .Config.Image }}')"
|
|
IFS=' ' read -r _image_ref image_repo image_tag image_label <<<"$(parse_image_ref "$image_ref")"
|
|
|
|
local tname="dev:$name"
|
|
if ! tmux has-session -t "$tname" 2>/dev/null; then
|
|
tmux new-session -ds "$tname" -e "DEV_IMAGE=$image_label" "$0 exec \"$name\""
|
|
tmux set-option -t "$tname" default-command "$0 exec \"$name\""
|
|
fi
|
|
|
|
if [[ -n "${TMUX-}" ]]; then
|
|
tmux switch-client -t "$tname"
|
|
else
|
|
tmux attach -t "$tname"
|
|
fi
|
|
}
|
|
|
|
shorten_project_path() {
|
|
local project="$1"
|
|
|
|
# Case 1: path is under PROJECT_DIR
|
|
if [[ "$project" == "$PROJECT_DIR"* ]]; then
|
|
project="~/$PROJECT_ABBR${project#$PROJECT_DIR}"
|
|
# Case 2: path is under HOME (but not PROJECT_DIR)
|
|
elif [[ "$project" == "$HOME"* ]]; then
|
|
project="~${project#$HOME}"
|
|
fi
|
|
|
|
echo "$project"
|
|
}
|
|
|
|
cmd_list() {
|
|
{
|
|
echo "NAME|IMAGE|PROJECT|STATUS"
|
|
docker ps -a --filter "label=dev=true" \
|
|
--format '{{.Label "dev.name"}}|{{.Image}}|{{.Label "dev.project_path"}}|{{.Status}}'
|
|
} | while IFS='|' read -r fname image project status; do
|
|
# Shorten registry prefix
|
|
image="${image/$REGISTRY\//$REGISTRY_ABBR/}"
|
|
|
|
# Shorten project path
|
|
project="$(shorten_project_path "$project")"
|
|
|
|
echo "$fname|$image|$project|$status"
|
|
done | column -t -s '|'
|
|
}
|
|
|
|
cmd_stop() {
|
|
local kill_flag=0
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--kill)
|
|
kill_flag=1
|
|
shift
|
|
;;
|
|
-*) usage ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
local name="${1:-}"
|
|
[[ -n "$name" ]] || fail "Missing project name"
|
|
local cname="dev-$name"
|
|
|
|
docker_container_exists "$cname" || fail "Container $cname does not exist"
|
|
|
|
if ((kill_flag)); then
|
|
echo "Killing container $cname..."
|
|
docker kill "$cname"
|
|
else
|
|
echo "Stopping container $cname..."
|
|
docker stop "$cname"
|
|
fi
|
|
}
|
|
|
|
cmd_rm() {
|
|
local force_flag=0
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--force | -f)
|
|
force_flag=1
|
|
shift
|
|
;;
|
|
-*) usage ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
local name="${1:-}"
|
|
[[ -n "$name" ]] || fail "Missing project name"
|
|
local cname="dev-$name"
|
|
|
|
docker_container_exists "$cname" || fail "Container $cname does not exist"
|
|
|
|
if ((force_flag)); then
|
|
echo "Removing container $cname (force)..."
|
|
docker rm -f "$cname"
|
|
else
|
|
echo "Removing container $cname..."
|
|
docker rm "$cname"
|
|
fi
|
|
}
|
|
|
|
cmd_respawn() {
|
|
local name="${1:-}"
|
|
[[ -n "$name" ]] || fail "Missing project name"
|
|
local cname="dev-$name"
|
|
|
|
panes=$(tmux list-panes -t "$cname" -s -F "#{session_name}:#{window_index}.#{pane_index}")
|
|
|
|
for pane in $panes; do
|
|
echo "Respawning $pane..."
|
|
tmux respawn-pane -t "$pane"
|
|
done
|
|
}
|
|
|
|
cmd_test() {
|
|
echo "Script dev is working fine!"
|
|
}
|
|
|
|
main() {
|
|
local cmd="${1:-}"
|
|
shift || true
|
|
case "$cmd" in
|
|
create) cmd_create "$@" ;;
|
|
connect) cmd_connect "$@" ;;
|
|
exec) cmd_exec "$@" ;;
|
|
list) cmd_list ;;
|
|
stop) cmd_stop "$@" ;;
|
|
rm) cmd_rm "$@" ;;
|
|
respawn) cmd_respawn "$@" ;;
|
|
test) cmd_test "$@" ;;
|
|
*) usage ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|