diff --git a/docker-exec-via-sock.sh b/docker-exec-via-sock.sh new file mode 100644 index 0000000..393d9e4 --- /dev/null +++ b/docker-exec-via-sock.sh @@ -0,0 +1,207 @@ +#!/bin/sh +set -eu + +# 通过 Docker Engine unix 套接字在容器中执行命令。 +# 示例: ./docker-exec-via-sock.sh --container=my-container --cmd="ls -l /" + +log() { + printf '%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" +} + +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 <&2 +Usage: $0 [--socket=PATH] [--api-version=VERSION] --container=NAME --cmd=COMMAND + +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 + --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="" + +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" '{ + AttachStdin: false, + AttachStdout: true, + AttachStderr: true, + Tty: true, + Cmd: ["bash", "-lc", $cmd] + }') + + DOCKER_LAST_RESPONSE=$(curl --fail --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 + log "failed to create $docker_exec_create_desc exec for $CONTAINER_NAME" + log "docker response: $DOCKER_LAST_RESPONSE" + return 1 + fi + + printf '%s' "$docker_exec_create_id" +} + +docker_exec_start() { + docker_exec_start_id=$1 + docker_exec_start_desc=$2 + docker_exec_start_endpoint="${docker_api_base}/exec/${docker_exec_start_id}/start" + DOCKER_LAST_RESPONSE=$(curl --fail --show-error --silent --unix-socket "$DOCKER_SOCKET" \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"Detach": false, "Tty": true}' \ + "$docker_exec_start_endpoint") + + log_stream "$docker_exec_start_desc output" "$DOCKER_LAST_RESPONSE" + + 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 --fail --silent --show-error --unix-socket "$DOCKER_SOCKET" "$docker_exec_inspect_endpoint") + docker_exec_exit_code_value=$(printf '%s' "$DOCKER_LAST_RESPONSE" | jq -r '.ExitCode // empty') + + if [ -z "$docker_exec_exit_code_value" ]; then + 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" + ;; + --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 [ ! -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" + +log "executing command in container $CONTAINER_NAME" + +if ! exec_id=$(docker_exec_create "command" "$CMD_TO_EXEC"); then + exit 1 +fi + +log "starting exec $exec_id" + +if ! start_output=$(docker_exec_start "$exec_id" "exec"); then + exit 1 +fi + +if ! exit_code=$(docker_exec_exit_code "$exec_id"); then + log "could not determine exec exit code" + log "docker inspect response: $DOCKER_LAST_RESPONSE" + exit 1 +fi + +if [ "$exit_code" != "0" ]; then + log "exec exited with status $exit_code" + log "docker inspect response: $DOCKER_LAST_RESPONSE" + exit "$exit_code" +fi + +log "command finished successfully" \ No newline at end of file