dev1
This commit is contained in:
parent
a6a97731af
commit
45f0f3cdc2
@ -1,64 +1,327 @@
|
||||
#!/bin/bash
|
||||
|
||||
#!/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() {
|
||||
echo "Usage: $0 -i <image> <name>"
|
||||
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
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while getopts ":i:" opt; do
|
||||
case ${opt} in
|
||||
i )
|
||||
IFS=':' read -r IMAGE IMAGE_TAG <<< "${OPTARG}" # Split image and tag
|
||||
IMAGE_TAG=${IMAGE_TAG:-latest} # Default to 'latest' if no tag is provided
|
||||
FULL_IMAGE_NAME="${REGISTRY}/${OPTARG}" # Keep the full image name with tag
|
||||
;;
|
||||
\? )
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift "$((OPTIND -1))"
|
||||
|
||||
if [ -z "$FULL_IMAGE_NAME" ] || [ -z "$1" ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
NAME="$1"
|
||||
CONTAINER_NAME="${IMAGE##*/}-${NAME}" # Use only the base image name (without tag)
|
||||
|
||||
exec_into_container() {
|
||||
docker exec --detach-keys "ctrl-q,ctrl-p" -it "$CONTAINER_NAME" bash -c "zsh"
|
||||
fail() {
|
||||
printf 'Error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if the container exists
|
||||
if [ "$(docker ps -a -q -f "name=$CONTAINER_NAME")" ]; then
|
||||
# Container exists, start it if it's not running
|
||||
if [ ! "$(docker ps -q -f "name=$CONTAINER_NAME")" ]; then
|
||||
echo "Container $CONTAINER_NAME exists but is not running. Starting it..."
|
||||
docker start "$CONTAINER_NAME"
|
||||
resolve_path() {
|
||||
local path="$1"
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
realpath "$path"
|
||||
else
|
||||
echo "Container $CONTAINER_NAME is already running."
|
||||
echo "$(cd "$(dirname "$path")" && pwd)/$(basename "$path")"
|
||||
fi
|
||||
else
|
||||
echo "Container $CONTAINER_NAME does not exist. Creating and running it in detached mode..."
|
||||
docker run -d \
|
||||
--network host \
|
||||
-v "$HOME/.ssh:/home/dev/.ssh" \
|
||||
-v "$PWD:/workspace" \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--group-add "$(getent group docker | cut -d: -f3)" \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--init \ # run tini as PID 1 to handle signals & reap zombies for cleaner container shutdown
|
||||
"$FULL_IMAGE_NAME" \
|
||||
sleep infinity # use if coreutils not available: tail -f /dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Executing into container $CONTAINER_NAME..."
|
||||
exec_into_container
|
||||
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 "$@"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user