Files
Linux/docker-exec-via-sock.sh

235 lines
5.9 KiB
Bash
Executable File

#!/bin/sh
set -eu
# 通过 Docker Engine unix 套接字在容器中执行命令。
# Docker Engine API: https://docs.docker.com/reference/api/engine/
# 示例: ./docker-exec-via-sock.sh --container=my-container --cmd="ls -l /"
log() {
printf '%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" >&2
}
log_stream() {
log_stream_prefix=$1
log_stream_payload=$2
if [ -n "$log_stream_payload" ]; then
printf '%s\n' "$log_stream_payload" | while IFS= read -r log_stream_line; do
log "$log_stream_prefix: $log_stream_line"
done
fi
}
print_usage() {
cat <<EOF >&2
Usage: $0 [--socket=PATH] [--api-version=VERSION] --container=NAME --cmd=COMMAND [--user=NAME] [--shell=SHELL]
Options:
--socket=PATH Docker Engine unix socket path (default: /var/run/docker.sock)
--api-version=VERSION Docker API version (default: v1.51)
--container=NAME Container name to execute command in
--cmd=COMMAND Command to execute in the container
--user=NAME Run command as the specified user (default: container default user)
--shell=SHELL Shell to use (default: sh)
--help Show this help message
EOF
}
DOCKER_SOCKET="/var/run/docker.sock"
DOCKER_API_VERSION="v1.51"
CONTAINER_NAME=""
CMD_TO_EXEC=""
EXEC_USER=""
DOCKER_LAST_RESPONSE=""
EXEC_SHELL="sh"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
printf '%s requires %s in PATH\n' "$0" "$1" >&2
exit 1
fi
}
missing_value() {
printf 'Missing value for %s\n' "$1" >&2
print_usage
exit 1
}
docker_exec_create() {
docker_exec_create_desc=$1
docker_exec_create_cmd=$2
docker_exec_create_payload=$(jq -n --arg cmd "$docker_exec_create_cmd" --arg user "$EXEC_USER" --arg shell "$EXEC_SHELL" '{
AttachStdin: false,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: [$shell, "-c", $cmd]
} | if ($user | length) > 0 then .User = $user else . end')
DOCKER_LAST_RESPONSE=$(curl --silent --show-error --unix-socket "$DOCKER_SOCKET" \
-X POST \
-H "Content-Type: application/json" \
-d "$docker_exec_create_payload" \
"$exec_create_endpoint")
docker_exec_create_id=$(printf '%s' "$DOCKER_LAST_RESPONSE" | jq -r '.Id // empty')
if [ -z "$docker_exec_create_id" ]; then
error_message=$(printf '%s' "$DOCKER_LAST_RESPONSE" | jq -r '.message // "unknown error"')
log "failed to create $docker_exec_create_desc exec for $CONTAINER_NAME: $error_message"
return 1
fi
printf '%s' "$docker_exec_create_id"
}
docker_exec_start() {
docker_exec_start_id=$1
docker_exec_start_endpoint="${docker_api_base}/exec/${docker_exec_start_id}/start"
DOCKER_LAST_RESPONSE=$(curl --silent --show-error --unix-socket "$DOCKER_SOCKET" \
-X POST \
-H "Content-Type: application/json" \
-d '{"Detach": false, "Tty": true}' \
"$docker_exec_start_endpoint")
printf '%s' "$DOCKER_LAST_RESPONSE"
}
docker_exec_exit_code() {
docker_exec_exit_id=$1
docker_exec_inspect_endpoint="${docker_api_base}/exec/${docker_exec_exit_id}/json"
DOCKER_LAST_RESPONSE=$(curl --silent --show-error --unix-socket "$DOCKER_SOCKET" "$docker_exec_inspect_endpoint")
docker_exec_exit_code_value=$(printf '%s' "$DOCKER_LAST_RESPONSE" | jq -r '.ExitCode')
if [ "$docker_exec_exit_code_value" = "null" ]; then
log "failed to get exit code. response: $DOCKER_LAST_RESPONSE"
return 1
fi
printf '%s' "$docker_exec_exit_code_value"
}
while [ "$#" -gt 0 ]; do
case $1 in
--socket=*)
DOCKER_SOCKET="${1#*=}"
;;
--socket)
if [ "$#" -lt 2 ]; then missing_value '--socket'; fi
shift
DOCKER_SOCKET="$1"
;;
--api-version=*)
DOCKER_API_VERSION="${1#*=}"
;;
--api-version)
if [ "$#" -lt 2 ]; then missing_value '--api-version'; fi
shift
DOCKER_API_VERSION="$1"
;;
--container=*)
CONTAINER_NAME="${1#*=}"
;;
--container)
if [ "$#" -lt 2 ]; then missing_value '--container'; fi
shift
CONTAINER_NAME="$1"
;;
--cmd=*)
CMD_TO_EXEC="${1#*=}"
;;
--cmd)
if [ "$#" -lt 2 ]; then missing_value '--cmd'; fi
shift
CMD_TO_EXEC="$1"
;;
--user=*)
EXEC_USER="${1#*=}"
;;
--user)
if [ "$#" -lt 2 ]; then missing_value '--user'; fi
shift
EXEC_USER="$1"
;;
--shell=*)
EXEC_SHELL="${1#*=}"
;;
--shell)
if [ "$#" -lt 2 ]; then missing_value '--shell'; fi
shift
EXEC_SHELL="$1"
;;
--help)
print_usage
exit 0
;;
--)
shift
break
;;
*)
printf 'Unknown option: %s\n' "$1" >&2
print_usage
exit 1
;;
esac
shift
done
if [ "$#" -gt 0 ]; then
printf 'Unexpected positional arguments: %s\n' "$*" >&2
print_usage
exit 1
fi
require_command curl
require_command jq
if [ -z "$DOCKER_SOCKET" ] || [ -z "$DOCKER_API_VERSION" ] || [ -z "$CONTAINER_NAME" ] || [ -z "$CMD_TO_EXEC" ]; then
print_usage
exit 1
fi
if [ -z "$EXEC_SHELL" ]; then
EXEC_SHELL="sh"
fi
if [ ! -S "$DOCKER_SOCKET" ]; then
log "docker socket $DOCKER_SOCKET not found"
exit 1
fi
docker_api_base="http://localhost/${DOCKER_API_VERSION}"
exec_create_endpoint="${docker_api_base}/containers/${CONTAINER_NAME}/exec"
if [ -n "$EXEC_USER" ]; then
log "executing command in container '$CONTAINER_NAME' as '$EXEC_USER': $CMD_TO_EXEC"
else
log "executing command in container '$CONTAINER_NAME': $CMD_TO_EXEC"
fi
exec_id=$(docker_exec_create "command" "$CMD_TO_EXEC")
if [ -z "$exec_id" ]; then
exit 1
fi
log "starting exec $exec_id"
output=$(docker_exec_start "$exec_id")
exit_code=$(docker_exec_exit_code "$exec_id")
if [ "$exit_code" = "0" ]; then
log "command finished successfully"
# 将纯净的输出打印到 stdout
printf '%s' "$output"
exit 0
else
log "command failed with exit code $exit_code"
log "output from container:"
# 将错误输出打印到 stderr
echo "$output" >&2
exit "$exit_code"
fi